Commit 96cbdb80 authored by Adrian Sutton's avatar Adrian Sutton

op-program: Implement wrapper methods required by derive.Engine

Adds flags to specify the l2 execution genesis and agreed l2 starting head.
parent 9ee1d5c2
......@@ -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
}
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