Commit 48049abc authored by Diederik Loerakker's avatar Diederik Loerakker Committed by GitHub

op-node: l2 rpc refactor (#3291)

* op-node: l2 rpc refactor

* op-node: implement l2 rpc review suggestions
Co-authored-by: default avatarmergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
parent 5e151113
......@@ -288,7 +288,7 @@ func (cfg SystemConfig) start() (*System, error) {
Alloc: l2Alloc,
Difficulty: common.Big1,
GasLimit: 5000000,
Nonce: 4660,
Nonce: 0,
// must be equal (or higher, while within bounds) as the L1 anchor point of the rollup
Timestamp: genesisTimestamp,
BaseFee: big.NewInt(7),
......
This diff is collapsed.
......@@ -42,7 +42,7 @@ type Metrics struct {
RPCClientResponsesTotal *prometheus.CounterVec
L1SourceCache *CacheMetrics
// TODO: L2SourceCache *CacheMetrics
L2SourceCache *CacheMetrics
DerivationIdle prometheus.Gauge
......@@ -136,6 +136,7 @@ func NewMetrics(procName string) *Metrics {
}),
L1SourceCache: NewCacheMetrics(registry, ns, "l1_source_cache", "L1 Source cache"),
L2SourceCache: NewCacheMetrics(registry, ns, "l2_source_cache", "L2 Source cache"),
DerivationIdle: promauto.With(registry).NewGauge(prometheus.GaugeOpts{
Namespace: ns,
......
......@@ -3,7 +3,6 @@ package node
import (
"context"
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-node/eth"
......@@ -13,20 +12,14 @@ import (
"github.com/ethereum-optimism/optimism/op-node/version"
"github.com/ethereum/go-ethereum"
"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/ethereum/go-ethereum/rpc"
)
type l2EthClient interface {
GetBlockHeader(ctx context.Context, blockTag string) (*types.Header, error)
InfoByRpcNumber(ctx context.Context, num rpc.BlockNumber) (eth.BlockInfo, error)
// GetProof returns a proof of the account, it may return a nil result without error if the address was not found.
GetProof(ctx context.Context, address common.Address, blockTag string) (*eth.AccountResult, error)
BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error)
L2BlockRefByNumber(ctx context.Context, l2Num *big.Int) (eth.L2BlockRef, error)
L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error)
}
type driverClient interface {
......@@ -75,7 +68,7 @@ func (n *nodeAPI) OutputAtBlock(ctx context.Context, number rpc.BlockNumber) ([]
defer recordDur()
// TODO: rpc.BlockNumber doesn't support the "safe" tag. Need a new type
head, err := n.client.GetBlockHeader(ctx, toBlockNumArg(number))
head, err := n.client.InfoByRpcNumber(ctx, number)
if err != nil {
n.log.Error("failed to get block", "err", err)
return nil, err
......@@ -93,13 +86,13 @@ func (n *nodeAPI) OutputAtBlock(ctx context.Context, number rpc.BlockNumber) ([]
return nil, ethereum.NotFound
}
// make sure that the proof (including storage hash) that we retrieved is correct by verifying it against the state-root
if err := proof.Verify(head.Root); err != nil {
n.log.Error("invalid withdrawal root detected in block", "stateRoot", head.Root, "blocknum", number, "msg", err)
if err := proof.Verify(head.Root()); err != nil {
n.log.Error("invalid withdrawal root detected in block", "stateRoot", head.Root(), "blocknum", number, "msg", err)
return nil, fmt.Errorf("invalid withdrawal root hash")
}
var l2OutputRootVersion eth.Bytes32 // it's zero for now
l2OutputRoot := rollup.ComputeL2OutputRoot(l2OutputRootVersion, head.Hash(), head.Root, proof.StorageHash)
l2OutputRoot := rollup.ComputeL2OutputRoot(l2OutputRootVersion, head.Hash(), head.Root(), proof.StorageHash)
return []eth.Bytes32{l2OutputRootVersion, l2OutputRoot}, nil
}
......@@ -123,11 +116,7 @@ func (n *nodeAPI) Version(ctx context.Context) (string, error) {
}
func toBlockNumArg(number rpc.BlockNumber) string {
if number == rpc.LatestBlockNumber {
return "latest"
}
if number == rpc.PendingBlockNumber {
return "pending"
}
return hexutil.EncodeUint64(uint64(number.Int64()))
// never returns an error
out, _ := number.MarshalText()
return string(out)
}
......@@ -11,7 +11,6 @@ import (
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/l2"
"github.com/ethereum-optimism/optimism/op-node/metrics"
"github.com/ethereum-optimism/optimism/op-node/p2p"
"github.com/ethereum-optimism/optimism/op-node/rollup/driver"
......@@ -28,9 +27,8 @@ type OpNode struct {
metrics *metrics.Metrics
l1HeadsSub ethereum.Subscription // Subscription to get L1 heads (automatically re-subscribes on error)
l1Source *sources.L1Client // L1 Client to fetch data from
l2Engine *driver.Driver // L2 Engine to Sync
l2Node client.RPC // L2 Execution Engine RPC connections to close at shutdown
l2Client client.Client // L2 client wrapper around eth namespace
l2Driver *driver.Driver // L2 Engine to Sync
l2Source *sources.EngineClient // L2 Execution Engine RPC bindings
server *rpcServer // RPC server hosting the rollup-node API
p2pNode *p2p.NodeP2P // P2P node functionality
p2pSigner p2p.Signer // p2p gogssip application messages will be signed with this signer
......@@ -139,25 +137,23 @@ func (n *OpNode) initL2(ctx context.Context, cfg *Config, snapshotLog log.Logger
if err != nil {
return fmt.Errorf("failed to setup L2 execution-engine RPC client: %w", err)
}
n.l2Node = client.NewInstrumentedRPC(rpcClient, n.metrics)
n.l2Client = client.NewInstrumentedClient(rpcClient, n.metrics)
source, err := l2.NewSource(n.l2Node, n.l2Client, &cfg.Rollup.Genesis, n.log)
n.l2Source, err = sources.NewEngineClient(
client.NewInstrumentedRPC(rpcClient, n.metrics), n.log, n.metrics.L2SourceCache,
sources.EngineClientDefaultConfig(&cfg.Rollup),
)
if err != nil {
return err
return fmt.Errorf("failed to create Engine client: %w", err)
}
n.l2Engine = driver.NewDriver(&cfg.Driver, &cfg.Rollup, source, n.l1Source, n, n.log, snapshotLog, n.metrics)
n.l2Driver = driver.NewDriver(&cfg.Driver, &cfg.Rollup, n.l2Source, n.l1Source, n, n.log, snapshotLog, n.metrics)
return nil
}
func (n *OpNode) initRPCServer(ctx context.Context, cfg *Config) error {
// TODO: attach the p2p node ID to the snapshot logger
client, err := l2.NewReadOnlySource(n.l2Node, n.l2Client, &cfg.Rollup.Genesis, n.log)
if err != nil {
return err
}
n.server, err = newRPCServer(ctx, &cfg.RPC, &cfg.Rollup, client, n.l2Engine, n.log, n.appVersion, n.metrics)
var err error
n.server, err = newRPCServer(ctx, &cfg.RPC, &cfg.Rollup, n.l2Source.L2Client, n.l2Driver, n.log, n.appVersion, n.metrics)
if err != nil {
return err
}
......@@ -165,7 +161,7 @@ func (n *OpNode) initRPCServer(ctx context.Context, cfg *Config) error {
n.server.EnableP2P(p2p.NewP2PAPIBackend(n.p2pNode, n.log, n.metrics))
}
if cfg.RPC.EnableAdmin {
n.server.EnableAdminAPI(newAdminAPI(n.l2Engine, n.metrics))
n.server.EnableAdminAPI(newAdminAPI(n.l2Driver, n.metrics))
}
n.log.Info("Starting JSON-RPC server")
if err := n.server.Start(); err != nil {
......@@ -218,7 +214,7 @@ func (n *OpNode) Start(ctx context.Context) error {
// Request initial head update, default to genesis otherwise
reqCtx, reqCancel := context.WithTimeout(ctx, time.Second*10)
// start driving engine: sync blocks by deriving them from L1 and driving them into the engine
err := n.l2Engine.Start(reqCtx)
err := n.l2Driver.Start(reqCtx)
reqCancel()
if err != nil {
n.log.Error("Could not start a rollup node", "err", err)
......@@ -234,7 +230,7 @@ func (n *OpNode) OnNewL1Head(ctx context.Context, sig eth.L1BlockRef) {
// Pass on the event to the L2 Engine
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
if err := n.l2Engine.OnL1Head(ctx, sig); err != nil {
if err := n.l2Driver.OnL1Head(ctx, sig); err != nil {
n.log.Warn("failed to notify engine driver of L1 head change", "err", err)
}
......@@ -268,7 +264,7 @@ func (n *OpNode) OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *e
// Pass on the event to the L2 Engine
ctx, cancel := context.WithTimeout(ctx, time.Second*30)
defer cancel()
if err := n.l2Engine.OnUnsafeL2Payload(ctx, payload); err != nil {
if err := n.l2Driver.OnUnsafeL2Payload(ctx, payload); err != nil {
n.log.Warn("failed to notify engine driver of new L2 payload", "err", err, "id", payload.ID())
}
......@@ -306,16 +302,16 @@ func (n *OpNode) Close() error {
n.l1HeadsSub.Unsubscribe()
}
// close L2 engine
if n.l2Engine != nil {
if err := n.l2Engine.Close(); err != nil {
// close L2 driver
if n.l2Driver != nil {
if err := n.l2Driver.Close(); err != nil {
result = multierror.Append(result, fmt.Errorf("failed to close L2 engine driver cleanly: %w", err))
}
}
// close L2 node
if n.l2Node != nil {
n.l2Node.Close()
// close L2 engine RPC client
if n.l2Source != nil {
n.l2Source.Close()
}
// close L1 data source
......
......@@ -7,11 +7,12 @@ import (
"net/http"
"strconv"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-node/metrics"
"github.com/ethereum-optimism/optimism/op-node/p2p"
"github.com/ethereum-optimism/optimism/op-node/l2"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum/go-ethereum/log"
......@@ -28,7 +29,7 @@ type rpcServer struct {
appVersion string
listenAddr net.Addr
log log.Logger
l2.Source
sources.L2Client
}
func newRPCServer(ctx context.Context, rpcCfg *RPCConfig, rollupCfg *rollup.Config, l2Client l2EthClient, dr driverClient, log log.Logger, appVersion string, m *metrics.Metrics) (*rpcServer, error) {
......
......@@ -3,10 +3,9 @@ package node
import (
"context"
"encoding/json"
"math/big"
"math/rand"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum-optimism/optimism/op-node/rollup/driver"
"github.com/ethereum-optimism/optimism/op-node/testutils"
......@@ -88,9 +87,20 @@ func TestOutputAtBlock(t *testing.T) {
// ignore other rollup config info in this test
}
l2Client := &mockL2Client{}
l2Client.mock.On("GetBlockHeader", "latest").Return(&header)
l2Client.mock.On("GetProof", predeploys.L2ToL1MessagePasserAddr, "latest").Return(&result)
l2Client := &testutils.MockL2Client{}
info := &testutils.MockBlockInfo{
InfoHash: header.Hash(),
InfoParentHash: header.ParentHash,
InfoCoinbase: header.Coinbase,
InfoRoot: header.Root,
InfoNum: header.Number.Uint64(),
InfoTime: header.Time,
InfoMixDigest: header.MixDigest,
InfoBaseFee: header.BaseFee,
InfoReceiptRoot: header.ReceiptHash,
}
l2Client.ExpectInfoByRpcNumber(rpc.LatestBlockNumber, info, nil)
l2Client.ExpectGetProof(predeploys.L2ToL1MessagePasserAddr, "latest", &result, nil)
drClient := &mockDriverClient{}
......@@ -106,12 +116,12 @@ func TestOutputAtBlock(t *testing.T) {
err = client.CallContext(context.Background(), &out, "optimism_outputAtBlock", "latest")
assert.NoError(t, err)
assert.Len(t, out, 2)
l2Client.mock.AssertExpectations(t)
l2Client.Mock.AssertExpectations(t)
}
func TestVersion(t *testing.T) {
log := testlog.Logger(t, log.LvlError)
l2Client := &mockL2Client{}
l2Client := &testutils.MockL2Client{}
drClient := &mockDriverClient{}
rpcCfg := &RPCConfig{
ListenAddr: "localhost",
......@@ -136,7 +146,7 @@ func TestVersion(t *testing.T) {
func TestSyncStatus(t *testing.T) {
log := testlog.Logger(t, log.LvlError)
l2Client := &mockL2Client{}
l2Client := &testutils.MockL2Client{}
drClient := &mockDriverClient{}
rng := rand.New(rand.NewSource(1234))
status := driver.SyncStatus{
......@@ -180,27 +190,3 @@ func (c *mockDriverClient) SyncStatus(ctx context.Context) (*driver.SyncStatus,
func (c *mockDriverClient) ResetDerivationPipeline(ctx context.Context) error {
return c.Mock.MethodCalled("ResetDerivationPipeline").Get(0).(error)
}
type mockL2Client struct {
mock mock.Mock
}
func (c *mockL2Client) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) {
return c.mock.MethodCalled("BlockByNumber", number).Get(0).(*types.Block), nil
}
func (c *mockL2Client) L2BlockRefByNumber(ctx context.Context, l2Num *big.Int) (eth.L2BlockRef, error) {
return c.mock.MethodCalled("L2BlockRefByNumber", l2Num).Get(0).(eth.L2BlockRef), nil
}
func (c *mockL2Client) L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error) {
return c.mock.MethodCalled("L2BlockRefByHash", l2Hash).Get(0).(eth.L2BlockRef), nil
}
func (c *mockL2Client) GetBlockHeader(ctx context.Context, blockTag string) (*types.Header, error) {
return c.mock.MethodCalled("GetBlockHeader", blockTag).Get(0).(*types.Header), nil
}
func (c *mockL2Client) GetProof(ctx context.Context, address common.Address, blockTag string) (*eth.AccountResult, error) {
return c.mock.MethodCalled("GetProof", address, blockTag).Get(0).(*eth.AccountResult), nil
}
......@@ -22,7 +22,7 @@ type Engine interface {
NewPayload(ctx context.Context, payload *eth.ExecutionPayload) (*eth.PayloadStatusV1, error)
PayloadByHash(context.Context, common.Hash) (*eth.ExecutionPayload, error)
PayloadByNumber(context.Context, uint64) (*eth.ExecutionPayload, error)
L2BlockRefHead(ctx context.Context) (eth.L2BlockRef, error)
L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error)
L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error)
}
......@@ -275,11 +275,12 @@ func (eq *EngineQueue) forceNextSafeAttributes(ctx context.Context) error {
// ResetStep Walks the L2 chain backwards until it finds an L2 block whose L1 origin is canonical.
// The unsafe head is set to the head of the L2 chain, unless the existing safe head is not canonical.
func (eq *EngineQueue) ResetStep(ctx context.Context, l1Fetcher L1Fetcher) error {
l2Head, err := eq.engine.L2BlockRefHead(ctx)
// TODO: this should be resetting using the safe head instead. Out of scope for L2 client bindings PR.
prevUnsafe, err := eq.engine.L2BlockRefByLabel(ctx, eth.Unsafe)
if err != nil {
return NewTemporaryError(fmt.Errorf("failed to find the L2 Head block: %w", err))
}
unsafe, safe, err := sync.FindL2Heads(ctx, l2Head, eq.cfg.SeqWindowSize, l1Fetcher, eq.engine, &eq.cfg.Genesis)
unsafe, safe, err := sync.FindL2Heads(ctx, prevUnsafe, eq.cfg.SeqWindowSize, l1Fetcher, eq.engine, &eq.cfg.Genesis)
if err != nil {
return NewTemporaryError(fmt.Errorf("failed to find the L2 Heads to start from: %w", err))
}
......
......@@ -2,7 +2,6 @@ package driver
import (
"context"
"math/big"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup"
......@@ -45,8 +44,7 @@ type L1Chain interface {
type L2Chain interface {
derive.Engine
L2BlockRefHead(ctx context.Context) (eth.L2BlockRef, error)
L2BlockRefByNumber(ctx context.Context, l2Num *big.Int) (eth.L2BlockRef, error)
L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error)
L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error)
}
......
......@@ -114,7 +114,7 @@ func (s *state) Start(ctx context.Context) error {
return err
}
s.l1Head = l1Head
s.l2Head, _ = s.l2.L2BlockRefByNumber(ctx, nil)
s.l2Head, _ = s.l2.L2BlockRefByLabel(ctx, eth.Unsafe)
s.metrics.RecordL1Ref("l1_head", s.l1Head)
s.metrics.RecordL2Ref("l2_unsafe", s.l2Head)
......
package sources
import (
"context"
"fmt"
"time"
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/sources/caching"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
)
type EngineClientConfig struct {
L2ClientConfig
}
func EngineClientDefaultConfig(config *rollup.Config) *EngineClientConfig {
return &EngineClientConfig{
// engine is trusted, no need to recompute responses etc.
L2ClientConfig: *L2ClientDefaultConfig(config, true),
}
}
// EngineClient extends L2Client with engine API bindings.
type EngineClient struct {
*L2Client
}
func NewEngineClient(client client.RPC, log log.Logger, metrics caching.Metrics, config *EngineClientConfig) (*EngineClient, error) {
l2Client, err := NewL2Client(client, log, metrics, &config.L2ClientConfig)
if err != nil {
return nil, err
}
return &EngineClient{
L2Client: l2Client,
}, nil
}
// ForkchoiceUpdate updates the forkchoice on the execution client. If attributes is not nil, the engine client will also begin building a block
// based on attributes after the new head block and return the payload ID.
//
// The RPC may return an error in ForkchoiceUpdatedResult.PayloadStatusV1.ValidationError or other non-success PayloadStatusV1,
// and this type of error is kept separate from the returned `error` used for RPC errors, like timeouts.
func (s *EngineClient) ForkchoiceUpdate(ctx context.Context, fc *eth.ForkchoiceState, attributes *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) {
e := s.log.New("state", fc, "attr", attributes)
e.Trace("Sharing forkchoice-updated signal")
fcCtx, cancel := context.WithTimeout(ctx, time.Second*5)
defer cancel()
var result eth.ForkchoiceUpdatedResult
err := s.client.CallContext(fcCtx, &result, "engine_forkchoiceUpdatedV1", fc, attributes)
if err == nil {
e.Trace("Shared forkchoice-updated signal")
if attributes != nil { // block building is optional, we only get a payload ID if we are building a block
e.Trace("Received payload id", "payloadId", result.PayloadID)
}
return &result, nil
} else {
e = e.New("err", err)
if rpcErr, ok := err.(rpc.Error); ok {
code := eth.ErrorCode(rpcErr.ErrorCode())
e.Warn("Unexpected error code in forkchoice-updated response", "code", code)
} else {
e.Error("Failed to share forkchoice-updated signal")
}
return nil, err
}
}
// NewPayload executes a full block on the execution engine.
// This returns a PayloadStatusV1 which encodes any validation/processing error,
// and this type of error is kept separate from the returned `error` used for RPC errors, like timeouts.
func (s *EngineClient) NewPayload(ctx context.Context, payload *eth.ExecutionPayload) (*eth.PayloadStatusV1, error) {
e := s.log.New("block_hash", payload.BlockHash)
e.Trace("sending payload for execution")
execCtx, cancel := context.WithTimeout(ctx, time.Second*5)
defer cancel()
var result eth.PayloadStatusV1
err := s.client.CallContext(execCtx, &result, "engine_newPayloadV1", payload)
e.Trace("Received payload execution result", "status", result.Status, "latestValidHash", result.LatestValidHash, "message", result.ValidationError)
if err != nil {
e.Error("Payload execution failed", "err", err)
return nil, fmt.Errorf("failed to execute payload: %w", err)
}
return &result, nil
}
// GetPayload gets the execution payload associated with the PayloadId
func (s *EngineClient) GetPayload(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayload, error) {
e := s.log.New("payload_id", payloadId)
e.Trace("getting payload")
var result eth.ExecutionPayload
err := s.client.CallContext(ctx, &result, "engine_getPayloadV1", payloadId)
if err != nil {
e = e.New("payload_id", payloadId, "err", err)
if rpcErr, ok := err.(rpc.Error); ok {
code := eth.ErrorCode(rpcErr.ErrorCode())
if code != eth.UnavailablePayload {
e.Warn("unexpected error code in get-payload response", "code", code)
} else {
e.Warn("unavailable payload in get-payload request", "code", code)
}
} else {
e.Error("failed to get payload")
}
return nil, err
}
e.Trace("Received payload")
return &result, nil
}
......@@ -31,12 +31,19 @@ type EthClientConfig struct {
TransactionsCacheSize int
// Number of block headers to cache
HeadersCacheSize int
// Number of payloads to cache
PayloadsCacheSize int
// If the RPC is untrusted, then we should not use cached information from responses,
// and instead verify against the block-hash.
// Of real L1 blocks no deposits can be missed/faked, no batches can be missed/faked,
// only the wrong L1 blocks can be retrieved.
TrustRPC bool
// If the RPC must ensure that the results fit the ExecutionPayload(Header) format.
// If this is not checked, disabled header fields like the nonce or difficulty
// may be used to get a different block-hash.
MustBePostMerge bool
}
func (c *EthClientConfig) Check() error {
......@@ -49,6 +56,9 @@ func (c *EthClientConfig) Check() error {
if c.HeadersCacheSize < 0 {
return fmt.Errorf("invalid headers cache size: %d", c.HeadersCacheSize)
}
if c.PayloadsCacheSize < 0 {
return fmt.Errorf("invalid payloads cache size: %d", c.PayloadsCacheSize)
}
if c.MaxConcurrentRequests < 1 {
return fmt.Errorf("expected at least 1 concurrent request, but max is %d", c.MaxConcurrentRequests)
}
......@@ -66,6 +76,8 @@ type EthClient struct {
trustRPC bool
mustBePostMerge bool
log log.Logger
// cache receipts in bundles per block hash
......@@ -79,6 +91,10 @@ type EthClient struct {
// cache block headers of blocks by hash
// common.Hash -> *HeaderInfo
headersCache *caching.LRUCache
// cache payloads by hash
// common.Hash -> *eth.ExecutionPayload
payloadsCache *caching.LRUCache
}
// NewEthClient wraps a RPC with bindings to fetch ethereum data,
......@@ -96,6 +112,7 @@ func NewEthClient(client client.RPC, log log.Logger, metrics caching.Metrics, co
receiptsCache: caching.NewLRUCache(metrics, "receipts", config.ReceiptsCacheSize),
transactionsCache: caching.NewLRUCache(metrics, "txs", config.TransactionsCacheSize),
headersCache: caching.NewLRUCache(metrics, "headers", config.HeadersCacheSize),
payloadsCache: caching.NewLRUCache(metrics, "payloads", config.PayloadsCacheSize),
}, nil
}
......@@ -115,7 +132,7 @@ func (s *EthClient) headerCall(ctx context.Context, method string, id interface{
if header == nil {
return nil, ethereum.NotFound
}
info, err := header.Info(s.trustRPC)
info, err := header.Info(s.trustRPC, s.mustBePostMerge)
if err != nil {
return nil, err
}
......@@ -132,7 +149,7 @@ func (s *EthClient) blockCall(ctx context.Context, method string, id interface{}
if block == nil {
return nil, nil, ethereum.NotFound
}
info, txs, err := block.Info(s.trustRPC)
info, txs, err := block.Info(s.trustRPC, s.mustBePostMerge)
if err != nil {
return nil, nil, err
}
......@@ -141,6 +158,23 @@ func (s *EthClient) blockCall(ctx context.Context, method string, id interface{}
return info, txs, nil
}
func (s *EthClient) payloadCall(ctx context.Context, method string, id interface{}) (*eth.ExecutionPayload, error) {
var block *rpcBlock
err := s.client.CallContext(ctx, &block, method, id, true)
if err != nil {
return nil, err
}
if block == nil {
return nil, ethereum.NotFound
}
payload, err := block.ExecutionPayload(s.trustRPC)
if err != nil {
return nil, err
}
s.payloadsCache.Add(payload.BlockHash, payload)
return payload, nil
}
func (s *EthClient) InfoByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, error) {
if header, ok := s.headersCache.Get(hash); ok {
return header.(*HeaderInfo), nil
......@@ -182,6 +216,21 @@ func (s *EthClient) InfoAndTxsByLabel(ctx context.Context, label eth.BlockLabel)
return s.blockCall(ctx, "eth_getBlockByNumber", string(label))
}
func (s *EthClient) PayloadByHash(ctx context.Context, hash common.Hash) (*eth.ExecutionPayload, error) {
if payload, ok := s.payloadsCache.Get(hash); ok {
return payload.(*eth.ExecutionPayload), nil
}
return s.payloadCall(ctx, "eth_getBlockByHash", hash)
}
func (s *EthClient) PayloadByNumber(ctx context.Context, number uint64) (*eth.ExecutionPayload, error) {
return s.payloadCall(ctx, "eth_getBlockByNumber", hexutil.EncodeUint64(number))
}
func (s *EthClient) PayloadByLabel(ctx context.Context, label eth.BlockLabel) (*eth.ExecutionPayload, error) {
return s.payloadCall(ctx, "eth_getBlockByNumber", string(label))
}
func (s *EthClient) Fetch(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Transactions, eth.ReceiptsFetcher, error) {
info, txs, err := s.InfoAndTxsByHash(ctx, blockHash)
if err != nil {
......@@ -224,7 +273,7 @@ func (s *EthClient) BlockIDRange(ctx context.Context, begin eth.BlockID, max uin
if result == nil {
break // no more headers from here
}
info, err := result.Info(s.trustRPC)
info, err := result.Info(s.trustRPC, s.mustBePostMerge)
if err != nil {
return nil, fmt.Errorf("bad header data for block %s: %w", headerRequests[i].Args[0], err)
}
......
......@@ -7,6 +7,7 @@ import (
"testing"
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
......@@ -43,9 +44,11 @@ var testEthClientConfig = &EthClientConfig{
ReceiptsCacheSize: 10,
TransactionsCacheSize: 10,
HeadersCacheSize: 10,
PayloadsCacheSize: 10,
MaxRequestsPerBatch: 20,
MaxConcurrentRequests: 10,
TrustRPC: false,
MustBePostMerge: false,
}
func randHash() (out common.Hash) {
......@@ -73,8 +76,23 @@ func randHeader() (*types.Header, *rpcHeader) {
BaseFee: big.NewInt(100),
}
rhdr := &rpcHeader{
cache: rpcHeaderCacheInfo{Hash: hdr.Hash()},
header: *hdr,
ParentHash: hdr.ParentHash,
UncleHash: hdr.UncleHash,
Coinbase: hdr.Coinbase,
Root: hdr.Root,
TxHash: hdr.TxHash,
ReceiptHash: hdr.ReceiptHash,
Bloom: eth.Bytes256(hdr.Bloom),
Difficulty: *(*hexutil.Big)(hdr.Difficulty),
Number: hexutil.Uint64(hdr.Number.Uint64()),
GasLimit: hexutil.Uint64(hdr.GasLimit),
GasUsed: hexutil.Uint64(hdr.GasUsed),
Time: hexutil.Uint64(hdr.Time),
Extra: hdr.Extra,
MixDigest: hdr.MixDigest,
Nonce: hdr.Nonce,
BaseFee: (*hexutil.Big)(hdr.BaseFee),
Hash: hdr.Hash(),
}
return hdr, rhdr
}
......@@ -82,20 +100,20 @@ func randHeader() (*types.Header, *rpcHeader) {
func TestEthClient_InfoByHash(t *testing.T) {
m := new(mockRPC)
_, rhdr := randHeader()
expectedInfo, _ := rhdr.Info(true)
expectedInfo, _ := rhdr.Info(true, false)
ctx := context.Background()
m.On("CallContext", ctx, new(*rpcHeader),
"eth_getBlockByHash", []interface{}{rhdr.cache.Hash, false}).Run(func(args mock.Arguments) {
"eth_getBlockByHash", []interface{}{rhdr.Hash, false}).Run(func(args mock.Arguments) {
*args[1].(**rpcHeader) = rhdr
}).Return([]error{nil})
s, err := NewEthClient(m, nil, nil, testEthClientConfig)
require.NoError(t, err)
info, err := s.InfoByHash(ctx, rhdr.cache.Hash)
info, err := s.InfoByHash(ctx, rhdr.Hash)
require.NoError(t, err)
require.Equal(t, info, expectedInfo)
m.Mock.AssertExpectations(t)
// Again, without expecting any calls from the mock, the cache will return the block
info, err = s.InfoByHash(ctx, rhdr.cache.Hash)
info, err = s.InfoByHash(ctx, rhdr.Hash)
require.NoError(t, err)
require.Equal(t, info, expectedInfo)
m.Mock.AssertExpectations(t)
......@@ -104,16 +122,16 @@ func TestEthClient_InfoByHash(t *testing.T) {
func TestEthClient_InfoByNumber(t *testing.T) {
m := new(mockRPC)
_, rhdr := randHeader()
expectedInfo, _ := rhdr.Info(true)
n := rhdr.header.Number
expectedInfo, _ := rhdr.Info(true, false)
n := rhdr.Number
ctx := context.Background()
m.On("CallContext", ctx, new(*rpcHeader),
"eth_getBlockByNumber", []interface{}{hexutil.EncodeBig(n), false}).Run(func(args mock.Arguments) {
"eth_getBlockByNumber", []interface{}{n.String(), false}).Run(func(args mock.Arguments) {
*args[1].(**rpcHeader) = rhdr
}).Return([]error{nil})
s, err := NewL1Client(m, nil, nil, L1ClientDefaultConfig(&rollup.Config{SeqWindowSize: 10}, true))
require.NoError(t, err)
info, err := s.InfoByNumber(ctx, n.Uint64())
info, err := s.InfoByNumber(ctx, uint64(n))
require.NoError(t, err)
require.Equal(t, info, expectedInfo)
m.Mock.AssertExpectations(t)
......
......@@ -30,9 +30,11 @@ func L1ClientDefaultConfig(config *rollup.Config, trustRPC bool) *L1ClientConfig
ReceiptsCacheSize: span,
TransactionsCacheSize: span,
HeadersCacheSize: span,
PayloadsCacheSize: span,
MaxRequestsPerBatch: 20, // TODO: tune batch param
MaxConcurrentRequests: 10,
TrustRPC: trustRPC,
MustBePostMerge: false,
},
L1BlockRefsCacheSize: span,
}
......
package sources
import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-node/client"
"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-node/sources/caching"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
type L2ClientConfig struct {
EthClientConfig
L2BlockRefsCacheSize int
Genesis rollup.Genesis
}
func L2ClientDefaultConfig(config *rollup.Config, trustRPC bool) *L2ClientConfig {
// Cache 3/2 worth of sequencing window of payloads, block references, receipts and txs
span := int(config.SeqWindowSize) * 3 / 2
// Estimate number of L2 blocks in this span of L1 blocks
// (there's always one L2 block per L1 block, L1 is thus the minimum, even if block time is very high)
if config.BlockTime < 12 && config.BlockTime > 0 {
span *= 12
span /= int(config.BlockTime)
}
if span > 1000 { // sanity cap. If a large sequencing window is configured, do not make the cache too large
span = 1000
}
return &L2ClientConfig{
EthClientConfig: EthClientConfig{
// receipts and transactions are cached per block
ReceiptsCacheSize: span,
TransactionsCacheSize: span,
HeadersCacheSize: span,
PayloadsCacheSize: span,
MaxRequestsPerBatch: 20, // TODO: tune batch param
MaxConcurrentRequests: 10,
TrustRPC: trustRPC,
MustBePostMerge: true,
},
L2BlockRefsCacheSize: span,
Genesis: config.Genesis,
}
}
// L2Client extends EthClient with functions to fetch and cache eth.L2BlockRef values.
type L2Client struct {
*EthClient
genesis *rollup.Genesis
// cache L2BlockRef by hash
// common.Hash -> eth.L2BlockRef
l2BlockRefsCache *caching.LRUCache
}
func NewL2Client(client client.RPC, log log.Logger, metrics caching.Metrics, config *L2ClientConfig) (*L2Client, error) {
ethClient, err := NewEthClient(client, log, metrics, &config.EthClientConfig)
if err != nil {
return nil, err
}
return &L2Client{
EthClient: ethClient,
genesis: &config.Genesis,
l2BlockRefsCache: caching.NewLRUCache(metrics, "blockrefs", config.L2BlockRefsCacheSize),
}, nil
}
// L2BlockRefByLabel returns the L2 block reference for the given label.
func (s *L2Client) L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error) {
payload, err := s.PayloadByLabel(ctx, label)
if err != nil {
// w%: wrap to preserve ethereum.NotFound case
return eth.L2BlockRef{}, fmt.Errorf("failed to determine L2BlockRef of %s, could not get payload: %w", label, err)
}
ref, err := derive.PayloadToBlockRef(payload, s.genesis)
if err != nil {
return eth.L2BlockRef{}, err
}
s.l2BlockRefsCache.Add(ref.Hash, ref)
return ref, nil
}
// L2BlockRefByNumber returns the L2 block reference for the given block number.
func (s *L2Client) L2BlockRefByNumber(ctx context.Context, num uint64) (eth.L2BlockRef, error) {
payload, err := s.PayloadByNumber(ctx, num)
if err != nil {
// w%: wrap to preserve ethereum.NotFound case
return eth.L2BlockRef{}, fmt.Errorf("failed to determine L2BlockRef of height %v, could not get payload: %w", num, err)
}
ref, err := derive.PayloadToBlockRef(payload, s.genesis)
if err != nil {
return eth.L2BlockRef{}, err
}
s.l2BlockRefsCache.Add(ref.Hash, ref)
return ref, nil
}
// L2BlockRefByHash returns the L2 block reference for the given block hash.
// The returned BlockRef may not be in the canonical chain.
func (s *L2Client) L2BlockRefByHash(ctx context.Context, hash common.Hash) (eth.L2BlockRef, error) {
if ref, ok := s.l2BlockRefsCache.Get(hash); ok {
return ref.(eth.L2BlockRef), nil
}
payload, err := s.PayloadByHash(ctx, hash)
if err != nil {
// w%: wrap to preserve ethereum.NotFound case
return eth.L2BlockRef{}, fmt.Errorf("failed to determine block-hash of hash %v, could not get payload: %w", hash, err)
}
ref, err := derive.PayloadToBlockRef(payload, s.genesis)
if err != nil {
return eth.L2BlockRef{}, err
}
s.l2BlockRefsCache.Add(ref.Hash, ref)
return ref, nil
}
......@@ -2,14 +2,17 @@ package sources
import (
"context"
"encoding/json"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/holiman/uint256"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum/go-ethereum/trie"
)
......@@ -20,6 +23,7 @@ type BatchCallContextFn func(ctx context.Context, b []rpc.BatchElem) error
// - ignore uncle data (does not even exist anymore post-Merge)
// - use cached block hash, if we trust the RPC.
// - verify transactions list matches tx-root, to ensure consistency with block-hash, if we do not trust the RPC
// - verify block contents are compatible with Post-Merge ExecutionPayload format
//
// Transaction-sender data from the RPC is not cached, since ethclient.setSenderFromServer is private,
// and we only need to compute the sender for transactions into the inbox.
......@@ -83,71 +87,175 @@ func (info *HeaderInfo) ReceiptHash() common.Hash {
return info.receiptHash
}
type rpcHeaderCacheInfo struct {
type rpcHeader struct {
ParentHash common.Hash `json:"parentHash"`
UncleHash common.Hash `json:"sha3Uncles"`
Coinbase common.Address `json:"miner"`
Root common.Hash `json:"stateRoot"`
TxHash common.Hash `json:"transactionsRoot"`
ReceiptHash common.Hash `json:"receiptsRoot"`
Bloom eth.Bytes256 `json:"logsBloom"`
Difficulty hexutil.Big `json:"difficulty"`
Number hexutil.Uint64 `json:"number"`
GasLimit hexutil.Uint64 `json:"gasLimit"`
GasUsed hexutil.Uint64 `json:"gasUsed"`
Time hexutil.Uint64 `json:"timestamp"`
Extra hexutil.Bytes `json:"extraData"`
MixDigest common.Hash `json:"mixHash"`
Nonce types.BlockNonce `json:"nonce"`
// BaseFee was added by EIP-1559 and is ignored in legacy headers.
BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"`
// untrusted info included by RPC, may have to be checked
Hash common.Hash `json:"hash"`
}
type rpcHeader struct {
cache rpcHeaderCacheInfo
header types.Header
// checkPostMerge checks that the block header meets all criteria to be a valid ExecutionPayloadHeader,
// see EIP-3675 (block header changes) and EIP-4399 (mixHash usage for prev-randao)
func (hdr *rpcHeader) checkPostMerge() error {
// TODO: the genesis block has a non-zero difficulty number value.
// Either this block needs to change, or we special case it. This is not valid w.r.t. EIP-3675.
if hdr.Number != 0 && (*big.Int)(&hdr.Difficulty).Cmp(common.Big0) != 0 {
return fmt.Errorf("post-merge block header requires zeroed difficulty field, but got: %s", &hdr.Difficulty)
}
if hdr.Nonce != (types.BlockNonce{}) {
return fmt.Errorf("post-merge block header requires zeroed block nonce field, but got: %s", hdr.Nonce)
}
if hdr.BaseFee == nil {
return fmt.Errorf("post-merge block header requires EIP-1559 basefee field, but got %s", hdr.BaseFee)
}
if len(hdr.Extra) > 32 {
return fmt.Errorf("post-merge block header requires 32 or less bytes of extra data, but got %d", len(hdr.Extra))
}
if hdr.UncleHash != types.EmptyUncleHash {
return fmt.Errorf("post-merge block header requires uncle hash to be of empty uncle list, but got %s", hdr.UncleHash)
}
return nil
}
func (header *rpcHeader) UnmarshalJSON(msg []byte) error {
if err := json.Unmarshal(msg, &header.header); err != nil {
return err
func (hdr *rpcHeader) computeBlockHash() common.Hash {
gethHeader := types.Header{
ParentHash: hdr.ParentHash,
UncleHash: hdr.UncleHash,
Coinbase: hdr.Coinbase,
Root: hdr.Root,
TxHash: hdr.TxHash,
ReceiptHash: hdr.ReceiptHash,
Bloom: types.Bloom(hdr.Bloom),
Difficulty: (*big.Int)(&hdr.Difficulty),
Number: new(big.Int).SetUint64(uint64(hdr.Number)),
GasLimit: uint64(hdr.GasLimit),
GasUsed: uint64(hdr.GasUsed),
Time: uint64(hdr.Time),
Extra: hdr.Extra,
MixDigest: hdr.MixDigest,
Nonce: hdr.Nonce,
BaseFee: (*big.Int)(hdr.BaseFee),
}
return json.Unmarshal(msg, &header.cache)
return gethHeader.Hash()
}
func (header *rpcHeader) Info(trustCache bool) (*HeaderInfo, error) {
info := HeaderInfo{
hash: header.cache.Hash,
parentHash: header.header.ParentHash,
root: header.header.Root,
number: header.header.Number.Uint64(),
time: header.header.Time,
mixDigest: header.header.MixDigest,
baseFee: header.header.BaseFee,
txHash: header.header.TxHash,
receiptHash: header.header.ReceiptHash,
func (hdr *rpcHeader) Info(trustCache bool, mustBePostMerge bool) (*HeaderInfo, error) {
if mustBePostMerge {
if err := hdr.checkPostMerge(); err != nil {
return nil, err
}
}
if !trustCache {
if computed := header.header.Hash(); computed != info.hash {
return nil, fmt.Errorf("failed to verify block hash: computed %s but RPC said %s", computed, info.hash)
if computed := hdr.computeBlockHash(); computed != hdr.Hash {
return nil, fmt.Errorf("failed to verify block hash: computed %s but RPC said %s", computed, hdr.Hash)
}
}
info := HeaderInfo{
hash: hdr.Hash,
parentHash: hdr.ParentHash,
coinbase: hdr.Coinbase,
root: hdr.Root,
number: uint64(hdr.Number),
time: uint64(hdr.Time),
mixDigest: hdr.MixDigest,
baseFee: (*big.Int)(hdr.BaseFee),
txHash: hdr.TxHash,
receiptHash: hdr.ReceiptHash,
}
return &info, nil
}
type rpcBlockCacheInfo struct {
type rpcBlock struct {
rpcHeader
Transactions []*types.Transaction `json:"transactions"`
}
type rpcBlock struct {
header rpcHeader
extra rpcBlockCacheInfo
func (block *rpcBlock) verify() error {
if computed := block.computeBlockHash(); computed != block.Hash {
return fmt.Errorf("failed to verify block hash: computed %s but RPC said %s", computed, block.Hash)
}
if computed := types.DeriveSha(types.Transactions(block.Transactions), trie.NewStackTrie(nil)); block.TxHash != computed {
return fmt.Errorf("failed to verify transactions list: computed %s but RPC said %s", computed, block.TxHash)
}
return nil
}
func (block *rpcBlock) UnmarshalJSON(msg []byte) error {
if err := json.Unmarshal(msg, &block.header); err != nil {
return err
func (block *rpcBlock) Info(trustCache bool, mustBePostMerge bool) (*HeaderInfo, types.Transactions, error) {
if mustBePostMerge {
if err := block.checkPostMerge(); err != nil {
return nil, nil, err
}
}
if !trustCache {
if err := block.verify(); err != nil {
return nil, nil, err
}
}
return json.Unmarshal(msg, &block.extra)
}
func (block *rpcBlock) Info(trustCache bool) (*HeaderInfo, types.Transactions, error) {
// verify the header data
info, err := block.header.Info(trustCache)
info, err := block.rpcHeader.Info(trustCache, mustBePostMerge)
if err != nil {
return nil, nil, fmt.Errorf("failed to verify block from RPC: %v", err)
return nil, nil, fmt.Errorf("failed to verify block from RPC: %w", err)
}
if !trustCache { // verify the list of transactions matches the tx-root
hasher := trie.NewStackTrie(nil)
computed := types.DeriveSha(types.Transactions(block.extra.Transactions), hasher)
if expected := info.txHash; expected != computed {
return nil, nil, fmt.Errorf("failed to verify transactions list: expected transactions root %s but retrieved %s", expected, computed)
return info, block.Transactions, nil
}
func (block *rpcBlock) ExecutionPayload(trustCache bool) (*eth.ExecutionPayload, error) {
if err := block.checkPostMerge(); err != nil {
return nil, err
}
if !trustCache {
if err := block.verify(); err != nil {
return nil, err
}
}
return info, block.extra.Transactions, nil
var baseFee uint256.Int
baseFee.SetFromBig((*big.Int)(block.BaseFee))
// Unfortunately eth_getBlockByNumber either returns full transactions, or only tx-hashes.
// There is no option for encoded transactions.
opaqueTxs := make([]hexutil.Bytes, len(block.Transactions))
for i, tx := range block.Transactions {
data, err := tx.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("failed to encode tx %d from RPC: %w", i, err)
}
opaqueTxs[i] = data
}
return &eth.ExecutionPayload{
ParentHash: block.ParentHash,
FeeRecipient: block.Coinbase,
StateRoot: eth.Bytes32(block.Root),
ReceiptsRoot: eth.Bytes32(block.ReceiptHash),
LogsBloom: block.Bloom,
PrevRandao: eth.Bytes32(block.MixDigest), // mix-digest field is used for prevRandao post-merge
BlockNumber: block.Number,
GasLimit: block.GasLimit,
GasUsed: block.GasUsed,
Timestamp: block.Time,
ExtraData: eth.BytesMax32(block.Extra),
BaseFeePerGas: baseFee,
BlockHash: block.Hash,
Transactions: opaqueTxs,
}, nil
}
......@@ -145,7 +145,10 @@ func (m *FakeChainSource) L1BlockRefByLabel(ctx context.Context, label eth.Block
return m.l1s[m.l1reorg][m.l1head], nil
}
func (m *FakeChainSource) L2BlockRefHead(ctx context.Context) (eth.L2BlockRef, error) {
func (m *FakeChainSource) L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error) {
if label != eth.Unsafe {
return eth.L2BlockRef{}, fmt.Errorf("testutil FakeChainSource does not support L2BlockRefByLabel(%s)", label)
}
m.log.Trace("L2BlockRefHead", "l2Head", m.l2head, "reorg", m.l2reorg)
if len(m.l2s[m.l2reorg]) == 0 {
panic("bad test, no l2 chain")
......
......@@ -4,30 +4,10 @@ import (
"context"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/mock"
)
type MockEngine struct {
mock.Mock
}
func (m *MockEngine) L2BlockRefHead(ctx context.Context) (eth.L2BlockRef, error) {
out := m.Mock.MethodCalled("L2BlockRefHead")
return out[0].(eth.L2BlockRef), *out[1].(*error)
}
func (m *MockEngine) ExpectL2BlockRefHead(ref eth.L1BlockRef, err error) {
m.Mock.On("L2BlockRefHead").Once().Return(ref, &err)
}
func (m *MockEngine) L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error) {
out := m.Mock.MethodCalled("L2BlockRefByHash", l2Hash)
return out[0].(eth.L2BlockRef), *out[1].(*error)
}
func (m *MockEngine) ExpectL2BlockRefByHash(l2Hash common.Hash, ref eth.L1BlockRef, err error) {
m.Mock.On("L2BlockRefByHash", l2Hash).Once().Return(ref, &err)
MockL2Client
}
func (m *MockEngine) GetPayload(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayload, error) {
......@@ -56,21 +36,3 @@ func (m *MockEngine) NewPayload(ctx context.Context, payload *eth.ExecutionPaylo
func (m *MockEngine) ExpectNewPayload(payload *eth.ExecutionPayload, result *eth.PayloadStatusV1, err error) {
m.Mock.On("NewPayload", payload).Once().Return(result, &err)
}
func (m *MockEngine) PayloadByHash(ctx context.Context, hash common.Hash) (*eth.ExecutionPayload, error) {
out := m.Mock.MethodCalled("PayloadByHash", hash)
return out[0].(*eth.ExecutionPayload), *out[1].(*error)
}
func (m *MockEngine) ExpectPayloadByHash(hash common.Hash, payload *eth.ExecutionPayload, err error) {
m.Mock.On("PayloadByHash", hash).Once().Return(payload, &err)
}
func (m *MockEngine) PayloadByNumber(ctx context.Context, n uint64) (*eth.ExecutionPayload, error) {
out := m.Mock.MethodCalled("PayloadByNumber", n)
return out[0].(*eth.ExecutionPayload), *out[1].(*error)
}
func (m *MockEngine) ExpectPayloadByNumber(hash common.Hash, payload *eth.ExecutionPayload, err error) {
m.Mock.On("PayloadByNumber", hash).Once().Return(payload, &err)
}
......@@ -77,6 +77,33 @@ func (m *MockEthClient) ExpectInfoAndTxsByLabel(label eth.BlockLabel, info eth.B
m.Mock.On("InfoAndTxsByLabel", label).Once().Return(info, transactions, &err)
}
func (m *MockEthClient) PayloadByHash(ctx context.Context, hash common.Hash) (*eth.ExecutionPayload, error) {
out := m.Mock.MethodCalled("PayloadByHash", hash)
return out[0].(*eth.ExecutionPayload), *out[1].(*error)
}
func (m *MockEthClient) ExpectPayloadByHash(hash common.Hash, payload *eth.ExecutionPayload, err error) {
m.Mock.On("PayloadByHash", hash).Once().Return(payload, &err)
}
func (m *MockEthClient) PayloadByNumber(ctx context.Context, n uint64) (*eth.ExecutionPayload, error) {
out := m.Mock.MethodCalled("PayloadByNumber", n)
return out[0].(*eth.ExecutionPayload), *out[1].(*error)
}
func (m *MockEthClient) ExpectPayloadByNumber(hash common.Hash, payload *eth.ExecutionPayload, err error) {
m.Mock.On("PayloadByNumber", hash).Once().Return(payload, &err)
}
func (m *MockEthClient) PayloadByLabel(ctx context.Context, label eth.BlockLabel) (*eth.ExecutionPayload, error) {
out := m.Mock.MethodCalled("PayloadByLabel", label)
return out[0].(*eth.ExecutionPayload), *out[1].(*error)
}
func (m *MockEthClient) ExpectPayloadByLabel(label eth.BlockLabel, payload *eth.ExecutionPayload, err error) {
m.Mock.On("PayloadByLabel", label).Once().Return(payload, &err)
}
func (m *MockEthClient) Fetch(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Transactions, eth.ReceiptsFetcher, error) {
out := m.Mock.MethodCalled("Fetch", blockHash)
return *out[0].(*eth.BlockInfo), out[1].(types.Transactions), out[2].(eth.ReceiptsFetcher), *out[3].(*error)
......
package testutils
import (
"context"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum/go-ethereum/common"
)
type MockL2Client struct {
MockEthClient
}
func (c *MockL2Client) L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error) {
return c.Mock.MethodCalled("L2BlockRefByLabel", label).Get(0).(eth.L2BlockRef), nil
}
func (m *MockL1Source) ExpectL2BlockRefByLabel(label eth.BlockLabel, ref eth.L2BlockRef, err error) {
m.Mock.On("L2BlockRefByLabel", label).Once().Return(ref, &err)
}
func (c *MockL2Client) L2BlockRefByNumber(ctx context.Context, num uint64) (eth.L2BlockRef, error) {
return c.Mock.MethodCalled("L2BlockRefByNumber", num).Get(0).(eth.L2BlockRef), nil
}
func (m *MockL1Source) ExpectL2BlockRefByNumber(num uint64, ref eth.L2BlockRef, err error) {
m.Mock.On("L2BlockRefByNumber", num).Once().Return(ref, &err)
}
func (c *MockL2Client) L2BlockRefByHash(ctx context.Context, hash common.Hash) (eth.L2BlockRef, error) {
return c.Mock.MethodCalled("L2BlockRefByHash", hash).Get(0).(eth.L2BlockRef), nil
}
func (m *MockL1Source) ExpectL2BlockRefByHash(hash common.Hash, ref eth.L2BlockRef, err error) {
m.Mock.On("L2BlockRefByHash", hash).Once().Return(ref, &err)
}
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