From 7bd0cd07d40643061da7736405475dedecc3d9dd Mon Sep 17 00:00:00 2001 From: Julian Meyer Date: Mon, 21 Oct 2024 18:42:30 -0700 Subject: [PATCH] feat: add experimental L2 source flag --- op-e2e/actions/proofs/helpers/env.go | 2 +- op-program/host/config/config.go | 44 ++++--- op-program/host/flags/flags.go | 16 +++ op-program/host/host.go | 25 ++-- op-program/host/l2_experimental_client.go | 45 +++++++ op-program/host/l2_source.go | 140 ++++++++++++++++++++++ 6 files changed, 236 insertions(+), 36 deletions(-) create mode 100644 op-program/host/l2_experimental_client.go create mode 100644 op-program/host/l2_source.go diff --git a/op-e2e/actions/proofs/helpers/env.go b/op-e2e/actions/proofs/helpers/env.go index 4283d0d1e162..75b7f2554f4c 100644 --- a/op-e2e/actions/proofs/helpers/env.go +++ b/op-e2e/actions/proofs/helpers/env.go @@ -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 }) diff --git a/op-program/host/config/config.go b/op-program/host/config/config.go index bb9f9d868e45..961558f9c8b2 100644 --- a/op-program/host/config/config.go +++ b/op-program/host/config/config.go @@ -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 @@ -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 } diff --git a/op-program/host/flags/flags.go b/op-program/host/flags/flags.go index 15489cf22c94..6668497f22c9 100644 --- a/op-program/host/flags/flags.go +++ b/op-program/host/flags/flags.go @@ -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.", @@ -131,6 +141,8 @@ var programFlags = []cli.Flag{ DataDir, DataFormat, L2NodeAddr, + L2NodeExperimentalAddr, + L2NodeExperimentalEnabled, L2GenesisPath, L1NodeAddr, L1BeaconAddr, @@ -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) } @@ -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]) diff --git a/op-program/host/host.go b/op-program/host/host.go index a60e451b972d..bd9a38801972 100644 --- a/op-program/host/host.go +++ b/op-program/host/host.go @@ -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) @@ -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 { diff --git a/op-program/host/l2_experimental_client.go b/op-program/host/l2_experimental_client.go new file mode 100644 index 000000000000..23985a16028f --- /dev/null +++ b/op-program/host/l2_experimental_client.go @@ -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 +} diff --git a/op-program/host/l2_source.go b/op-program/host/l2_source.go new file mode 100644 index 000000000000..d9f9cbffa11c --- /dev/null +++ b/op-program/host/l2_source.go @@ -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) +}