Commit deaa3565 authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge branch 'develop' into op-chain-ops/migrate-fix

parents b3efd63c 9c4eeaaa
......@@ -208,6 +208,11 @@ func (d *DeployConfig) Check() error {
if d.L2GenesisBlockGasLimit == 0 {
return fmt.Errorf("%w: L2 genesis block gas limit cannot be 0", ErrInvalidDeployConfig)
}
// When the initial resource config is made to be configurable by the DeployConfig, ensure
// that this check is updated to use the values from the DeployConfig instead of the defaults.
if uint64(d.L2GenesisBlockGasLimit) < uint64(defaultResourceConfig.MaxResourceLimit+defaultResourceConfig.SystemTxMaxGas) {
return fmt.Errorf("%w: L2 genesis block gas limit is too small", ErrInvalidDeployConfig)
}
if d.L2GenesisBlockBaseFeePerGas == nil {
return fmt.Errorf("%w: L2 genesis block base fee per gas cannot be nil", ErrInvalidDeployConfig)
}
......
......@@ -15,7 +15,8 @@ import (
"github.com/ethereum/go-ethereum/params"
)
const defaultL2GasLimit = 15_000_000
// defaultL2GasLimit represents the default gas limit for an L2 block.
const defaultL2GasLimit = 30_000_000
// NewL2Genesis will create a new L2 genesis
func NewL2Genesis(config *DeployConfig, block *types.Block) (*core.Genesis, error) {
......
......@@ -95,7 +95,7 @@ func FaultProofProgram(logger log.Logger, cfg *config.Config) error {
ctx := context.Background()
logger.Info("Connecting to L2 node", "l2", cfg.L2URL)
_, err := l2.NewFetchingL2Oracle(ctx, logger, cfg.L2URL)
_, err := l2.NewFetchingEngine(ctx, logger, cfg)
if err != nil {
return fmt.Errorf("connect l2 oracle: %w", err)
}
......
......@@ -7,10 +7,13 @@ import (
"github.com/ethereum-optimism/optimism/op-node/chaincfg"
"github.com/ethereum-optimism/optimism/op-program/config"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
var l2HeadValue = "0x6303578b1fa9480389c51bbcef6fe045bb877da39740819e9eb5f36f94949bd0"
func TestLogLevel(t *testing.T) {
t.Run("RejectInvalid", func(t *testing.T) {
verifyArgsInvalid(t, "unknown level: foo", addRequiredArgs("--log.level=foo"))
......@@ -28,7 +31,7 @@ func TestLogLevel(t *testing.T) {
func TestDefaultCLIOptionsMatchDefaultConfig(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs())
require.Equal(t, config.NewConfig(&chaincfg.Goerli), cfg)
require.Equal(t, config.NewConfig(&chaincfg.Goerli, "genesis.json", common.HexToHash(l2HeadValue)), cfg)
}
func TestNetwork(t *testing.T) {
......@@ -72,10 +75,36 @@ func TestL2(t *testing.T) {
require.Equal(t, expected, cfg.L2URL)
}
func TestL2Genesis(t *testing.T) {
t.Run("Required", func(t *testing.T) {
verifyArgsInvalid(t, "flag l2.genesis is required", addRequiredArgsExcept("--l2.genesis"))
})
t.Run("Valid", func(t *testing.T) {
cfg := configForArgs(t, replaceRequiredArg("--l2.genesis", "/tmp/genesis.json"))
require.Equal(t, "/tmp/genesis.json", cfg.L2GenesisPath)
})
}
func TestL2Head(t *testing.T) {
t.Run("Required", func(t *testing.T) {
verifyArgsInvalid(t, "flag l2.head is required", addRequiredArgsExcept("--l2.head"))
})
t.Run("Valid", func(t *testing.T) {
cfg := configForArgs(t, replaceRequiredArg("--l2.head", l2HeadValue))
require.Equal(t, common.HexToHash(l2HeadValue), cfg.L2Head)
})
t.Run("Invalid", func(t *testing.T) {
verifyArgsInvalid(t, config.ErrInvalidL2Head.Error(), replaceRequiredArg("--l2.head", "something"))
})
}
// Offline support will be added later, but for now it just bails out with an error
func TestOfflineModeNotSupported(t *testing.T) {
logger := log.New()
err := FaultProofProgram(logger, config.NewConfig(&chaincfg.Goerli))
err := FaultProofProgram(logger, config.NewConfig(&chaincfg.Goerli, "genesis.json", common.HexToHash(l2HeadValue)))
require.ErrorContains(t, err, "offline mode not supported")
}
......@@ -124,7 +153,9 @@ func replaceRequiredArg(name string, value string) []string {
// to create a valid Config
func requiredArgs() map[string]string {
return map[string]string{
"--network": "goerli",
"--network": "goerli",
"--l2.genesis": "genesis.json",
"--l2.head": l2HeadValue,
}
}
......
......@@ -6,16 +6,21 @@ import (
opnode "github.com/ethereum-optimism/optimism/op-node"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-program/flags"
"github.com/ethereum/go-ethereum/common"
"github.com/urfave/cli"
)
var (
ErrMissingRollupConfig = errors.New("missing rollup config")
ErrMissingL2Genesis = errors.New("missing l2 genesis")
ErrInvalidL2Head = errors.New("invalid l2 head")
)
type Config struct {
Rollup *rollup.Config
L2URL string
Rollup *rollup.Config
L2URL string
L2GenesisPath string
L2Head common.Hash
}
func (c *Config) Check() error {
......@@ -25,6 +30,12 @@ func (c *Config) Check() error {
if err := c.Rollup.Check(); err != nil {
return err
}
if c.L2GenesisPath == "" {
return ErrMissingL2Genesis
}
if c.L2Head == (common.Hash{}) {
return ErrInvalidL2Head
}
return nil
}
......@@ -33,9 +44,11 @@ func (c *Config) FetchingEnabled() bool {
}
// NewConfig creates a Config with all optional values set to the CLI default value
func NewConfig(rollupCfg *rollup.Config) *Config {
func NewConfig(rollupCfg *rollup.Config, l2GenesisPath string, l2Head common.Hash) *Config {
return &Config{
Rollup: rollupCfg,
Rollup: rollupCfg,
L2GenesisPath: l2GenesisPath,
L2Head: l2Head,
}
}
......@@ -47,8 +60,14 @@ func NewConfigFromCLI(ctx *cli.Context) (*Config, error) {
if err != nil {
return nil, err
}
l2Head := common.HexToHash(ctx.GlobalString(flags.L2Head.Name))
if l2Head == (common.Hash{}) {
return nil, ErrInvalidL2Head
}
return &Config{
Rollup: rollupCfg,
L2URL: ctx.GlobalString(flags.L2NodeAddr.Name),
Rollup: rollupCfg,
L2URL: ctx.GlobalString(flags.L2NodeAddr.Name),
L2GenesisPath: ctx.GlobalString(flags.L2GenesisPath.Name),
L2Head: l2Head,
}, nil
}
......@@ -5,34 +5,63 @@ import (
"github.com/ethereum-optimism/optimism/op-node/chaincfg"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
var validRollupConfig = &chaincfg.Goerli
var validL2GenesisPath = "genesis.json"
var validL2Head = common.HexToHash("0x6303578b1fa9480389c51bbcef6fe045bb877da39740819e9eb5f36f94949bd0")
func TestDefaultConfigIsValid(t *testing.T) {
err := NewConfig(&chaincfg.Goerli).Check()
err := NewConfig(validRollupConfig, validL2GenesisPath, validL2Head).Check()
require.NoError(t, err)
}
func TestRollupConfig(t *testing.T) {
t.Run("Required", func(t *testing.T) {
err := NewConfig(nil).Check()
err := NewConfig(nil, validL2GenesisPath, validL2Head).Check()
require.ErrorIs(t, err, ErrMissingRollupConfig)
})
t.Run("Valid", func(t *testing.T) {
err := NewConfig(&rollup.Config{}).Check()
t.Run("Invalid", func(t *testing.T) {
err := NewConfig(&rollup.Config{}, validL2GenesisPath, validL2Head).Check()
require.ErrorIs(t, err, rollup.ErrBlockTimeZero)
})
}
func TestL2Genesis(t *testing.T) {
t.Run("Required", func(t *testing.T) {
err := NewConfig(validRollupConfig, "", validL2Head).Check()
require.ErrorIs(t, err, ErrMissingL2Genesis)
})
t.Run("Valid", func(t *testing.T) {
err := NewConfig(validRollupConfig, validL2GenesisPath, validL2Head).Check()
require.NoError(t, err)
})
}
func TestL2Head(t *testing.T) {
t.Run("Required", func(t *testing.T) {
err := NewConfig(validRollupConfig, validL2GenesisPath, common.Hash{}).Check()
require.ErrorIs(t, err, ErrInvalidL2Head)
})
t.Run("Valid", func(t *testing.T) {
err := NewConfig(validRollupConfig, validL2GenesisPath, validL2Head).Check()
require.NoError(t, err)
})
}
func TestFetchingEnabled(t *testing.T) {
t.Run("FetchingNotEnabledWhenNoFetcherUrlsSpecified", func(t *testing.T) {
cfg := NewConfig(&chaincfg.Beta1)
cfg := NewConfig(&chaincfg.Beta1, validL2GenesisPath, validL2Head)
require.False(t, cfg.FetchingEnabled(), "Should not enable fetching when node URL not supplied")
})
t.Run("FetchingEnabledWhenFetcherUrlsSpecified", func(t *testing.T) {
cfg := NewConfig(&chaincfg.Beta1)
cfg := NewConfig(&chaincfg.Beta1, validL2GenesisPath, validL2Head)
cfg.L2URL = "https://example.com:1234"
require.True(t, cfg.FetchingEnabled(), "Should enable fetching when node URL supplied")
})
......
......@@ -28,6 +28,16 @@ var (
Usage: "Address of L2 JSON-RPC endpoint to use (eth and debug namespace required)",
EnvVar: service.PrefixEnvVar(envVarPrefix, "L2_RPC"),
}
L2GenesisPath = cli.StringFlag{
Name: "l2.genesis",
Usage: "Path to the op-geth genesis file",
EnvVar: service.PrefixEnvVar(envVarPrefix, "L2_GENESIS"),
}
L2Head = cli.StringFlag{
Name: "l2.head",
Usage: "Hash of the agreed L2 block to start derivation from",
EnvVar: service.PrefixEnvVar(envVarPrefix, "L2_HEAD"),
}
)
// Flags contains the list of configuration options available to the binary.
......@@ -37,6 +47,8 @@ var programFlags = []cli.Flag{
RollupConfig,
Network,
L2NodeAddr,
L2GenesisPath,
L2Head,
}
func init() {
......@@ -53,5 +65,11 @@ func CheckRequired(ctx *cli.Context) error {
if rollupConfig != "" && network != "" {
return fmt.Errorf("cannot specify both %s and %s", RollupConfig.Name, Network.Name)
}
if ctx.GlobalString(L2GenesisPath.Name) == "" {
return fmt.Errorf("flag %s is required", L2GenesisPath.Name)
}
if ctx.GlobalString(L2Head.Name) == "" {
return fmt.Errorf("flag %s is required", L2Head.Name)
}
return nil
}
package l2
import (
"context"
"errors"
"fmt"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-program/l2/engineapi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
)
var (
ErrNotFound = errors.New("not found")
)
type OracleEngine struct {
api *engineapi.L2EngineAPI
backend engineapi.EngineBackend
rollupCfg *rollup.Config
}
func NewOracleEngine(rollupCfg *rollup.Config, logger log.Logger, backend engineapi.EngineBackend) *OracleEngine {
engineAPI := engineapi.NewL2EngineAPI(logger, backend)
return &OracleEngine{
api: engineAPI,
backend: backend,
rollupCfg: rollupCfg,
}
}
func (o OracleEngine) GetPayload(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayload, error) {
return o.api.GetPayloadV1(ctx, payloadId)
}
func (o OracleEngine) ForkchoiceUpdate(ctx context.Context, state *eth.ForkchoiceState, attr *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) {
return o.api.ForkchoiceUpdatedV1(ctx, state, attr)
}
func (o OracleEngine) NewPayload(ctx context.Context, payload *eth.ExecutionPayload) (*eth.PayloadStatusV1, error) {
return o.api.NewPayloadV1(ctx, payload)
}
func (o OracleEngine) PayloadByHash(ctx context.Context, hash common.Hash) (*eth.ExecutionPayload, error) {
block := o.backend.GetBlockByHash(hash)
if block == nil {
return nil, ErrNotFound
}
return eth.BlockAsPayload(block)
}
func (o OracleEngine) PayloadByNumber(ctx context.Context, n uint64) (*eth.ExecutionPayload, error) {
hash := o.backend.GetCanonicalHash(n)
if hash == (common.Hash{}) {
return nil, ErrNotFound
}
return o.PayloadByHash(ctx, hash)
}
func (o OracleEngine) L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error) {
var header *types.Header
switch label {
case eth.Unsafe:
header = o.backend.CurrentHeader()
case eth.Safe:
header = o.backend.CurrentSafeBlock()
case eth.Finalized:
header = o.backend.CurrentFinalBlock()
default:
return eth.L2BlockRef{}, fmt.Errorf("unknown label: %v", label)
}
if header == nil {
return eth.L2BlockRef{}, ErrNotFound
}
block := o.backend.GetBlockByHash(header.Hash())
if block == nil {
return eth.L2BlockRef{}, ErrNotFound
}
return derive.L2BlockToBlockRef(block, &o.rollupCfg.Genesis)
}
func (o OracleEngine) L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error) {
block := o.backend.GetBlockByHash(l2Hash)
if block == nil {
return eth.L2BlockRef{}, ErrNotFound
}
return derive.L2BlockToBlockRef(block, &o.rollupCfg.Genesis)
}
func (o OracleEngine) SystemConfigByL2Hash(ctx context.Context, hash common.Hash) (eth.SystemConfig, error) {
payload, err := o.PayloadByHash(ctx, hash)
if err != nil {
return eth.SystemConfig{}, err
}
return derive.PayloadToSystemConfig(payload, o.rollupCfg)
}
package l2
import (
"context"
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-node/chaincfg"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie"
"github.com/stretchr/testify/require"
)
// Should implement derive.Engine
var _ derive.Engine = (*OracleEngine)(nil)
func TestPayloadByHash(t *testing.T) {
ctx := context.Background()
t.Run("KnownBlock", func(t *testing.T) {
engine, stub := createOracleEngine(t)
block := stub.head
payload, err := engine.PayloadByHash(ctx, block.Hash())
require.NoError(t, err)
expected, err := eth.BlockAsPayload(block)
require.NoError(t, err)
require.Equal(t, expected, payload)
})
t.Run("UnknownBlock", func(t *testing.T) {
engine, _ := createOracleEngine(t)
hash := common.HexToHash("0x878899")
payload, err := engine.PayloadByHash(ctx, hash)
require.ErrorIs(t, err, ErrNotFound)
require.Nil(t, payload)
})
}
func TestPayloadByNumber(t *testing.T) {
ctx := context.Background()
t.Run("KnownBlock", func(t *testing.T) {
engine, stub := createOracleEngine(t)
block := stub.head
payload, err := engine.PayloadByNumber(ctx, block.NumberU64())
require.NoError(t, err)
expected, err := eth.BlockAsPayload(block)
require.NoError(t, err)
require.Equal(t, expected, payload)
})
t.Run("NoCanonicalHash", func(t *testing.T) {
engine, _ := createOracleEngine(t)
payload, err := engine.PayloadByNumber(ctx, uint64(700))
require.ErrorIs(t, err, ErrNotFound)
require.Nil(t, payload)
})
t.Run("UnknownBlock", func(t *testing.T) {
engine, stub := createOracleEngine(t)
hash := common.HexToHash("0x878899")
number := uint64(700)
stub.canonical[number] = hash
payload, err := engine.PayloadByNumber(ctx, number)
require.ErrorIs(t, err, ErrNotFound)
require.Nil(t, payload)
})
}
func TestL2BlockRefByLabel(t *testing.T) {
ctx := context.Background()
engine, stub := createOracleEngine(t)
tests := []struct {
name eth.BlockLabel
block *types.Block
}{
{eth.Unsafe, stub.head},
{eth.Safe, stub.safe},
{eth.Finalized, stub.finalized},
}
for _, test := range tests {
t.Run(string(test.name), func(t *testing.T) {
expected, err := derive.L2BlockToBlockRef(test.block, &engine.rollupCfg.Genesis)
require.NoError(t, err)
blockRef, err := engine.L2BlockRefByLabel(ctx, test.name)
require.NoError(t, err)
require.Equal(t, expected, blockRef)
})
}
t.Run("UnknownLabel", func(t *testing.T) {
_, err := engine.L2BlockRefByLabel(ctx, "nope")
require.ErrorContains(t, err, "unknown label")
})
}
func TestL2BlockRefByHash(t *testing.T) {
ctx := context.Background()
engine, stub := createOracleEngine(t)
t.Run("KnownBlock", func(t *testing.T) {
expected, err := derive.L2BlockToBlockRef(stub.safe, &engine.rollupCfg.Genesis)
require.NoError(t, err)
ref, err := engine.L2BlockRefByHash(ctx, stub.safe.Hash())
require.NoError(t, err)
require.Equal(t, expected, ref)
})
t.Run("UnknownBlock", func(t *testing.T) {
ref, err := engine.L2BlockRefByHash(ctx, common.HexToHash("0x878899"))
require.ErrorIs(t, err, ErrNotFound)
require.Equal(t, eth.L2BlockRef{}, ref)
})
}
func TestSystemConfigByL2Hash(t *testing.T) {
ctx := context.Background()
engine, stub := createOracleEngine(t)
t.Run("KnownBlock", func(t *testing.T) {
payload, err := eth.BlockAsPayload(stub.safe)
require.NoError(t, err)
expected, err := derive.PayloadToSystemConfig(payload, engine.rollupCfg)
require.NoError(t, err)
cfg, err := engine.SystemConfigByL2Hash(ctx, stub.safe.Hash())
require.NoError(t, err)
require.Equal(t, expected, cfg)
})
t.Run("UnknownBlock", func(t *testing.T) {
ref, err := engine.SystemConfigByL2Hash(ctx, common.HexToHash("0x878899"))
require.ErrorIs(t, err, ErrNotFound)
require.Equal(t, eth.SystemConfig{}, ref)
})
}
func createOracleEngine(t *testing.T) (*OracleEngine, *stubEngineBackend) {
head := createL2Block(t, 4)
safe := createL2Block(t, 3)
finalized := createL2Block(t, 2)
backend := &stubEngineBackend{
head: head,
safe: safe,
finalized: finalized,
blocks: map[common.Hash]*types.Block{
head.Hash(): head,
safe.Hash(): safe,
finalized.Hash(): finalized,
},
canonical: map[uint64]common.Hash{
head.NumberU64(): head.Hash(),
safe.NumberU64(): safe.Hash(),
finalized.NumberU64(): finalized.Hash(),
},
}
engine := OracleEngine{
backend: backend,
rollupCfg: &chaincfg.Goerli,
}
return &engine, backend
}
func createL2Block(t *testing.T, number int) *types.Block {
tx, err := derive.L1InfoDeposit(uint64(1), eth.HeaderBlockInfo(&types.Header{
Number: big.NewInt(32),
BaseFee: big.NewInt(7),
}), eth.SystemConfig{}, true)
require.NoError(t, err)
header := &types.Header{
Number: big.NewInt(int64(number)),
BaseFee: big.NewInt(7),
}
return types.NewBlock(header, []*types.Transaction{types.NewTx(tx)}, nil, nil, trie.NewStackTrie(nil))
}
type stubEngineBackend struct {
head *types.Block
safe *types.Block
finalized *types.Block
blocks map[common.Hash]*types.Block
canonical map[uint64]common.Hash
}
func (s stubEngineBackend) CurrentHeader() *types.Header {
return s.head.Header()
}
func (s stubEngineBackend) CurrentSafeBlock() *types.Header {
return s.safe.Header()
}
func (s stubEngineBackend) CurrentFinalBlock() *types.Header {
return s.finalized.Header()
}
func (s stubEngineBackend) GetBlockByHash(hash common.Hash) *types.Block {
return s.blocks[hash]
}
func (s stubEngineBackend) GetCanonicalHash(n uint64) common.Hash {
return s.canonical[n]
}
func (s stubEngineBackend) GetBlock(hash common.Hash, number uint64) *types.Block {
panic("unsupported")
}
func (s stubEngineBackend) HasBlockAndState(hash common.Hash, number uint64) bool {
panic("unsupported")
}
func (s stubEngineBackend) GetVMConfig() *vm.Config {
panic("unsupported")
}
func (s stubEngineBackend) Config() *params.ChainConfig {
panic("unsupported")
}
func (s stubEngineBackend) Engine() consensus.Engine {
panic("unsupported")
}
func (s stubEngineBackend) StateAt(root common.Hash) (*state.StateDB, error) {
panic("unsupported")
}
func (s stubEngineBackend) InsertBlockWithoutSetHead(block *types.Block) error {
panic("unsupported")
}
func (s stubEngineBackend) SetCanonical(head *types.Block) (common.Hash, error) {
panic("unsupported")
}
func (s stubEngineBackend) SetFinalized(header *types.Header) {
panic("unsupported")
}
func (s stubEngineBackend) SetSafe(header *types.Header) {
panic("unsupported")
}
func (s stubEngineBackend) GetHeader(hash common.Hash, number uint64) *types.Header {
panic("unsupported")
}
func (s stubEngineBackend) GetHeaderByNumber(number uint64) *types.Header {
panic("unsupported")
}
func (s stubEngineBackend) GetHeaderByHash(hash common.Hash) *types.Header {
panic("unsupported")
}
func (s stubEngineBackend) GetTd(hash common.Hash, number uint64) *big.Int {
panic("unsupported")
}
package l2
import (
"context"
"encoding/json"
"fmt"
"os"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-program/config"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
)
func NewFetchingEngine(ctx context.Context, logger log.Logger, cfg *config.Config) (derive.Engine, error) {
genesis, err := loadL2Genesis(cfg)
if err != nil {
return nil, err
}
oracle, err := NewFetchingL2Oracle(ctx, logger, cfg.L2URL)
if err != nil {
return nil, fmt.Errorf("connect l2 oracle: %w", err)
}
engineBackend, err := NewOracleBackedL2Chain(logger, oracle, genesis, cfg.L2Head)
if err != nil {
return nil, fmt.Errorf("create l2 chain: %w", err)
}
return NewOracleEngine(cfg.Rollup, logger, engineBackend), nil
}
func loadL2Genesis(cfg *config.Config) (*params.ChainConfig, error) {
data, err := os.ReadFile(cfg.L2GenesisPath)
if err != nil {
return nil, fmt.Errorf("read l2 genesis file: %w", err)
}
var genesis core.Genesis
err = json.Unmarshal(data, &genesis)
if err != nil {
return nil, fmt.Errorf("parse l2 genesis file: %w", err)
}
return genesis.Config, nil
}
......@@ -78,11 +78,17 @@ var (
EnvVar: opservice.PrefixEnvVar(envVarPrefix, "BUILDING_TIME"),
Value: time.Second * 6,
}
AllowGaps = cli.BoolFlag{
Name: "allow-gaps",
Usage: "allow gaps in block building, like missed slots on the beacon chain.",
EnvVar: opservice.PrefixEnvVar(envVarPrefix, "ALLOW_GAPS"),
}
)
func ParseBuildingArgs(ctx *cli.Context) *engine.BlockBuildingSettings {
return &engine.BlockBuildingSettings{
BlockTime: ctx.Uint64(BlockTimeFlag.Name),
AllowGaps: ctx.Bool(AllowGaps.Name),
Random: hashFlagValue(RandaoFlag.Name, ctx),
FeeRecipient: addrFlagValue(FeeRecipientFlag.Name, ctx),
BuildTime: ctx.Duration(BuildingTime.Name),
......@@ -336,7 +342,7 @@ var (
Usage: "build the next block using the Engine API",
Flags: []cli.Flag{
EngineEndpoint, EngineJWTPath,
FeeRecipientFlag, RandaoFlag, BlockTimeFlag, BuildingTime,
FeeRecipientFlag, RandaoFlag, BlockTimeFlag, BuildingTime, AllowGaps,
},
// TODO: maybe support transaction and tx pool engine flags, since we use op-geth?
// TODO: reorg flag
......@@ -362,7 +368,7 @@ var (
Description: "The block time can be changed. The execution engine must be synced to a post-Merge state first.",
Flags: append(append([]cli.Flag{
EngineEndpoint, EngineJWTPath,
FeeRecipientFlag, RandaoFlag, BlockTimeFlag, BuildingTime,
FeeRecipientFlag, RandaoFlag, BlockTimeFlag, BuildingTime, AllowGaps,
}, oplog.CLIFlags(envVarPrefix)...), opmetrics.CLIFlags(envVarPrefix)...),
Action: EngineAction(func(ctx *cli.Context, client client.RPC) error {
logCfg := oplog.ReadLocalCLIConfig(ctx)
......
......@@ -95,13 +95,22 @@ func updateForkchoice(ctx context.Context, client client.RPC, head, safe, finali
}
type BlockBuildingSettings struct {
BlockTime uint64
BlockTime uint64
// skip a block; timestamps will still increase in multiples of BlockTime like L1, but there may be gaps.
AllowGaps bool
Random common.Hash
FeeRecipient common.Address
BuildTime time.Duration
}
func BuildBlock(ctx context.Context, client client.RPC, status *StatusData, settings *BlockBuildingSettings) (*engine.ExecutableData, error) {
timestamp := status.Head.Time + settings.BlockTime
if settings.AllowGaps {
now := uint64(time.Now().Unix())
if now > timestamp {
timestamp = now - ((now - timestamp) % settings.BlockTime)
}
}
var pre engine.ForkChoiceResponse
if err := client.CallContext(ctx, &pre, "engine_forkchoiceUpdatedV1",
engine.ForkchoiceStateV1{
......@@ -109,7 +118,7 @@ func BuildBlock(ctx context.Context, client client.RPC, status *StatusData, sett
SafeBlockHash: status.Safe.Hash,
FinalizedBlockHash: status.Finalized.Hash,
}, engine.PayloadAttributes{
Timestamp: status.Head.Time + settings.BlockTime,
Timestamp: timestamp,
Random: settings.Random,
SuggestedFeeRecipient: settings.FeeRecipient,
// TODO: maybe use the L2 fields to hack in tx embedding CLI option?
......@@ -210,6 +219,7 @@ func Auto(ctx context.Context, metrics Metricer, client client.RPC, log log.Logg
payload, err := BuildBlock(ctx, client, status, &BlockBuildingSettings{
BlockTime: settings.BlockTime,
AllowGaps: settings.AllowGaps,
Random: settings.Random,
FeeRecipient: settings.FeeRecipient,
BuildTime: buildTime,
......
......@@ -25,7 +25,7 @@
"governanceTokenName": "Optimism",
"governanceTokenSymbol": "OP",
"governanceTokenOwner": "0x90F79bf6EB2c4f870365E785982E1f101E93b906",
"l2GenesisBlockGasLimit": "0x17D7840",
"l2GenesisBlockGasLimit": "0x1c9c380",
"l2GenesisBlockCoinbase": "0x4200000000000000000000000000000000000011",
"l2GenesisBlockBaseFeePerGas": "0x3b9aca00",
"gasPriceOracleOverhead": 2100,
......@@ -33,4 +33,4 @@
"eip1559Denominator": 50,
"eip1559Elasticity": 10,
"l2GenesisRegolithTimeOffset": "0x0"
}
\ No newline at end of file
}
......@@ -37,7 +37,7 @@ const config: DeployConfig = {
governanceTokenSymbol: 'OP',
governanceTokenOwner: '0x90F79bf6EB2c4f870365E785982E1f101E93b906',
l2GenesisBlockGasLimit: '0x17D7840',
l2GenesisBlockGasLimit: '0x1c9c380',
l2GenesisBlockCoinbase: '0x4200000000000000000000000000000000000011',
l2GenesisBlockBaseFeePerGas: '0x3b9aca00',
......
# Dispute Game Interface
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
- [Overview](#overview)
- [Types](#types)
- [`DisputeGameFactory` Interface](#disputegamefactory-interface)
- [`DisputeGame` Interface](#disputegame-interface)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Overview
A dispute game is played between multiple parties when contesting the truthiness
of a claim. In the context of an optimistic rollup, claims are made about the
state of the layer two network to enable withdrawals to the layer one. A proposer
makes a claim about the layer two state such that they can withdraw and a
challenger can dispute the validity of the claim. The security of the layer two
comes from the ability of fraudulent withdrawals being able to be disputed.
A dispute game interface is defined to allow for multiple implementations of
dispute games to exist. If multiple dispute games run in production, it gives
a similar security model as having multiple protocol clients, as a bug in a
single dispute game will not result in the bug becoming consensus.
## Types
For added context, we define a few types that are used in the following snippets.
```solidity
/// @notice The type of proof system being used.
enum GameType {
/// @dev The game will use a `IDisputeGame` implementation that utilizes fault proofs.
FAULT,
/// @dev The game will use a `IDisputeGame` implementation that utilizes validity proofs.
VALIDITY,
/// @dev The game will use a `IDisputeGame` implementation that utilizes attestation proofs.
ATTESTATION
}
/// @notice The current status of the dispute game.
enum GameStatus {
/// @dev The game is currently in progress, and has not been resolved.
IN_PROGRESS,
/// @dev The game has concluded, and the `rootClaim` was challenged successfully.
CHALLENGER_WINS,
/// @dev The game has concluded, and the `rootClaim` could not be contested.
DEFENDER_WINS
}
/// @notice A dedicated timestamp type.
type Timestamp is uint64;
/// @notice A `Claim` type represents a 32 byte hash or other unique identifier for a claim about
/// a certain piece of information.
/// @dev For the `FAULT` `GameType`, this will be a root of the merklized state of the fault proof
/// program at the end of the state transition.
/// For the `ATTESTATION` `GameType`, this will be an output root.
type Claim is bytes32;
```
## `DisputeGameFactory` Interface
The dispute game factory is responsible for creating new `DisputeGame` contracts
given a `GameType` and a root `Claim`. Challenger agents will listen to the
`DisputeGameCreated` events that are emitted by the factory as well as other events
that pertain to detecting fault (i.e. `OutputProposed(bytes32,uint256,uint256,uint256)`) in order to keep up
with on-going disputes in the protocol.
A [`clones-with-immutable-args`](https://github.com/Saw-mon-and-Natalie/clones-with-immutable-args) factory
(originally by @wighawag, but forked by @Saw-mon-and-Natalie) is used to create Clones. Each `GameType` has
a corresponding implementation within the factory, and when a new game is created, the factory creates a
clone of the `GameType`'s pre-deployed implementation contract.
The `rootClaim` of created dispute games can either be a claim that the creator agrees or disagrees with.
This is an implementation detail that is left up to the `IDisputeGame` to handle within its `resolve` function.
When the `DisputeGameFactory` creates a new `DisputeGame` contract, it calls `initialize()` on the clone to
set up the game.
```solidity
/// @title IDisputeGameFactory
/// @notice The interface for a DisputeGameFactory contract.
interface IDisputeGameFactory {
/// @notice Emitted when a new dispute game is created
/// @param disputeProxy The address of the dispute game proxy
/// @param gameType The type of the dispute game proxy's implementation
/// @param rootClaim The root claim of the dispute game
event DisputeGameCreated(address indexed disputeProxy, GameType indexed gameType, Claim indexed rootClaim);
/// @notice `games` queries an internal a mapping that maps the hash of `gameType ++ rootClaim ++ extraData`
/// to the deployed `DisputeGame` clone.
/// @dev `++` equates to concatenation.
/// @param gameType The type of the DisputeGame - used to decide the proxy implementation
/// @param rootClaim The root claim of the DisputeGame.
/// @param extraData Any extra data that should be provided to the created dispute game.
/// @return _proxy The clone of the `DisputeGame` created with the given parameters. Returns `address(0)` if nonexistent.
function games(GameType gameType, Claim rootClaim, bytes calldata extraData) external view returns (IDisputeGame _proxy);
/// @notice `gameImpls` is a mapping that maps `GameType`s to their respective `IDisputeGame` implementations.
/// @param gameType The type of the dispute game.
/// @return _impl The address of the implementation of the game type. Will be cloned on creation of a new dispute game
/// with the given `gameType`.
function gameImpls(GameType gameType) public view returns (IDisputeGame _impl);
/// @notice The owner of the contract.
/// @dev Owner Permissions:
/// - Update the implementation contracts for a given game type.
/// @return _owner The owner of the contract.
function owner() public view returns (address _owner);
/// @notice Creates a new DisputeGame proxy contract.
/// @param gameType The type of the DisputeGame - used to decide the proxy implementation
/// @param rootClaim The root claim of the DisputeGame.
/// @param extraData Any extra data that should be provided to the created dispute game.
function create(GameType gameType, Claim rootClaim, bytes calldata extraData) external returns (IDisputeGame proxy);
/// @notice Sets the implementation contract for a specific `GameType`
/// @dev May only be called by the `owner`.
/// @param gameType The type of the DisputeGame
/// @param impl The implementation contract for the given `GameType`
function setImplementation(GameType gameType, IDisputeGame impl) external;
}
```
## `DisputeGame` Interface
The dispute game interface should be generic enough to allow it to work with any
proof system. This means that it should work fault proofs, validity proofs,
an attestation based proof system, or any other source of truth that adheres to
the interface.
Clones of the `IDisputeGame`'s `initialize` functions will be called by the `DisputeGameFactory` upon creation.
```solidity
////////////////////////////////////////////////////////////////
// GENERIC DISPUTE GAME //
////////////////////////////////////////////////////////////////
/// @title IDisputeGame
/// @notice The generic interface for a DisputeGame contract.
interface IDisputeGame {
/// @notice Initializes the DisputeGame contract.
/// @custom:invariant The `initialize` function may only be called once.
function initialize() external;
/// @notice Returns the semantic version of the DisputeGame contract
function version() external pure returns (string memory _version);
/// @notice Returns the timestamp that the DisputeGame contract was created at.
function createdAt() external pure returns (Timestamp _createdAt);
/// @notice Returns the current status of the game.
function status() external view returns (GameStatus _status);
/// @notice Getter for the game type.
/// @dev `clones-with-immutable-args` argument #1
/// @dev The reference impl should be entirely different depending on the type (fault, validity)
/// i.e. The game type should indicate the security model.
/// @return _gameType The type of proof system being used.
function gameType() external view returns (GameType _gameType);
/// @notice Getter for the root claim.
/// @return _rootClaim The root claim of the DisputeGame.
/// @dev `clones-with-immutable-args` argument #2
function rootClaim() external view returns (Claim _rootClaim);
/// @notice Getter for the extra data.
/// @dev `clones-with-immutable-args` argument #3
/// @return _extraData Any extra data supplied to the dispute game contract by the creator.
function extraData() external view returns (bytes memory _extraData);
/// @notice Returns the address of the `BondManager` used
function bondManager() public view returns (IBondManager _bondManager);
/// @notice If all necessary information has been gathered, this function should mark the game
/// status as either `CHALLENGER_WINS` or `DEFENDER_WINS` and return the status of
/// the resolved game. It is at this stage that the bonds should be awarded to the
/// necessary parties.
/// @dev May only be called if the `status` is `IN_PROGRESS`.
function resolve() public returns (GameStatus _status);
}
////////////////////////////////////////////////////////////////
// OUTPUT ATTESTATION DISPUTE GAME //
////////////////////////////////////////////////////////////////
/// @title IDisputeGame_OutputAttestation
/// @notice The interface for an attestation-based DisputeGame meant to contest output
/// proposals in Optimism's `L2OutputOracle` contract.
interface IDisputeGame_OutputAttestation is IDisputeGame {
/// @notice A mapping of addresses from the `signerSet` to booleans signifying whether
/// or not they have authorized the `rootClaim` to be invalidated.
function challenges(address challenger) external view returns (bool _challenged);
/// @notice The signer set consists of authorized public keys that may challenge the `rootClaim`.
/// @return An array of authorized signers.
function signerSet() external view returns (address[] memory _signers);
/// @notice The amount of signatures required to successfully challenge the `rootClaim`
/// output proposal. Once this threshold is met by members of the `signerSet`
/// calling `challenge`, the game will be resolved to `CHALLENGER_WINS`.
/// @custom:invariant The `signatureThreshold` may never be greater than the length of the `signerSet`.
function signatureThreshold() public view returns (uint16 _signatureThreshold);
/// @notice Returns the L2 Block Number that the `rootClaim` commits to. Exists within the `extraData`.
function l2BlockNumber() public view returns (uint256 _l2BlockNumber);
/// @notice Challenge the `rootClaim`.
/// @dev - If the `ecrecover`ed address that created the signature is not a part of the
/// signer set returned by `signerSet`, this function should revert.
/// - If the `ecrecover`ed address that created the signature is not the msg.sender,
/// this function should revert.
/// - If the signature provided is the signature that breaches the signature threshold,
/// the function should call the `resolve` function to resolve the game as `CHALLENGER_WINS`.
/// - When the game resolves, the bond attached to the root claim should be distributed among
/// the signers who participated in challenging the invalid claim.
/// @param signature An EIP-712 signature committing to the `rootClaim` and `l2BlockNumber` (within the `extraData`)
/// from a key that exists within the `signerSet`.
function challenge(bytes calldata signature) external;
}
```
# Dispute Game
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
- [Attestation Dispute Game](#attestation-dispute-game)
- [Smart Contract Implementation](#smart-contract-implementation)
- [Attestation Structure](#attestation-structure)
- [Why EIP-712](#why-eip-712)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Attestation Dispute Game
The output attestation based dispute game shifts the current permissioned output proposal process
to a permissionless, social-consensus based architecture that can progressively decentralize over
time by increasing the size of the signer set. In this "game," output proposals can be submitted
permissionlessly. To prevent "invalid output proposals," a social quorum can revert an output proposal
when an invalid one is discovered. The set of signers is maintained in the `SystemConfig` contract,
and these signers will issue [EIP-712](https://eips.ethereum.org/EIPS/eip-712) signatures
over canonical output roots and the `l2BlockNumber`s they commit to as attestations. To learn more,
see the [DisputeGame Interface Spec](./dispute-game-interface.md).
In the above language, an "invalid output proposal" is defined as an output proposal that represents
a non-canonical state of the L2 chain.
### Smart Contract Implementation
The `AttestationDisputeGame` should implement the `IDisputeGame` interface and also be able to call
out to the `L2OutputOracle`. It is expected that the `L2OutputOracle` will grant permissions to
`AttestationDisputeGame` contracts to call its `deleteL2Outputs` function at the *specific* `l2BlockNumber`
that is embedded in the `AttestationDisputeGame`'s `extraData`.
The `AttestationDisputeGame` should be configured with a quorum ratio at deploy time. It should also
maintain a set of attestor accounts, which is fetched by the `SystemConfig` contract and snapshotted
at deploy time. This snapshot is necessary to have a fixed upper bound on resolution cost, which in
turn gives a fix cost for the necessary bond attached to output proposals.
The ability to add and remove attestor accounts should be enabled by a single immutable
account that controls the `SystemConfig`. It should be impossible to remove accounts such that quorum
is not able to be reached. It is ok to allow accounts to be added or removed in the middle of an
open challenge, as it will not affect the `signerSet` that exists within open challenges.
A challenge is created when an alternative output root for a given `l2BlockNumber` is presented to the
`DisputeGameFactory` contract. Multiple challenges should be able to run in parallel.
For simplicity, the `AttestationDisputeGame` does not need to track what output proposals are
committed to as part of the attestations. It only needs to check that the attested output root
is different than the proposed output root. If this is not checked, then it will be possible
to remove output proposals that are in agreement with the attestations and create a griefing vector.
#### Attestation Structure
The EIP-712 [typeHash](https://eips.ethereum.org/EIPS/eip-712#rationale-for-typehash) should be
defined as the following:
```solidity
TYPE_HASH = keccak256("Dispute(bytes32 outputRoot,uint256 l2BlockNumber)");
```
The components for the `typeHash` are as follows:
- `outputRoot` - The **correct** output root that commits to the given `l2BlockNumber`. This should be a
positive attestation where the `rootClaim` of the `AttestationDisputeGame` is the **correct** output root
for the given `l2BlockNumber`.
- `l2BlockNumber` - The L2 block number that the `outputRoot` commits to. The `outputRoot` should commit
to the entirety of the L2 state from genesis up to and including this `l2BlockNumber`.
### Why EIP-712
It is important to use EIP-712 to decouple the originator of the transaction and the attestor. This
will allow a decentralized network of attestors that serve attestations to bots that are responsible
for ensuring that all output proposals submitted to the network will not allow for malicious withdrawals
from the bridge.
It is important to have replay protection to ensure that attestations cannot be used more than once.
......@@ -67,7 +67,7 @@ censorship resistance.
- All information required to derive the chain is embedded into layer 1 blocks. That way as long as the layer 1
chain is available, so is the rollup.
1. **Validity** - All transactions must be correctly executed and all withdrawals correctly processed.
- The rollup state and withdrawals are managed on an L1 contract called the `L2 State Oracle`. This oracle is
- The rollup state and withdrawals are managed on an L1 contract called the `L2OutputOracle`. This oracle is
guaranteed to _only_ finalize correct (ie. valid) rollup block hashes given a **single honest verifier** assumption. If
there is ever an invalid block hash asserted on layer 1, an honest verifier will prove it is invalid and win a bond.
......
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