l2_source.go 6.24 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
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 *L2Client
}

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.WithDialAttempts(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 len(config.L2ExperimentalURL) == 0 {
		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.WithDialAttempts(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
	}

	source.experimentalClient = experimentalL2Client

	return source, nil
}

func (l *L2Source) ExperimentalEnabled() bool {
	return l.experimentalClient != 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)
		return nil, prefetcher.ErrExperimentalPrefetchDisabled
	}

	// log errors, but return standard error so we know to retry with legacy source
	witness, err := l.experimentalClient.ExecutionWitness(ctx, blockNum)
	if err != nil {
		l.logger.Error("Failed to fetch execution witness from experimental source", "blockNum", blockNum, "err", err)
		return nil, prefetcher.ErrExperimentalPrefetchFailed
	}
	return witness, nil
}

// 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() {
		return l.experimentalClient.GetProof(ctx, address, storage, blockTag)
	}
	proof, err := l.canonicalEthClient.GetProof(ctx, address, storage, blockTag)
	if err != nil {
		l.logger.Error("Failed to fetch proof from canonical source", "address", address, "storage", storage, "blockTag", blockTag, "err", err)
		return nil, prefetcher.ErrExperimentalPrefetchFailed
	}
	return proof, nil
}