Commit 4a083276 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-e2e: Add a cannon e2e test that executes a defend step (#6709)

Establishes infrastructure for e2e tests to create correct cannon trace values.
parent 818d7592
......@@ -53,7 +53,7 @@ type CannonTraceProvider struct {
func NewTraceProvider(ctx context.Context, logger log.Logger, cfg *config.Config, l1Client bind.ContractCaller) (*CannonTraceProvider, error) {
l2Client, err := ethclient.DialContext(ctx, cfg.CannonL2)
if err != nil {
return nil, fmt.Errorf("dial l2 cleint %v: %w", cfg.CannonL2, err)
return nil, fmt.Errorf("dial l2 client %v: %w", cfg.CannonL2, err)
}
defer l2Client.Close() // Not needed after fetching the inputs
gameCaller, err := bindings.NewFaultDisputeGameCaller(cfg.GameAddress, l1Client)
......
......@@ -26,6 +26,22 @@ type Option func(config2 *config.Config)
func NewChallenger(t *testing.T, ctx context.Context, l1Endpoint string, name string, options ...Option) *Helper {
log := testlog.Logger(t, log.LvlInfo).New("role", name)
log.Info("Creating challenger", "l1", l1Endpoint)
cfg := NewChallengerConfig(t, l1Endpoint, options...)
errCh := make(chan error, 1)
ctx, cancel := context.WithCancel(ctx)
go func() {
defer close(errCh)
errCh <- op_challenger.Main(ctx, log, cfg)
}()
return &Helper{
log: log,
cancel: cancel,
errors: errCh,
}
}
func NewChallengerConfig(t *testing.T, l1Endpoint string, options ...Option) *config.Config {
txmgrCfg := txmgr.NewCLIConfig(l1Endpoint)
txmgrCfg.NumConfirmations = 1
txmgrCfg.ReceiptQueryInterval = 1 * time.Second
......@@ -53,18 +69,7 @@ func NewChallenger(t *testing.T, ctx context.Context, l1Endpoint string, name st
_, err := os.Stat(cfg.CannonAbsolutePreState)
require.NoError(t, err, "cannon pre-state should be built. Make sure you've run make cannon-prestate")
}
errCh := make(chan error, 1)
ctx, cancel := context.WithCancel(ctx)
go func() {
defer close(errCh)
errCh <- op_challenger.Main(ctx, log, cfg)
}()
return &Helper{
log: log,
cancel: cancel,
errors: errCh,
}
return cfg
}
func (h *Helper) Close() error {
......
......@@ -7,10 +7,13 @@ import (
"path/filepath"
"github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-challenger/fault/cannon"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/log"
)
type CannonGameHelper struct {
......@@ -18,31 +21,7 @@ type CannonGameHelper struct {
}
func (g *CannonGameHelper) StartChallenger(ctx context.Context, rollupCfg *rollup.Config, l2Genesis *core.Genesis, l1Endpoint string, l2Endpoint string, name string, options ...challenger.Option) *challenger.Helper {
opts := []challenger.Option{
func(c *config.Config) {
c.GameAddress = g.addr
c.TraceType = config.TraceTypeCannon
c.AgreeWithProposedOutput = false
c.CannonL2 = l2Endpoint
c.CannonBin = "../cannon/bin/cannon"
c.CannonDatadir = g.t.TempDir()
c.CannonServer = "../op-program/bin/op-program"
c.CannonAbsolutePreState = "../op-program/bin/prestate.json"
c.CannonSnapshotFreq = 10_000_000
genesisBytes, err := json.Marshal(l2Genesis)
g.require.NoError(err, "marshall l2 genesis config")
genesisFile := filepath.Join(c.CannonDatadir, "l2-genesis.json")
g.require.NoError(os.WriteFile(genesisFile, genesisBytes, 0644))
c.CannonL2GenesisPath = genesisFile
rollupBytes, err := json.Marshal(rollupCfg)
g.require.NoError(err, "marshall rollup config")
rollupFile := filepath.Join(c.CannonDatadir, "rollup.json")
g.require.NoError(os.WriteFile(rollupFile, rollupBytes, 0644))
c.CannonRollupConfigPath = rollupFile
},
}
opts := []challenger.Option{g.createConfigOption(rollupCfg, l2Genesis, l2Endpoint)}
opts = append(opts, options...)
c := challenger.NewChallenger(g.t, ctx, l1Endpoint, name, opts...)
g.t.Cleanup(func() {
......@@ -51,6 +30,43 @@ func (g *CannonGameHelper) StartChallenger(ctx context.Context, rollupCfg *rollu
return c
}
func (g *FaultGameHelper) Address() common.Address {
return g.addr
func (g *CannonGameHelper) CreateHonestActor(ctx context.Context, rollupCfg *rollup.Config, l2Genesis *core.Genesis, l1Client bind.ContractCaller, l1Endpoint string, l2Endpoint string, options ...challenger.Option) *HonestHelper {
opts := []challenger.Option{g.createConfigOption(rollupCfg, l2Genesis, l2Endpoint)}
opts = append(opts, options...)
cfg := challenger.NewChallengerConfig(g.t, l1Endpoint, opts...)
provider, err := cannon.NewTraceProvider(ctx, testlog.Logger(g.t, log.LvlTrace).New("role", "CorrectTrace"), cfg, l1Client)
g.require.NoError(err, "create cannon trace provider")
return &HonestHelper{
t: g.t,
require: g.require,
game: &g.FaultGameHelper,
correctTrace: provider,
}
}
func (g *CannonGameHelper) createConfigOption(rollupCfg *rollup.Config, l2Genesis *core.Genesis, l2Endpoint string) challenger.Option {
return func(c *config.Config) {
c.GameAddress = g.addr
c.TraceType = config.TraceTypeCannon
c.AgreeWithProposedOutput = false
c.CannonL2 = l2Endpoint
c.CannonBin = "../cannon/bin/cannon"
c.CannonDatadir = g.t.TempDir()
c.CannonServer = "../op-program/bin/op-program"
c.CannonAbsolutePreState = "../op-program/bin/prestate.json"
c.CannonSnapshotFreq = 10_000_000
genesisBytes, err := json.Marshal(l2Genesis)
g.require.NoError(err, "marshall l2 genesis config")
genesisFile := filepath.Join(c.CannonDatadir, "l2-genesis.json")
g.require.NoError(os.WriteFile(genesisFile, genesisBytes, 0644))
c.CannonL2GenesisPath = genesisFile
rollupBytes, err := json.Marshal(rollupCfg)
g.require.NoError(err, "marshall rollup config")
rollupFile := filepath.Join(c.CannonDatadir, "rollup.json")
g.require.NoError(os.WriteFile(rollupFile, rollupBytes, 0644))
c.CannonRollupConfigPath = rollupFile
}
}
......@@ -82,6 +82,16 @@ func (g *FaultGameHelper) WaitForClaim(ctx context.Context, predicate func(claim
g.require.NoError(err)
}
// 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 *FaultGameHelper) 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 *FaultGameHelper) WaitForClaimAtMaxDepth(ctx context.Context, countered bool) {
maxDepth := g.MaxDepth(ctx)
g.WaitForClaim(ctx, func(claim ContractClaim) bool {
......@@ -146,5 +156,5 @@ func (g *FaultGameHelper) LogGameData(ctx context.Context) {
}
status, err := g.game.Status(opts)
g.require.NoError(err, "Load game status")
g.t.Logf("Game %v:\n%v\nCurrent status: %v\n", g.addr, info, Status(status))
g.t.Logf("Game %v (%v):\n%v\n", g.addr, Status(status), info)
}
package disputegame
import (
"context"
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/stretchr/testify/require"
)
type HonestHelper struct {
t *testing.T
require *require.Assertions
game *FaultGameHelper
correctTrace types.TraceProvider
}
func (h *HonestHelper) Attack(ctx context.Context, claimIdx int64) {
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
claim := h.game.getClaim(ctx, claimIdx)
pos := types.NewPositionFromGIndex(claim.Position.Uint64())
attackPos := pos.Attack()
traceIdx := attackPos.TraceIndex(int(h.game.MaxDepth(ctx)))
h.t.Logf("Attacking at position %v using correct trace from index %v", attackPos.ToGIndex(), traceIdx)
value, err := h.correctTrace.Get(ctx, traceIdx)
h.require.NoErrorf(err, "Get correct claim at trace index %v", traceIdx)
h.t.Log("Performing attack")
h.game.Attack(ctx, claimIdx, value)
h.t.Log("Attack complete")
}
func (h *HonestHelper) Defend(ctx context.Context, claimIdx int64) {
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
claim := h.game.getClaim(ctx, claimIdx)
pos := types.NewPositionFromGIndex(claim.Position.Uint64())
defendPos := pos.Defend()
traceIdx := defendPos.TraceIndex(int(h.game.MaxDepth(ctx)))
value, err := h.correctTrace.Get(ctx, traceIdx)
h.game.require.NoErrorf(err, "Get correct claim at trace index %v", traceIdx)
h.game.Defend(ctx, claimIdx, value)
}
......@@ -206,6 +206,59 @@ func TestCannonDisputeGame(t *testing.T) {
}
}
func TestCannonDefendStep(t *testing.T) {
InitParallel(t)
ctx := context.Background()
sys, l1Client := startFaultDisputeSystem(t)
t.Cleanup(sys.Close)
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys.cfg.L1Deployments, l1Client)
game := disputeGameFactory.StartCannonGame(ctx, common.Hash{0xaa})
require.NotNil(t, game)
game.LogGameData(ctx)
l1Endpoint := sys.NodeEndpoint("l1")
l2Endpoint := sys.NodeEndpoint("sequencer")
game.StartChallenger(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint, "Challenger", func(c *config.Config) {
c.AgreeWithProposedOutput = true // Agree with the proposed output, so disagree with the root claim
c.TxMgrConfig.PrivateKey = e2eutils.EncodePrivKeyToString(sys.cfg.Secrets.Alice)
})
correctTrace := game.CreateHonestActor(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Client, l1Endpoint, l2Endpoint, func(c *config.Config) {
c.TxMgrConfig.PrivateKey = e2eutils.EncodePrivKeyToString(sys.cfg.Secrets.Mallory)
})
maxDepth := game.MaxDepth(ctx)
for claimCount := int64(1); claimCount < maxDepth; {
game.LogGameData(ctx)
claimCount++
// Wait for the challenger to counter
game.WaitForClaimCount(ctx, claimCount)
// Post invalid claims for most steps to get down into the early part of the trace
if claimCount < 28 {
game.Attack(ctx, claimCount-1, common.Hash{byte(claimCount)})
} else {
// Post our own counter but using the correct hash in low levels to force a defense step
correctTrace.Attack(ctx, claimCount-1)
}
claimCount++
game.LogGameData(ctx)
game.WaitForClaimCount(ctx, claimCount)
}
game.LogGameData(ctx)
// Wait for the challenger to call step and counter our invalid claim
game.WaitForClaimAtMaxDepth(ctx, true)
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx))
require.NoError(t, utils.WaitNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, disputegame.StatusChallengerWins)
game.LogGameData(ctx)
}
func startFaultDisputeSystem(t *testing.T) (*System, *ethclient.Client) {
cfg := DefaultSystemConfig(t)
delete(cfg.Nodes, "verifier")
......
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