Commit 0a6023cb authored by Adrian Sutton's avatar Adrian Sutton

op-challenger: Add e2e test that disputes an output root by agreeing with it

Tests the corner case where both sides have the same trace that says the output root is valid but the malicious user has disputed the original output root anyway.
parent ee860521
......@@ -32,7 +32,7 @@ type Executor struct {
logger log.Logger
l1 string
l2 string
inputs localGameInputs
inputs LocalGameInputs
cannon string
server string
network string
......@@ -45,7 +45,7 @@ type Executor struct {
cmdExecutor cmdExecutor
}
func NewExecutor(logger log.Logger, cfg *config.Config, inputs localGameInputs) *Executor {
func NewExecutor(logger log.Logger, cfg *config.Config, inputs LocalGameInputs) *Executor {
return &Executor{
logger: logger,
l1: cfg.L1EthRpc,
......@@ -92,11 +92,11 @@ func (e *Executor) GenerateProof(ctx context.Context, dir string, i uint64) erro
"--l1", e.l1,
"--l2", e.l2,
"--datadir", dataDir,
"--l1.head", e.inputs.l1Head.Hex(),
"--l2.head", e.inputs.l2Head.Hex(),
"--l2.outputroot", e.inputs.l2OutputRoot.Hex(),
"--l2.claim", e.inputs.l2Claim.Hex(),
"--l2.blocknumber", e.inputs.l2BlockNumber.Text(10),
"--l1.head", e.inputs.L1Head.Hex(),
"--l2.head", e.inputs.L2Head.Hex(),
"--l2.outputroot", e.inputs.L2OutputRoot.Hex(),
"--l2.claim", e.inputs.L2Claim.Hex(),
"--l2.blocknumber", e.inputs.L2BlockNumber.Text(10),
)
if e.network != "" {
args = append(args, "--network", e.network)
......
......@@ -29,12 +29,12 @@ func TestGenerateProof(t *testing.T) {
cfg.CannonL2 = "http://localhost:9999"
cfg.CannonSnapshotFreq = 500
inputs := localGameInputs{
l1Head: common.Hash{0x11},
l2Head: common.Hash{0x22},
l2OutputRoot: common.Hash{0x33},
l2Claim: common.Hash{0x44},
l2BlockNumber: big.NewInt(3333),
inputs := LocalGameInputs{
L1Head: common.Hash{0x11},
L2Head: common.Hash{0x22},
L2OutputRoot: common.Hash{0x33},
L2Claim: common.Hash{0x44},
L2BlockNumber: big.NewInt(3333),
}
captureExec := func(t *testing.T, cfg config.Config, proofAt uint64) (string, string, map[string]string) {
executor := NewExecutor(testlog.Logger(t, log.LvlInfo), &cfg, inputs)
......@@ -94,10 +94,10 @@ func TestGenerateProof(t *testing.T) {
require.NotContains(t, args, "--l2.genesis")
// Local game inputs
require.Equal(t, inputs.l1Head.Hex(), args["--l1.head"])
require.Equal(t, inputs.l2Head.Hex(), args["--l2.head"])
require.Equal(t, inputs.l2OutputRoot.Hex(), args["--l2.outputroot"])
require.Equal(t, inputs.l2Claim.Hex(), args["--l2.claim"])
require.Equal(t, inputs.L1Head.Hex(), args["--l1.head"])
require.Equal(t, inputs.L2Head.Hex(), args["--l2.head"])
require.Equal(t, inputs.L2OutputRoot.Hex(), args["--l2.outputroot"])
require.Equal(t, inputs.L2Claim.Hex(), args["--l2.claim"])
require.Equal(t, "3333", args["--l2.blocknumber"])
})
......
......@@ -11,12 +11,12 @@ import (
ethtypes "github.com/ethereum/go-ethereum/core/types"
)
type localGameInputs struct {
l1Head common.Hash
l2Head common.Hash
l2OutputRoot common.Hash
l2Claim common.Hash
l2BlockNumber *big.Int
type LocalGameInputs struct {
L1Head common.Hash
L2Head common.Hash
L2OutputRoot common.Hash
L2Claim common.Hash
L2BlockNumber *big.Int
}
type L2DataSource interface {
......@@ -32,30 +32,30 @@ type GameInputsSource interface {
}, error)
}
func fetchLocalInputs(ctx context.Context, gameAddr common.Address, caller GameInputsSource, l2Client L2DataSource) (localGameInputs, error) {
func fetchLocalInputs(ctx context.Context, gameAddr common.Address, caller GameInputsSource, l2Client L2DataSource) (LocalGameInputs, error) {
opts := &bind.CallOpts{Context: ctx}
l1Head, err := caller.L1Head(opts)
if err != nil {
return localGameInputs{}, fmt.Errorf("fetch L1 head for game %v: %w", gameAddr, err)
return LocalGameInputs{}, fmt.Errorf("fetch L1 head for game %v: %w", gameAddr, err)
}
proposals, err := caller.Proposals(opts)
if err != nil {
return localGameInputs{}, fmt.Errorf("fetch proposals: %w", err)
return LocalGameInputs{}, fmt.Errorf("fetch proposals: %w", err)
}
claimedOutput := proposals.Disputed
agreedOutput := proposals.Starting
agreedHeader, err := l2Client.HeaderByNumber(ctx, agreedOutput.L2BlockNumber)
if err != nil {
return localGameInputs{}, fmt.Errorf("fetch L2 block header %v: %w", agreedOutput.L2BlockNumber, err)
return LocalGameInputs{}, fmt.Errorf("fetch L2 block header %v: %w", agreedOutput.L2BlockNumber, err)
}
l2Head := agreedHeader.Hash()
return localGameInputs{
l1Head: l1Head,
l2Head: l2Head,
l2OutputRoot: agreedOutput.OutputRoot,
l2Claim: claimedOutput.OutputRoot,
l2BlockNumber: claimedOutput.L2BlockNumber,
return LocalGameInputs{
L1Head: l1Head,
L2Head: l2Head,
L2OutputRoot: agreedOutput.OutputRoot,
L2Claim: claimedOutput.OutputRoot,
L2BlockNumber: claimedOutput.L2BlockNumber,
}, nil
}
......@@ -39,11 +39,11 @@ func TestFetchLocalInputs(t *testing.T) {
inputs, err := fetchLocalInputs(ctx, gameAddr, l1Client, l2Client)
require.NoError(t, err)
require.Equal(t, l1Client.l1Head, inputs.l1Head)
require.Equal(t, l2Client.header.Hash(), inputs.l2Head)
require.EqualValues(t, l1Client.starting.OutputRoot, inputs.l2OutputRoot)
require.EqualValues(t, l1Client.disputed.OutputRoot, inputs.l2Claim)
require.Equal(t, l1Client.disputed.L2BlockNumber, inputs.l2BlockNumber)
require.Equal(t, l1Client.l1Head, inputs.L1Head)
require.Equal(t, l2Client.header.Hash(), inputs.L2Head)
require.EqualValues(t, l1Client.starting.OutputRoot, inputs.L2OutputRoot)
require.EqualValues(t, l1Client.disputed.OutputRoot, inputs.L2Claim)
require.Equal(t, l1Client.disputed.L2BlockNumber, inputs.L2BlockNumber)
}
type mockGameInputsSource struct {
......
......@@ -60,16 +60,20 @@ func NewTraceProvider(ctx context.Context, logger log.Logger, cfg *config.Config
if err != nil {
return nil, fmt.Errorf("create caller for game %v: %w", cfg.GameAddress, err)
}
l1Head, err := fetchLocalInputs(ctx, cfg.GameAddress, gameCaller, l2Client)
localInputs, err := fetchLocalInputs(ctx, cfg.GameAddress, gameCaller, l2Client)
if err != nil {
return nil, fmt.Errorf("fetch local game inputs: %w", err)
}
return NewTraceProviderFromInputs(logger, cfg, localInputs), nil
}
func NewTraceProviderFromInputs(logger log.Logger, cfg *config.Config, localInputs LocalGameInputs) *CannonTraceProvider {
return &CannonTraceProvider{
logger: logger,
dir: cfg.CannonDatadir,
prestate: cfg.CannonAbsolutePreState,
generator: NewExecutor(logger, cfg, l1Head),
}, nil
generator: NewExecutor(logger, cfg, localInputs),
}
}
func (p *CannonTraceProvider) Get(ctx context.Context, i uint64) (common.Hash, error) {
......
......@@ -36,6 +36,11 @@ func (s *Solver) NextMove(ctx context.Context, claim types.Claim, agreeWithClaim
if claim.Depth() == s.gameDepth {
return nil, types.ErrGameDepthReached
}
if claim.IsRoot() {
// We can't defend the root claim so attack regardless of whether we agree or not
// This isn't necessarily the right solution...
return s.attack(ctx, claim)
}
agree, err := s.agreeWithClaim(ctx, claim.ClaimData)
if err != nil {
return nil, err
......
......@@ -5,6 +5,7 @@ import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-challenger/fault/cannon"
......@@ -12,8 +13,10 @@ import (
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
type CannonGameHelper struct {
......@@ -21,7 +24,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{g.createConfigOption(rollupCfg, l2Genesis, l2Endpoint)}
opts := []challenger.Option{createConfigOption(g.t, rollupCfg, l2Genesis, g.addr, l2Endpoint)}
opts = append(opts, options...)
c := challenger.NewChallenger(g.t, ctx, l1Endpoint, name, opts...)
g.t.Cleanup(func() {
......@@ -31,10 +34,10 @@ func (g *CannonGameHelper) StartChallenger(ctx context.Context, rollupCfg *rollu
}
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 := []challenger.Option{createConfigOption(g.t, rollupCfg, l2Genesis, g.addr, 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)
provider, err := cannon.NewTraceProvider(ctx, testlog.Logger(g.t, log.LvlInfo).New("role", "CorrectTrace"), cfg, l1Client)
g.require.NoError(err, "create cannon trace provider")
return &HonestHelper{
......@@ -45,28 +48,29 @@ func (g *CannonGameHelper) CreateHonestActor(ctx context.Context, rollupCfg *rol
}
}
func (g *CannonGameHelper) createConfigOption(rollupCfg *rollup.Config, l2Genesis *core.Genesis, l2Endpoint string) challenger.Option {
func createConfigOption(t *testing.T, rollupCfg *rollup.Config, l2Genesis *core.Genesis, gameAddr common.Address, l2Endpoint string) challenger.Option {
return func(c *config.Config) {
c.GameAddress = g.addr
require := require.New(t)
c.GameAddress = gameAddr
c.TraceType = config.TraceTypeCannon
c.AgreeWithProposedOutput = false
c.CannonL2 = l2Endpoint
c.CannonBin = "../cannon/bin/cannon"
c.CannonDatadir = g.t.TempDir()
c.CannonDatadir = 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")
require.NoError(err, "marshall l2 genesis config")
genesisFile := filepath.Join(c.CannonDatadir, "l2-genesis.json")
g.require.NoError(os.WriteFile(genesisFile, genesisBytes, 0644))
require.NoError(os.WriteFile(genesisFile, genesisBytes, 0644))
c.CannonL2GenesisPath = genesisFile
rollupBytes, err := json.Marshal(rollupCfg)
g.require.NoError(err, "marshall rollup config")
require.NoError(err, "marshall rollup config")
rollupFile := filepath.Join(c.CannonDatadir, "rollup.json")
g.require.NoError(os.WriteFile(rollupFile, rollupBytes, 0644))
require.NoError(os.WriteFile(rollupFile, rollupBytes, 0644))
c.CannonRollupConfigPath = rollupFile
}
}
......@@ -42,7 +42,7 @@ func (g *FaultGameHelper) WaitForClaimCount(ctx context.Context, count int64) {
g.t.Log("Waiting for claim count", "current", actual, "expected", count, "game", g.addr)
return actual.Cmp(big.NewInt(count)) == 0, nil
})
g.require.NoError(err)
g.require.NoErrorf(err, "Did not find expected claim count %v", count)
}
type ContractClaim struct {
......
......@@ -4,6 +4,7 @@ import (
"context"
"encoding/binary"
"fmt"
"math"
"math/big"
"testing"
"time"
......@@ -13,11 +14,16 @@ import (
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-challenger/fault/alphabet"
"github.com/ethereum-optimism/optimism/op-challenger/fault/cannon"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
......@@ -125,9 +131,57 @@ func (h *FactoryHelper) StartAlphabetGame(ctx context.Context, claimedAlphabet s
}
func (h *FactoryHelper) StartCannonGame(ctx context.Context, rootClaim common.Hash) *CannonGameHelper {
l2BlockNumber := h.waitForProposals(ctx)
l1Head := h.checkpointL1Block(ctx)
l2BlockNumber, l1Head := h.prepareCannonGame(ctx)
return h.createCannonGame(ctx, l2BlockNumber, l1Head, rootClaim)
}
func (h *FactoryHelper) StartCannonGameWithCorrectRoot(ctx context.Context, rollupCfg *rollup.Config, l2Genesis *core.Genesis, l1Endpoint string, l2Endpoint string, options ...challenger.Option) (*CannonGameHelper, *HonestHelper) {
l2BlockNumber, l1Head := h.prepareCannonGame(ctx)
challengerOpts := []challenger.Option{createConfigOption(h.t, rollupCfg, l2Genesis, common.Address{0xaa}, l2Endpoint)}
challengerOpts = append(challengerOpts, options...)
cfg := challenger.NewChallengerConfig(h.t, l1Endpoint, challengerOpts...)
opts := &bind.CallOpts{Context: ctx}
outputIdx, err := h.l2oo.GetL2OutputIndexAfter(opts, new(big.Int).SetUint64(l2BlockNumber))
h.require.NoError(err, "Fetch challenged output index")
challengedOutput, err := h.l2oo.GetL2Output(opts, outputIdx)
h.require.NoError(err, "Fetch challenged output")
agreedOutput, err := h.l2oo.GetL2Output(opts, new(big.Int).Sub(outputIdx, common.Big1))
h.require.NoError(err, "Fetch agreed output")
l1BlockInfo, err := h.blockOracle.Load(opts, l1Head)
h.require.NoError(err, "Fetch L1 block info")
l2Client, err := ethclient.DialContext(ctx, cfg.CannonL2)
if err != nil {
h.require.NoErrorf(err, "Failed to dial l2 client %v", l2Endpoint)
}
defer l2Client.Close()
agreedHeader, err := l2Client.HeaderByNumber(ctx, agreedOutput.L2BlockNumber)
if err != nil {
h.require.NoErrorf(err, "Failed to fetch L2 block header %v", agreedOutput.L2BlockNumber)
}
inputs := cannon.LocalGameInputs{
L1Head: l1BlockInfo.Hash,
L2Head: agreedHeader.Hash(),
L2OutputRoot: agreedOutput.OutputRoot,
L2Claim: challengedOutput.OutputRoot,
L2BlockNumber: challengedOutput.L2BlockNumber,
}
provider := cannon.NewTraceProviderFromInputs(testlog.Logger(h.t, log.LvlInfo).New("role", "CorrectTrace"), cfg, inputs)
rootClaim, err := provider.Get(ctx, math.MaxUint64)
h.require.NoError(err, "Compute correct root hash")
game := h.createCannonGame(ctx, l2BlockNumber, l1Head, rootClaim)
honestHelper := &HonestHelper{
t: h.t,
require: h.require,
game: &game.FaultGameHelper,
correctTrace: provider,
}
return game, honestHelper
}
func (h *FactoryHelper) createCannonGame(ctx context.Context, l2BlockNumber uint64, l1Head *big.Int, rootClaim common.Hash) *CannonGameHelper {
ctx, cancel := context.WithTimeout(ctx, 1*time.Minute)
defer cancel()
......@@ -171,6 +225,12 @@ func (h *FactoryHelper) StartChallenger(ctx context.Context, l1Endpoint string,
return c
}
func (h *FactoryHelper) prepareCannonGame(ctx context.Context) (uint64, *big.Int) {
l2BlockNumber := h.waitForProposals(ctx)
l1Head := h.checkpointL1Block(ctx)
return l2BlockNumber, l1Head
}
// waitForProposals waits until there are at least two proposals in the output oracle
// This is the minimum required for creating a game.
// Returns the l2 block number of the latest available proposal
......
......@@ -290,6 +290,53 @@ func TestCannonDefendStep(t *testing.T) {
game.LogGameData(ctx)
}
func TestCannonChallengeWithCorrectRoot(t *testing.T) {
InitParallel(t)
ctx := context.Background()
sys, l1Client := startFaultDisputeSystem(t)
t.Cleanup(sys.Close)
l1Endpoint := sys.NodeEndpoint("l1")
l2Endpoint := sys.NodeEndpoint("sequencer")
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys.cfg.L1Deployments, l1Client)
game, correctTrace := disputeGameFactory.StartCannonGameWithCorrectRoot(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint, func(c *config.Config) {
c.TxMgrConfig.PrivateKey = e2eutils.EncodePrivKeyToString(sys.cfg.Secrets.Mallory)
})
require.NotNil(t, game)
game.LogGameData(ctx)
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)
})
maxDepth := game.MaxDepth(ctx)
for claimCount := int64(1); claimCount < maxDepth; {
game.LogGameData(ctx)
claimCount++
// Wait for the challenger to counter
game.WaitForClaimCount(ctx, claimCount)
// Defend everything because we have the same trace as the honest proposer
correctTrace.Defend(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, wait.ForNextBlock(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