Skip to content

Commit

Permalink
feat: add experimental L2 source flag
Browse files Browse the repository at this point in the history
  • Loading branch information
meyer9 committed Oct 22, 2024
1 parent d8012e0 commit 7bd0cd0
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 36 deletions.
2 changes: 1 addition & 1 deletion op-e2e/actions/proofs/helpers/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ func (env *L2FaultProofEnv) RunFaultProofProgram(t helpers.Testing, l2ClaimBlock
l2RPC := env.Engine.RPCClient()
l2Client, err := host.NewL2Client(l2RPC, env.log, nil, &host.L2ClientConfig{L2ClientConfig: l2ClCfg, L2Head: cfg.L2Head})
require.NoError(t, err, "failed to create L2 client")
l2DebugCl := &host.L2Source{L2Client: l2Client, DebugClient: sources.NewDebugClient(l2RPC.CallContext)}
l2DebugCl := host.NewL2SourceWithClient(logger, l2Client, sources.NewDebugClient(l2RPC.CallContext))

return prefetcher.NewPrefetcher(logger, l1Cl, l1BlobFetcher, l2DebugCl, kv), nil
})
Expand Down
44 changes: 26 additions & 18 deletions op-program/host/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ type Config struct {
L2Head common.Hash
// L2OutputRoot is the agreed L2 output root to start derivation from
L2OutputRoot common.Hash
L2URL string
// L2URL is the URL of the L2 node to fetch L2 data from, this is the canonical URL for L2 data
// in the case of L2ExperimentalEnabled = true, this URL is used as a fallback if the experimental URL fails or cannot retrieve the desired data
L2URL string
// L2ExperimentalURL is the URL of the L2 node (non hash db archival node, for example, reth archival node) to fetch L2 data from
L2ExperimentalURL string
// L2ExperimentalEnabled is a flag to enable experimental features on the L2 node
L2ExperimentalEnabled bool
// L2Claim is the claimed L2 output root to verify
L2Claim common.Hash
// L2ClaimBlockNumber is the block number the claimed L2 output root is from
Expand Down Expand Up @@ -214,23 +220,25 @@ func NewConfigFromCLI(log log.Logger, ctx *cli.Context) (*Config, error) {
return nil, fmt.Errorf("invalid %w: %v", ErrInvalidDataFormat, dbFormat)
}
return &Config{
Rollup: rollupCfg,
DataDir: ctx.String(flags.DataDir.Name),
DataFormat: dbFormat,
L2URL: ctx.String(flags.L2NodeAddr.Name),
L2ChainConfig: l2ChainConfig,
L2Head: l2Head,
L2OutputRoot: l2OutputRoot,
L2Claim: l2Claim,
L2ClaimBlockNumber: l2ClaimBlockNum,
L1Head: l1Head,
L1URL: ctx.String(flags.L1NodeAddr.Name),
L1BeaconURL: ctx.String(flags.L1BeaconAddr.Name),
L1TrustRPC: ctx.Bool(flags.L1TrustRPC.Name),
L1RPCKind: sources.RPCProviderKind(ctx.String(flags.L1RPCProviderKind.Name)),
ExecCmd: ctx.String(flags.Exec.Name),
ServerMode: ctx.Bool(flags.Server.Name),
IsCustomChainConfig: isCustomConfig,
Rollup: rollupCfg,
DataDir: ctx.String(flags.DataDir.Name),
DataFormat: dbFormat,
L2URL: ctx.String(flags.L2NodeAddr.Name),
L2ExperimentalURL: ctx.String(flags.L2NodeExperimentalAddr.Name),
L2ExperimentalEnabled: ctx.Bool(flags.L2NodeExperimentalEnabled.Name),
L2ChainConfig: l2ChainConfig,
L2Head: l2Head,
L2OutputRoot: l2OutputRoot,
L2Claim: l2Claim,
L2ClaimBlockNumber: l2ClaimBlockNum,
L1Head: l1Head,
L1URL: ctx.String(flags.L1NodeAddr.Name),
L1BeaconURL: ctx.String(flags.L1BeaconAddr.Name),
L1TrustRPC: ctx.Bool(flags.L1TrustRPC.Name),
L1RPCKind: sources.RPCProviderKind(ctx.String(flags.L1RPCProviderKind.Name)),
ExecCmd: ctx.String(flags.Exec.Name),
ServerMode: ctx.Bool(flags.Server.Name),
IsCustomChainConfig: isCustomConfig,
}, nil
}

Expand Down
16 changes: 16 additions & 0 deletions op-program/host/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ var (
Usage: "Address of L2 JSON-RPC endpoint to use (eth and debug namespace required)",
EnvVars: prefixEnvVars("L2_RPC"),
}
L2NodeExperimentalAddr = &cli.StringFlag{
Name: "l2.experimental",
Usage: "Address of L2 JSON-RPC endpoint to use for experimental features (debug_executionWitness)",
EnvVars: prefixEnvVars("L2_RPC_EXPERIMENTAL_RPC"),
}
L2NodeExperimentalEnabled = &cli.BoolFlag{
Name: "l2.experimental.enabled",
Usage: "Enable experimental features on the L2 JSON-RPC endpoint, will fallback to L2NodeAddr if not able to retrieve desired data",
EnvVars: prefixEnvVars("L2_RPC_EXPERIMENTAL_ENABLED"),
}
L1Head = &cli.StringFlag{
Name: "l1.head",
Usage: "Hash of the L1 head block. Derivation stops after this block is processed.",
Expand Down Expand Up @@ -131,6 +141,8 @@ var programFlags = []cli.Flag{
DataDir,
DataFormat,
L2NodeAddr,
L2NodeExperimentalAddr,
L2NodeExperimentalEnabled,
L2GenesisPath,
L1NodeAddr,
L1BeaconAddr,
Expand All @@ -149,6 +161,7 @@ func init() {
func CheckRequired(ctx *cli.Context) error {
rollupConfig := ctx.String(RollupConfig.Name)
network := ctx.String(Network.Name)
l2ExperimentalEnabled := ctx.Bool(L2NodeExperimentalEnabled.Name)
if rollupConfig == "" && network == "" {
return fmt.Errorf("flag %s or %s is required", RollupConfig.Name, Network.Name)
}
Expand All @@ -161,6 +174,9 @@ func CheckRequired(ctx *cli.Context) error {
if ctx.String(L2GenesisPath.Name) != "" && network != "" {
return fmt.Errorf("cannot specify both %s and %s", L2GenesisPath.Name, Network.Name)
}
if l2ExperimentalEnabled && ctx.String(L2NodeExperimentalAddr.Name) == "" {
return fmt.Errorf("flag %s is required when %s is enabled", L2NodeExperimentalAddr.Name, L2NodeExperimentalEnabled.Name)
}
for _, flag := range requiredFlags {
if !ctx.IsSet(flag.Names()[0]) {
return fmt.Errorf("flag %s is required", flag.Names()[0])
Expand Down
25 changes: 8 additions & 17 deletions op-program/host/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ import (
"github.com/ethereum/go-ethereum/log"
)

type L2Source struct {
*L2Client
*sources.DebugClient
}

type Prefetcher interface {
Hint(hint string) error
GetPreimage(ctx context.Context, key common.Hash) ([]byte, error)
Expand Down Expand Up @@ -234,27 +229,23 @@ func makeDefaultPrefetcher(ctx context.Context, logger log.Logger, kv kvstore.KV
if err != nil {
return nil, fmt.Errorf("failed to setup L1 RPC: %w", err)
}

logger.Info("Connecting to L2 node", "l2", cfg.L2URL)
l2RPC, err := client.NewRPC(ctx, logger, cfg.L2URL, client.WithDialBackoff(10))
if err != nil {
return nil, fmt.Errorf("failed to setup L2 RPC: %w", err)
}

l1ClCfg := sources.L1ClientDefaultConfig(cfg.Rollup, cfg.L1TrustRPC, cfg.L1RPCKind)
l2ClCfg := sources.L2ClientDefaultConfig(cfg.Rollup, true)
l1Cl, err := sources.NewL1Client(l1RPC, logger, nil, l1ClCfg)
if err != nil {
return nil, fmt.Errorf("failed to create L1 client: %w", err)
}

logger.Info("Connecting to L1 beacon", "l1", cfg.L1BeaconURL)
l1Beacon := sources.NewBeaconHTTPClient(client.NewBasicHTTPClient(cfg.L1BeaconURL, logger))
l1BlobFetcher := sources.NewL1BeaconClient(l1Beacon, sources.L1BeaconClientConfig{FetchAllSidecars: false})
l2Cl, err := NewL2Client(l2RPC, logger, nil, &L2ClientConfig{L2ClientConfig: l2ClCfg, L2Head: cfg.L2Head})

logger.Info("Initializing L2 clients")
l2Client, err := NewL2Source(ctx, logger, cfg)
if err != nil {
return nil, fmt.Errorf("failed to create L2 client: %w", err)
return nil, fmt.Errorf("failed to create L2 source: %w", err)
}
l2DebugCl := &L2Source{L2Client: l2Cl, DebugClient: sources.NewDebugClient(l2RPC.CallContext)}
return prefetcher.NewPrefetcher(logger, l1Cl, l1BlobFetcher, l2DebugCl, kv), nil

return prefetcher.NewPrefetcher(logger, l1Cl, l1BlobFetcher, l2Client, kv), nil
}

func routeHints(logger log.Logger, hHostRW io.ReadWriter, hinter preimage.HintHandler) chan error {
Expand Down
45 changes: 45 additions & 0 deletions op-program/host/l2_experimental_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package host

import (
"context"

"github.com/ethereum-optimism/optimism/op-program/host/prefetcher"
"github.com/ethereum-optimism/optimism/op-service/client"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)

type L2ExperimentalClient struct {
*L2Client
client client.RPC
}

var _ prefetcher.L2Source = &L2ExperimentalClient{}

func NewL2ExperimentalClient(l2Client *L2Client, client client.RPC) *L2ExperimentalClient {
return &L2ExperimentalClient{
L2Client: l2Client,
client: client,
}
}

// CodeByHash implements prefetcher.L2Source.
func (s *L2ExperimentalClient) CodeByHash(ctx context.Context, hash common.Hash) ([]byte, error) {
panic("unsupported")
}

// NodeByHash implements prefetcher.L2Source.
func (s *L2ExperimentalClient) NodeByHash(ctx context.Context, hash common.Hash) ([]byte, error) {
panic("unsupported")
}

func (s *L2ExperimentalClient) ExecutionWitness(ctx context.Context, blockNum uint64) (*eth.ExecutionWitness, error) {
var witness eth.ExecutionWitness

err := s.client.CallContext(ctx, &witness, "debug_executionWitness", hexutil.EncodeUint64(blockNum), true)
if err != nil {
return nil, err
}
return &witness, nil
}
140 changes: 140 additions & 0 deletions op-program/host/l2_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package host

import (
"context"
"time"

"github.com/ethereum-optimism/optimism/op-program/host/config"
"github.com/ethereum-optimism/optimism/op-program/host/prefetcher"
"github.com/ethereum-optimism/optimism/op-service/client"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/sources"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
)

// L2Source is a source of L2 data, it abstracts away the details of how to fetch L2 data between canonical and experimental sources.
// It also tracks metrics for each of the apis. Once experimental sources are stable, this will only route to the "experimental" source.
type L2Source struct {
logger log.Logger

// canonical source, used as a fallback if experimental source is enabled but fails
// the underlying node should be a geth hash scheme archival node.
canonicalEthClient *L2Client
canonicalDebugClient *sources.DebugClient

// experimental source, used as the primary source if enabled
experimentalClient *L2ExperimentalClient

// whether to use the experimental source
experimentalEnabled bool
}

var _ prefetcher.L2Source = &L2Source{}

// NewL2SourceWithClient creates a new L2 source with the given client as the canonical client.
// This doesn't configure the experimental source, but is useful for testing.
func NewL2SourceWithClient(logger log.Logger, canonicalL2Client *L2Client, canonicalDebugClient *sources.DebugClient) *L2Source {
source := &L2Source{
logger: logger,
canonicalEthClient: canonicalL2Client,
canonicalDebugClient: canonicalDebugClient,
}

return source
}

func NewL2Source(ctx context.Context, logger log.Logger, config *config.Config) (*L2Source, error) {
logger.Info("Connecting to canonical L2 source", "url", config.L2URL)

// eth_getProof calls are expensive and takes time, so we use a longer timeout
canonicalL2RPC, err := client.NewRPC(ctx, logger, config.L2URL, client.WithDialBackoff(10), client.WithCallTimeout(5*time.Minute))
if err != nil {
return nil, err
}
canonicalDebugClient := sources.NewDebugClient(canonicalL2RPC.CallContext)

canonicalL2ClientCfg := sources.L2ClientDefaultConfig(config.Rollup, true)
canonicalL2Client, err := NewL2Client(canonicalL2RPC, logger, nil, &L2ClientConfig{L2ClientConfig: canonicalL2ClientCfg, L2Head: config.L2Head})
if err != nil {
return nil, err
}

source := NewL2SourceWithClient(logger, canonicalL2Client, canonicalDebugClient)

if !config.L2ExperimentalEnabled {
return source, nil
}

logger.Info("Connecting to experimental L2 source", "url", config.L2ExperimentalURL)
// debug_executionWitness calls are expensive and takes time, so we use a longer timeout
experimentalRPC, err := client.NewRPC(ctx, logger, config.L2ExperimentalURL, client.WithDialBackoff(10), client.WithCallTimeout(5*time.Minute))
if err != nil {
return nil, err
}
experimentalL2ClientCfg := sources.L2ClientDefaultConfig(config.Rollup, true)
experimentalL2Client, err := NewL2Client(experimentalRPC, logger, nil, &L2ClientConfig{L2ClientConfig: experimentalL2ClientCfg, L2Head: config.L2Head})
if err != nil {
return nil, err
}

experimentalClient := NewL2ExperimentalClient(experimentalL2Client, experimentalRPC)
source.experimentalClient = experimentalClient
source.experimentalEnabled = true

return source, nil
}

// CodeByHash implements prefetcher.L2Source.
func (l *L2Source) CodeByHash(ctx context.Context, hash common.Hash) ([]byte, error) {
if l.experimentalEnabled {
// This means experimental source was not able to retrieve relevant information from eth_getProof or debug_executionWitness
// We should fall back to the canonical source, and log a warning, and record a metric
l.logger.Warn("Experimental source failed to retrieve code by hash, falling back to canonical source", "hash", hash)
}
return l.canonicalDebugClient.CodeByHash(ctx, hash)
}

// NodeByHash implements prefetcher.L2Source.
func (l *L2Source) NodeByHash(ctx context.Context, hash common.Hash) ([]byte, error) {
if l.experimentalEnabled {
// This means experimental source was not able to retrieve relevant information from eth_getProof or debug_executionWitness
// We should fall back to the canonical source, and log a warning, and record a metric
l.logger.Warn("Experimental source failed to retrieve node by hash, falling back to canonical source", "hash", hash)
}
return l.canonicalDebugClient.NodeByHash(ctx, hash)
}

// InfoAndTxsByHash implements prefetcher.L2Source.
func (l *L2Source) InfoAndTxsByHash(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Transactions, error) {
if l.experimentalEnabled {
return l.experimentalClient.InfoAndTxsByHash(ctx, blockHash)
}
return l.canonicalEthClient.InfoAndTxsByHash(ctx, blockHash)
}

// OutputByRoot implements prefetcher.L2Source.
func (l *L2Source) OutputByRoot(ctx context.Context, root common.Hash) (eth.Output, error) {
if l.experimentalEnabled {
return l.experimentalClient.OutputByRoot(ctx, root)
}
return l.canonicalEthClient.OutputByRoot(ctx, root)
}

// ExecutionWitness implements prefetcher.L2Source.
func (l *L2Source) ExecutionWitness(ctx context.Context, blockNum uint64) (*eth.ExecutionWitness, error) {
if !l.experimentalEnabled {
l.logger.Error("Experimental source is not enabled, cannot fetch execution witness", "blockNum", blockNum)
panic("experimental source is not enabled")
}
return l.experimentalClient.ExecutionWitness(ctx, blockNum)
}

// GetProof implements prefetcher.L2Source.
func (l *L2Source) GetProof(ctx context.Context, address common.Address, storage []common.Hash, blockTag string) (*eth.AccountResult, error) {
if l.experimentalEnabled {
l.experimentalClient.GetProof(ctx, address, storage, blockTag)
}
return l.canonicalEthClient.GetProof(ctx, address, storage, blockTag)
}

0 comments on commit 7bd0cd0

Please sign in to comment.