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

Merge pull request #5309 from ethereum-optimism/aj/fpp-l2-fetcher

op-program: Add minimal L2 fetcher
parents b9f8f3ce 60d9e4f6
package main package main
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"github.com/ethereum-optimism/optimism/op-node/chaincfg" "github.com/ethereum-optimism/optimism/op-node/chaincfg"
"github.com/ethereum-optimism/optimism/op-program/config" "github.com/ethereum-optimism/optimism/op-program/config"
"github.com/ethereum-optimism/optimism/op-program/flags" "github.com/ethereum-optimism/optimism/op-program/flags"
"github.com/ethereum-optimism/optimism/op-program/l2"
"github.com/ethereum-optimism/optimism/op-program/version" "github.com/ethereum-optimism/optimism/op-program/version"
oplog "github.com/ethereum-optimism/optimism/op-service/log" oplog "github.com/ethereum-optimism/optimism/op-service/log"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -84,7 +86,16 @@ func setupLogging(ctx *cli.Context) (log.Logger, error) { ...@@ -84,7 +86,16 @@ func setupLogging(ctx *cli.Context) (log.Logger, error) {
} }
// FaultProofProgram is the programmatic entry-point for the fault proof program // FaultProofProgram is the programmatic entry-point for the fault proof program
func FaultProofProgram(log log.Logger, cfg *config.Config) error { func FaultProofProgram(logger log.Logger, cfg *config.Config) error {
cfg.Rollup.LogDescription(log, chaincfg.L2ChainIDToNetworkName) cfg.Rollup.LogDescription(logger, chaincfg.L2ChainIDToNetworkName)
if !cfg.FetchingEnabled() {
return errors.New("offline mode not supported")
}
logger.Info("Connecting to L2 node", "l2", cfg.L2URL)
_, err := l2.NewFetchingL2Oracle(logger, cfg.L2URL)
if err != nil {
return fmt.Errorf("connect l2 oracle: %w", err)
}
return nil return nil
} }
...@@ -66,6 +66,19 @@ func TestNetwork(t *testing.T) { ...@@ -66,6 +66,19 @@ func TestNetwork(t *testing.T) {
} }
} }
func TestL2(t *testing.T) {
expected := "https://example.com:8545"
cfg := configForArgs(t, addRequiredArgs("--l2", expected))
require.Equal(t, expected, cfg.L2URL)
}
// 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))
require.ErrorContains(t, err, "offline mode not supported")
}
func verifyArgsInvalid(t *testing.T, messageContains string, cliArgs []string) { func verifyArgsInvalid(t *testing.T, messageContains string, cliArgs []string) {
_, _, err := runWithArgs(cliArgs) _, _, err := runWithArgs(cliArgs)
require.ErrorContains(t, err, messageContains) require.ErrorContains(t, err, messageContains)
......
...@@ -15,6 +15,7 @@ var ( ...@@ -15,6 +15,7 @@ var (
type Config struct { type Config struct {
Rollup *rollup.Config Rollup *rollup.Config
L2URL string
} }
func (c *Config) Check() error { func (c *Config) Check() error {
...@@ -27,6 +28,10 @@ func (c *Config) Check() error { ...@@ -27,6 +28,10 @@ func (c *Config) Check() error {
return nil return nil
} }
func (c *Config) FetchingEnabled() bool {
return c.L2URL != ""
}
// NewConfig creates a Config with all optional values set to the CLI default value // 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) *Config {
return &Config{ return &Config{
...@@ -44,5 +49,6 @@ func NewConfigFromCLI(ctx *cli.Context) (*Config, error) { ...@@ -44,5 +49,6 @@ func NewConfigFromCLI(ctx *cli.Context) (*Config, error) {
} }
return &Config{ return &Config{
Rollup: rollupCfg, Rollup: rollupCfg,
L2URL: ctx.GlobalString(flags.L2NodeAddr.Name),
}, nil }, nil
} }
...@@ -24,3 +24,16 @@ func TestRollupConfig(t *testing.T) { ...@@ -24,3 +24,16 @@ func TestRollupConfig(t *testing.T) {
require.ErrorIs(t, err, rollup.ErrBlockTimeZero) require.ErrorIs(t, err, rollup.ErrBlockTimeZero)
}) })
} }
func TestFetchingEnabled(t *testing.T) {
t.Run("FetchingNotEnabledWhenNoFetcherUrlsSpecified", func(t *testing.T) {
cfg := NewConfig(&chaincfg.Beta1)
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.L2URL = "https://example.com:1234"
require.True(t, cfg.FetchingEnabled(), "Should enable fetching when node URL supplied")
})
}
...@@ -23,6 +23,11 @@ var ( ...@@ -23,6 +23,11 @@ var (
Usage: fmt.Sprintf("Predefined network selection. Available networks: %s", strings.Join(chaincfg.AvailableNetworks(), ", ")), Usage: fmt.Sprintf("Predefined network selection. Available networks: %s", strings.Join(chaincfg.AvailableNetworks(), ", ")),
EnvVar: service.PrefixEnvVar(envVarPrefix, "NETWORK"), EnvVar: service.PrefixEnvVar(envVarPrefix, "NETWORK"),
} }
L2NodeAddr = cli.StringFlag{
Name: "l2",
Usage: "Address of L2 JSON-RPC endpoint to use (eth and debug namespace required)",
EnvVar: service.PrefixEnvVar(envVarPrefix, "L2_RPC"),
}
) )
// Flags contains the list of configuration options available to the binary. // Flags contains the list of configuration options available to the binary.
...@@ -31,6 +36,7 @@ var Flags []cli.Flag ...@@ -31,6 +36,7 @@ var Flags []cli.Flag
var programFlags = []cli.Flag{ var programFlags = []cli.Flag{
RollupConfig, RollupConfig,
Network, Network,
L2NodeAddr,
} }
func init() { func init() {
......
package l2
import (
"context"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
)
type BlockSource interface {
BlockByHash(ctx context.Context, blockHash common.Hash) (*types.Block, error)
}
type CallContext interface {
CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error
}
type FetchingL2Oracle struct {
logger log.Logger
blockSource BlockSource
callContext CallContext
}
func NewFetchingL2Oracle(logger log.Logger, l2Url string) (*FetchingL2Oracle, error) {
rpcClient, err := rpc.Dial(l2Url)
if err != nil {
return nil, err
}
ethClient := ethclient.NewClient(rpcClient)
return &FetchingL2Oracle{
logger: logger,
blockSource: ethClient,
callContext: rpcClient,
}, nil
}
func (s FetchingL2Oracle) NodeByHash(ctx context.Context, nodeHash common.Hash) ([]byte, error) {
var node hexutil.Bytes
err := s.callContext.CallContext(ctx, &node, "debug_dbGet", nodeHash.Hex())
if err != nil {
return nil, fmt.Errorf("fetch node %s: %w", nodeHash.Hex(), err)
}
return node, nil
}
func (s FetchingL2Oracle) BlockByHash(ctx context.Context, blockHash common.Hash) (*types.Block, error) {
block, err := s.blockSource.BlockByHash(ctx, blockHash)
if err != nil {
return nil, fmt.Errorf("fetch block %s: %w", blockHash.Hex(), err)
}
return block, nil
}
package l2
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/rand"
"reflect"
"testing"
"github.com/ethereum-optimism/optimism/op-node/testutils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
type callContextRequest struct {
ctx context.Context
method string
args []interface{}
}
type stubCallContext struct {
nextResult any
nextErr error
requests []callContextRequest
}
func (c *stubCallContext) CallContext(ctx context.Context, result any, method string, args ...interface{}) error {
if result != nil && reflect.TypeOf(result).Kind() != reflect.Ptr {
return fmt.Errorf("call result parameter must be pointer or nil interface: %v", result)
}
c.requests = append(c.requests, callContextRequest{ctx: ctx, method: method, args: args})
if c.nextErr != nil {
return c.nextErr
}
res, err := json.Marshal(c.nextResult)
if err != nil {
return fmt.Errorf("json marshal: %w", err)
}
err = json.Unmarshal(res, result)
if err != nil {
return fmt.Errorf("json unmarshal: %w", err)
}
return nil
}
func TestNodeByHash(t *testing.T) {
rng := rand.New(rand.NewSource(1234))
hash := testutils.RandomHash(rng)
ctx := context.Background()
t.Run("Error", func(t *testing.T) {
stub := &stubCallContext{
nextErr: errors.New("oops"),
}
fetcher := newFetcher(nil, stub)
node, err := fetcher.NodeByHash(ctx, hash)
require.ErrorIs(t, err, stub.nextErr)
require.Nil(t, node)
})
t.Run("Success", func(t *testing.T) {
expected := (hexutil.Bytes)([]byte{12, 34})
stub := &stubCallContext{
nextResult: expected,
}
fetcher := newFetcher(nil, stub)
node, err := fetcher.NodeByHash(ctx, hash)
require.NoError(t, err)
require.EqualValues(t, expected, node)
})
t.Run("RequestArgs", func(t *testing.T) {
stub := &stubCallContext{
nextResult: (hexutil.Bytes)([]byte{12, 34}),
}
fetcher := newFetcher(nil, stub)
_, _ = fetcher.NodeByHash(ctx, hash)
require.Len(t, stub.requests, 1, "should make single request")
req := stub.requests[0]
require.Equal(t, "debug_dbGet", req.method)
require.Equal(t, []interface{}{hash.Hex()}, req.args)
})
}
type blockRequest struct {
ctx context.Context
blockHash common.Hash
}
type stubBlockSource struct {
requests []blockRequest
nextErr error
nextResult *types.Block
}
func (s *stubBlockSource) BlockByHash(ctx context.Context, blockHash common.Hash) (*types.Block, error) {
s.requests = append(s.requests, blockRequest{
ctx: ctx,
blockHash: blockHash,
})
return s.nextResult, s.nextErr
}
func TestBlockByHash(t *testing.T) {
rng := rand.New(rand.NewSource(1234))
hash := testutils.RandomHash(rng)
ctx := context.Background()
t.Run("Success", func(t *testing.T) {
block, _ := testutils.RandomBlock(rng, 1)
stub := &stubBlockSource{nextResult: block}
fetcher := newFetcher(stub, nil)
res, err := fetcher.BlockByHash(ctx, hash)
require.NoError(t, err)
require.Same(t, block, res)
})
t.Run("Error", func(t *testing.T) {
stub := &stubBlockSource{nextErr: errors.New("boom")}
fetcher := newFetcher(stub, nil)
res, err := fetcher.BlockByHash(ctx, hash)
require.ErrorIs(t, err, stub.nextErr)
require.Nil(t, res)
})
t.Run("RequestArgs", func(t *testing.T) {
stub := &stubBlockSource{}
fetcher := newFetcher(stub, nil)
_, _ = fetcher.BlockByHash(ctx, hash)
require.Len(t, stub.requests, 1, "should make single request")
req := stub.requests[0]
require.Equal(t, hash, req.blockHash)
})
}
func newFetcher(blockSource BlockSource, callContext CallContext) *FetchingL2Oracle {
return &FetchingL2Oracle{
logger: log.New(),
blockSource: blockSource,
callContext: callContext,
}
}
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