Commit e251e9c0 authored by OptimismBot's avatar OptimismBot Committed by GitHub

Merge pull request #6364 from ethereum-optimism/aj/deploy-dispute-game-e2e

op-e2e: Add ability to deploy dispute game contracts and test that games can be resolved
parents b4921f3e 81d0d336
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
"L1BlockNumber", "L1BlockNumber",
"DisputeGameFactory", "DisputeGameFactory",
"FaultDisputeGame", "FaultDisputeGame",
"AlphabetVM",
"StandardBridge", "StandardBridge",
"CrossDomainMessenger", "CrossDomainMessenger",
"MIPS", "MIPS",
......
This diff is collapsed.
// Code generated - DO NOT EDIT.
// This file is a generated binding and any manual changes will be lost.
package bindings
import (
"encoding/json"
"github.com/ethereum-optimism/optimism/op-bindings/solc"
)
const AlphabetVMStorageLayoutJSON = "{\"storage\":null,\"types\":{}}"
var AlphabetVMStorageLayout = new(solc.StorageLayout)
var AlphabetVMDeployedBin = "0x608060405234801561001057600080fd5b506004361061002b5760003560e01c8063f8e0cb9614610030575b600080fd5b61004361003e366004610157565b610055565b60405190815260200160405180910390f35b60008060007f0000000000000000000000000000000000000000000000000000000000000000878760405161008b9291906101c3565b6040518091039020036100af57600091506100a8868801886101d3565b90506100ce565b6100bb868801886101ec565b9092509050816100ca8161023d565b9250505b816100da826001610275565b6040805160208101939093528201526060016040516020818303038152906040528051906020012092505050949350505050565b60008083601f84011261012057600080fd5b50813567ffffffffffffffff81111561013857600080fd5b60208301915083602082850101111561015057600080fd5b9250929050565b6000806000806040858703121561016d57600080fd5b843567ffffffffffffffff8082111561018557600080fd5b6101918883890161010e565b909650945060208701359150808211156101aa57600080fd5b506101b78782880161010e565b95989497509550505050565b8183823760009101908152919050565b6000602082840312156101e557600080fd5b5035919050565b600080604083850312156101ff57600080fd5b50508035926020909101359150565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff820361026e5761026e61020e565b5060010190565b600082198211156102885761028861020e565b50019056fea164736f6c634300080f000a"
func init() {
if err := json.Unmarshal([]byte(AlphabetVMStorageLayoutJSON), AlphabetVMStorageLayout); err != nil {
panic(err)
}
layouts["AlphabetVM"] = AlphabetVMStorageLayout
deployedBytecodes["AlphabetVM"] = AlphabetVMDeployedBin
}
...@@ -15,7 +15,7 @@ import ( ...@@ -15,7 +15,7 @@ import (
) )
// Main is the programmatic entry-point for running op-challenger // Main is the programmatic entry-point for running op-challenger
func Main(logger log.Logger, cfg *config.Config) error { func Main(ctx context.Context, logger log.Logger, cfg *config.Config) error {
client, err := ethclient.Dial(cfg.L1EthRpc) client, err := ethclient.Dial(cfg.L1EthRpc)
if err != nil { if err != nil {
return fmt.Errorf("failed to dial L1: %w", err) return fmt.Errorf("failed to dial L1: %w", err)
...@@ -49,14 +49,19 @@ func Main(logger log.Logger, cfg *config.Config) error { ...@@ -49,14 +49,19 @@ func Main(logger log.Logger, cfg *config.Config) error {
for { for {
logger.Info("Performing action") logger.Info("Performing action")
_ = agent.Act(context.Background()) _ = agent.Act(ctx)
status, _ := caller.GetGameStatus(context.Background()) status, _ := caller.GetGameStatus(ctx)
if status != 0 { if status != 0 {
caller.LogGameStatus() caller.LogGameStatus()
return nil return nil
} else { } else {
caller.LogGameInfo() caller.LogGameInfo()
} }
time.Sleep(300 * time.Millisecond) select {
case <-time.After(300 * time.Millisecond):
// Continue
case <-ctx.Done():
return ctx.Err()
}
} }
} }
package main package main
import ( import (
"context"
"fmt" "fmt"
"os" "os"
...@@ -41,7 +42,7 @@ func main() { ...@@ -41,7 +42,7 @@ func main() {
} }
} }
type ConfigAction func(log log.Logger, config *config.Config) error type ConfigAction func(ctx context.Context, log log.Logger, config *config.Config) error
func run(args []string, action ConfigAction) error { func run(args []string, action ConfigAction) error {
oplog.SetupDefaults() oplog.SetupDefaults()
...@@ -63,7 +64,7 @@ func run(args []string, action ConfigAction) error { ...@@ -63,7 +64,7 @@ func run(args []string, action ConfigAction) error {
if err != nil { if err != nil {
return err return err
} }
return action(logger, cfg) return action(ctx.Context, logger, cfg)
} }
return app.Run(args) return app.Run(args)
} }
......
package main package main
import ( import (
"context"
"fmt" "fmt"
"testing" "testing"
...@@ -137,7 +138,7 @@ func runWithArgs(cliArgs []string) (log.Logger, config.Config, error) { ...@@ -137,7 +138,7 @@ func runWithArgs(cliArgs []string) (log.Logger, config.Config, error) {
cfg := new(config.Config) cfg := new(config.Config)
var logger log.Logger var logger log.Logger
fullArgs := append([]string{"op-challenger"}, cliArgs...) fullArgs := append([]string{"op-challenger"}, cliArgs...)
err := run(fullArgs, func(log log.Logger, config *config.Config) error { err := run(fullArgs, func(ctx context.Context, log log.Logger, config *config.Config) error {
logger = log logger = log
cfg = config cfg = config
return nil return nil
......
package challenger
import (
"context"
"errors"
"testing"
"time"
op_challenger "github.com/ethereum-optimism/optimism/op-challenger"
"github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
type Helper struct {
log log.Logger
cancel func()
errors chan error
}
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)
txmgrCfg := txmgr.NewCLIConfig(l1Endpoint)
txmgrCfg.NumConfirmations = 1
txmgrCfg.ReceiptQueryInterval = 1 * time.Second
cfg := &config.Config{
L1EthRpc: l1Endpoint,
AlphabetTrace: "",
AgreeWithProposedOutput: true,
TxMgrConfig: txmgrCfg,
}
for _, option := range options {
option(cfg)
}
require.NotEmpty(t, cfg.TxMgrConfig.PrivateKey, "Missing private key for TxMgrConfig")
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 (h *Helper) Close() error {
h.cancel()
select {
case <-time.After(1 * time.Minute):
return errors.New("timed out while stopping challenger")
case err := <-h.errors:
if !errors.Is(err, context.Canceled) {
return err
}
return nil
}
}
package disputegame
import (
"context"
"math/big"
"time"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-chain-ops/deployer"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require"
)
// deployDisputeGameContracts deploys the DisputeGameFactory, AlphabetVM and FaultDisputeGame contracts
// It configures the alphabet fault game as game type 0 (faultGameType)
// If/when the dispute game factory becomes a predeployed contract this can be removed and just use the
// predeployed version
func deployDisputeGameContracts(require *require.Assertions, ctx context.Context, client *ethclient.Client, opts *bind.TransactOpts, gameDuration uint64) *bindings.DisputeGameFactory {
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
// Deploy the proxy
_, tx, proxy, err := bindings.DeployProxy(opts, client, deployer.TestAddress)
require.NoError(err)
proxyAddr, err := bind.WaitDeployed(ctx, client, tx)
require.NoError(err)
// Deploy the dispute game factory implementation
_, tx, _, err = bindings.DeployDisputeGameFactory(opts, client)
require.NoError(err)
factoryAddr, err := bind.WaitDeployed(ctx, client, tx)
require.NoError(err)
// Point the proxy at the implementation and create bindings going via the proxy
disputeGameFactoryAbi, err := bindings.DisputeGameFactoryMetaData.GetAbi()
require.NoError(err)
data, err := disputeGameFactoryAbi.Pack("initialize", deployer.TestAddress)
require.NoError(err)
_, err = proxy.UpgradeToAndCall(opts, factoryAddr, data)
require.NoError(err)
factory, err := bindings.NewDisputeGameFactory(proxyAddr, client)
require.NoError(err)
// Now setup the fault dispute game type
// Start by deploying the AlphabetVM
_, tx, _, err = bindings.DeployAlphabetVM(opts, client, alphabetVMAbsolutePrestate)
require.NoError(err)
alphaVMAddr, err := bind.WaitDeployed(ctx, client, tx)
require.NoError(err)
// Deploy the fault dispute game implementation
_, tx, _, err = bindings.DeployFaultDisputeGame(opts, client, alphabetVMAbsolutePrestate, big.NewInt(alphabetGameDepth), gameDuration, alphaVMAddr)
require.NoError(err)
faultDisputeGameAddr, err := bind.WaitDeployed(ctx, client, tx)
require.NoError(err)
// Set the fault game type implementation
_, err = factory.SetImplementation(opts, faultGameType, faultDisputeGameAddr)
require.NoError(err)
return factory
}
package disputegame
import (
"context"
"fmt"
"math"
"math/big"
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"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-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-service/client/utils"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/holiman/uint256"
"github.com/stretchr/testify/require"
)
const faultGameType uint8 = 0
const alphabetGameDepth = 4
type Status uint8
const (
StatusInProgress Status = iota
StatusChallengerWins
StatusDefenderWins
)
var alphaExtraData = common.Hex2Bytes("1000000000000000000000000000000000000000000000000000000000000000")
var alphabetVMAbsolutePrestate = uint256.NewInt(96).Bytes32()
type FactoryHelper struct {
t *testing.T
require *require.Assertions
client *ethclient.Client
opts *bind.TransactOpts
factory *bindings.DisputeGameFactory
}
func NewFactoryHelper(t *testing.T, ctx context.Context, client *ethclient.Client, gameDuration uint64) *FactoryHelper {
require := require.New(t)
chainID, err := client.ChainID(ctx)
require.NoError(err)
opts, err := bind.NewKeyedTransactorWithChainID(deployer.TestKey, chainID)
require.NoError(err)
factory := deployDisputeGameContracts(require, ctx, client, opts, gameDuration)
return &FactoryHelper{
t: t,
require: require,
client: client,
opts: opts,
factory: factory,
}
}
func (h *FactoryHelper) StartAlphabetGame(ctx context.Context, claimedAlphabet string) *FaultGameHelper {
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
trace := fault.NewAlphabetProvider(claimedAlphabet, 4)
rootClaim, err := trace.Get(uint64(math.Pow(2, alphabetGameDepth)) - 1)
h.require.NoError(err)
tx, err := h.factory.Create(h.opts, faultGameType, rootClaim, alphaExtraData)
h.require.NoError(err)
rcpt, err := utils.WaitReceiptOK(ctx, h.client, tx.Hash())
h.require.NoError(err)
h.require.Len(rcpt.Logs, 1, "should have emitted a single DisputeGameCreated event")
createdEvent, err := h.factory.ParseDisputeGameCreated(*rcpt.Logs[0])
h.require.NoError(err)
game, err := bindings.NewFaultDisputeGame(createdEvent.DisputeProxy, h.client)
h.require.NoError(err)
return &FaultGameHelper{
t: h.t,
require: h.require,
client: h.client,
opts: h.opts,
game: game,
addr: createdEvent.DisputeProxy,
claimedAlphabet: claimedAlphabet,
}
}
type FaultGameHelper struct {
t *testing.T
require *require.Assertions
client *ethclient.Client
opts *bind.TransactOpts
game *bindings.FaultDisputeGame
addr common.Address
claimedAlphabet string
}
func (g *FaultGameHelper) StartChallenger(ctx context.Context, l1Endpoint string, name string, options ...challenger.Option) *challenger.Helper {
opts := []challenger.Option{
func(c *config.Config) {
c.GameAddress = g.addr
c.GameDepth = alphabetGameDepth
// 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
c.AgreeWithProposedOutput = false
},
}
opts = append(opts, options...)
return challenger.NewChallenger(g.t, ctx, l1Endpoint, name, opts...)
}
func (g *FaultGameHelper) WaitForClaimCount(ctx context.Context, count int64) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Minute)
defer cancel()
err := utils.WaitFor(ctx, 1*time.Second, func() (bool, error) {
actual, err := g.game.ClaimDataLen(&bind.CallOpts{Context: ctx})
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
})
g.require.NoError(err)
}
func (g *FaultGameHelper) Resolve(ctx context.Context) {
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
tx, err := g.game.Resolve(g.opts)
g.require.NoError(err)
_, err = utils.WaitReceiptOK(ctx, g.client, tx.Hash())
g.require.NoError(err)
}
func (g *FaultGameHelper) WaitForGameStatus(ctx context.Context, expected Status) {
ctx, cancel := context.WithTimeout(ctx, 1*time.Minute)
defer cancel()
err := utils.WaitFor(ctx, 1*time.Second, func() (bool, error) {
ctx, cancel := context.WithTimeout(ctx, 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)
}
return expected == Status(status), nil
})
g.require.NoError(err, "wait for game status")
}
...@@ -5,37 +5,48 @@ import ( ...@@ -5,37 +5,48 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame"
"github.com/ethereum-optimism/optimism/op-service/client/utils" "github.com/ethereum-optimism/optimism/op-service/client/utils"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestTimeTravel(t *testing.T) { func TestResolveDisputeGame(t *testing.T) {
InitParallel(t) InitParallel(t)
ctx := context.Background()
cfg := DefaultSystemConfig(t) cfg := DefaultSystemConfig(t)
cfg.DeployConfig.L1BlockTime = 1
delete(cfg.Nodes, "verifier") delete(cfg.Nodes, "verifier")
delete(cfg.Nodes, "sequencer")
cfg.SupportL1TimeTravel = true cfg.SupportL1TimeTravel = true
sys, err := cfg.Start() sys, err := cfg.Start()
require.Nil(t, err, "Error starting up system") require.Nil(t, err, "Error starting up system")
defer sys.Close() defer sys.Close()
l1Client := sys.Clients["l1"] l1Client := sys.Clients["l1"]
preTravel, err := l1Client.BlockByNumber(context.Background(), nil) gameDuration := 24 * time.Hour
require.NoError(t, err) disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, l1Client, uint64(gameDuration.Seconds()))
game := disputeGameFactory.StartAlphabetGame(ctx, "zyxwvut")
sys.TimeTravelClock.AdvanceTime(24 * time.Hour) require.NotNil(t, game)
// Check that the L1 chain reaches the new time reasonably quickly (ie without taking a week) game.WaitForGameStatus(ctx, disputegame.StatusInProgress)
// It should be able to jump straight to the new time with just a single block
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) honest := game.StartChallenger(ctx, sys.NodeEndpoint("l1"), "honestAlice", func(c *config.Config) {
defer cancel() c.AgreeWithProposedOutput = true // Agree with the proposed output, so disagree with the root claim
err = utils.WaitFor(ctx, time.Second, func() (bool, error) { c.AlphabetTrace = "abcdefg"
postTravel, err := l1Client.BlockByNumber(context.Background(), nil) c.TxMgrConfig.PrivateKey = hexutil.Encode(e2eutils.EncodePrivKey(cfg.Secrets.Alice))
if err != nil {
return false, err
}
diff := time.Duration(postTravel.Time()-preTravel.Time()) * time.Second
return diff.Hours() > 23, nil
}) })
require.NoError(t, err) defer honest.Close()
game.WaitForClaimCount(ctx, 2)
sys.TimeTravelClock.AdvanceTime(gameDuration)
require.NoError(t, utils.WaitNextBlock(ctx, l1Client))
// Challenger should resolve the game now that the clocks have expired.
game.WaitForGameStatus(ctx, disputegame.StatusChallengerWins)
require.NoError(t, honest.Close())
} }
...@@ -616,6 +616,10 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) { ...@@ -616,6 +616,10 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) {
} }
} }
// Don't start batch submitter and proposer if there's no sequencer.
if sys.RollupNodes["sequencer"] == nil {
return sys, nil
}
// L2Output Submitter // L2Output Submitter
sys.L2OutputSubmitter, err = l2os.NewL2OutputSubmitterFromCLIConfig(l2os.CLIConfig{ sys.L2OutputSubmitter, err = l2os.NewL2OutputSubmitterFromCLIConfig(l2os.CLIConfig{
L1EthRpc: sys.Nodes["l1"].WSEndpoint(), L1EthRpc: sys.Nodes["l1"].WSEndpoint(),
......
...@@ -59,6 +59,14 @@ func WaitBlock(ctx context.Context, client *ethclient.Client, n uint64) error { ...@@ -59,6 +59,14 @@ func WaitBlock(ctx context.Context, client *ethclient.Client, n uint64) error {
return nil return nil
} }
func WaitNextBlock(ctx context.Context, client *ethclient.Client) error {
current, err := client.BlockNumber(ctx)
if err != nil {
return fmt.Errorf("get starting block number: %w", err)
}
return WaitBlock(ctx, client, current+1)
}
func WaitFor(ctx context.Context, rate time.Duration, cb func() (bool, error)) error { func WaitFor(ctx context.Context, rate time.Duration, cb func() (bool, error)) error {
tick := time.NewTicker(rate) tick := time.NewTicker(rate)
defer tick.Stop() defer tick.Stop()
......
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