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) { ...@@ -288,7 +288,7 @@ func (cfg SystemConfig) start() (*System, error) {
Alloc: l2Alloc, Alloc: l2Alloc,
Difficulty: common.Big1, Difficulty: common.Big1,
GasLimit: 5000000, GasLimit: 5000000,
Nonce: 4660, Nonce: 0,
// must be equal (or higher, while within bounds) as the L1 anchor point of the rollup // must be equal (or higher, while within bounds) as the L1 anchor point of the rollup
Timestamp: genesisTimestamp, Timestamp: genesisTimestamp,
BaseFee: big.NewInt(7), BaseFee: big.NewInt(7),
......
package l2
import (
"context"
"fmt"
"math/big"
"time"
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum/go-ethereum"
"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/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
)
type Source struct {
rpc client.RPC // raw RPC client. Used for the consensus namespace
client client.Client // go-ethereum's wrapper around the rpc client for the eth namespace
genesis *rollup.Genesis
log log.Logger
}
func NewSource(l2Node client.RPC, l2Client client.Client, genesis *rollup.Genesis, log log.Logger) (*Source, error) {
return &Source{
rpc: l2Node,
client: l2Client,
genesis: genesis,
log: log,
}, nil
}
func (s *Source) Close() {
s.rpc.Close()
}
func (s *Source) PayloadByHash(ctx context.Context, hash common.Hash) (*eth.ExecutionPayload, error) {
// TODO: we really do not need to parse every single tx and block detail, keeping transactions encoded is faster.
block, err := s.client.BlockByHash(ctx, hash)
if err != nil {
return nil, fmt.Errorf("failed to retrieve L2 block by hash: %v", err)
}
payload, err := eth.BlockAsPayload(block)
if err != nil {
return nil, fmt.Errorf("failed to read L2 block as payload: %w", err)
}
return payload, nil
}
func (s *Source) PayloadByNumber(ctx context.Context, number uint64) (*eth.ExecutionPayload, error) {
// TODO: we really do not need to parse every single tx and block detail, keeping transactions encoded is faster.
block, err := s.client.BlockByNumber(ctx, big.NewInt(int64(number)))
if err != nil {
return nil, fmt.Errorf("failed to retrieve L2 block by number: %v", err)
}
payload, err := eth.BlockAsPayload(block)
if err != nil {
return nil, fmt.Errorf("failed to read L2 block as payload: %w", err)
}
return payload, 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.
// May return an error in ForkChoiceResult, but the error is marshalled into the error return
func (s *Source) 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.rpc.CallContext(fcCtx, &result, "engine_forkchoiceUpdatedV1", fc, attributes)
if err == nil {
e.Trace("Shared forkchoice-updated signal")
if attributes != nil {
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
}
}
// ExecutePayload executes a built block on the execution engine and returns an error if it was not successful.
func (s *Source) 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.rpc.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: %v", err)
}
return &result, nil
}
// GetPayload gets the execution payload associated with the PayloadId
func (s *Source) 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.rpc.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")
}
} else {
e.Error("failed to get payload")
}
return nil, err
}
e.Trace("Received payload")
return &result, nil
}
// L2BlockRefHead returns the canonical block and parent ids.
func (s *Source) L2BlockRefHead(ctx context.Context) (eth.L2BlockRef, error) {
block, err := s.client.BlockByNumber(ctx, nil)
if err != nil {
// w%: wrap the error, we still need to detect if a canonical block is not found, a.k.a. end of chain.
return eth.L2BlockRef{}, fmt.Errorf("failed to determine block-hash of head, could not get header: %w", err)
}
return blockToBlockRef(block, s.genesis)
}
// L2BlockRefByNumber returns the canonical block and parent ids.
func (s *Source) L2BlockRefByNumber(ctx context.Context, l2Num *big.Int) (eth.L2BlockRef, error) {
block, err := s.client.BlockByNumber(ctx, l2Num)
if err != nil {
// w%: wrap the error, we still need to detect if a canonical block is not found, a.k.a. end of chain.
return eth.L2BlockRef{}, fmt.Errorf("failed to determine block-hash of height %v, could not get header: %w", l2Num, err)
}
return blockToBlockRef(block, s.genesis)
}
// L2BlockRefByHash returns the block & parent ids based on the supplied hash. The returned BlockRef may not be in the canonical chain
func (s *Source) L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error) {
block, err := s.client.BlockByHash(ctx, l2Hash)
if err != nil {
// w%: wrap the error, we still need to detect if a canonical block is not found, a.k.a. end of chain.
return eth.L2BlockRef{}, fmt.Errorf("failed to determine block-hash of height %v, could not get header: %w", l2Hash, err)
}
return blockToBlockRef(block, s.genesis)
}
// blockToBlockRef extracts the essential L2BlockRef information from a block,
// falling back to genesis information if necessary.
func blockToBlockRef(block *types.Block, genesis *rollup.Genesis) (eth.L2BlockRef, error) {
var l1Origin eth.BlockID
var sequenceNumber uint64
if block.NumberU64() == genesis.L2.Number {
if block.Hash() != genesis.L2.Hash {
return eth.L2BlockRef{}, fmt.Errorf("expected L2 genesis hash to match L2 block at genesis block number %d: %s <> %s", genesis.L2.Number, block.Hash(), genesis.L2.Hash)
}
l1Origin = genesis.L1
sequenceNumber = 0
} else {
txs := block.Transactions()
if len(txs) == 0 {
return eth.L2BlockRef{}, fmt.Errorf("l2 block is missing L1 info deposit tx, block hash: %s", block.Hash())
}
tx := txs[0]
if tx.Type() != types.DepositTxType {
return eth.L2BlockRef{}, fmt.Errorf("first block tx has unexpected tx type: %d", tx.Type())
}
info, err := derive.L1InfoDepositTxData(tx.Data())
if err != nil {
return eth.L2BlockRef{}, fmt.Errorf("failed to parse L1 info deposit tx from L2 block: %v", err)
}
l1Origin = eth.BlockID{Hash: info.BlockHash, Number: info.Number}
sequenceNumber = info.SequenceNumber
}
return eth.L2BlockRef{
Hash: block.Hash(),
Number: block.NumberU64(),
ParentHash: block.ParentHash(),
Time: block.Time(),
L1Origin: l1Origin,
SequenceNumber: sequenceNumber,
}, nil
}
type ReadOnlySource struct {
rpc client.RPC // raw RPC client. Used for methods that do not already have bindings
client client.Client // go-ethereum's wrapper around the rpc client for the eth namespace
genesis *rollup.Genesis
log log.Logger
}
func NewReadOnlySource(l2Node client.RPC, l2Client client.Client, genesis *rollup.Genesis, log log.Logger) (*ReadOnlySource, error) {
return &ReadOnlySource{
rpc: l2Node,
client: l2Client,
genesis: genesis,
log: log,
}, nil
}
// TODO: de-duplicate Source and ReadOnlySource.
// We should really have a L1-downloader like binding that is more configurable and has caching.
// L2BlockRefByNumber returns the canonical block and parent ids.
func (s *ReadOnlySource) L2BlockRefByNumber(ctx context.Context, l2Num *big.Int) (eth.L2BlockRef, error) {
block, err := s.client.BlockByNumber(ctx, l2Num)
if err != nil {
// w%: wrap the error, we still need to detect if a canonical block is not found, a.k.a. end of chain.
return eth.L2BlockRef{}, fmt.Errorf("failed to determine block-hash of height %v, could not get header: %w", l2Num, err)
}
return blockToBlockRef(block, s.genesis)
}
// L2BlockRefByHash returns the block & parent ids based on the supplied hash. The returned BlockRef may not be in the canonical chain
func (s *ReadOnlySource) L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error) {
block, err := s.client.BlockByHash(ctx, l2Hash)
if err != nil {
// w%: wrap the error, we still need to detect if a canonical block is not found, a.k.a. end of chain.
return eth.L2BlockRef{}, fmt.Errorf("failed to determine block-hash of height %v, could not get header: %w", l2Hash, err)
}
return blockToBlockRef(block, s.genesis)
}
func (s *ReadOnlySource) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) {
return s.client.BlockByNumber(ctx, number)
}
func (s *ReadOnlySource) GetBlockHeader(ctx context.Context, blockTag string) (*types.Header, error) {
var head *types.Header
err := s.rpc.CallContext(ctx, &head, "eth_getBlockByNumber", blockTag, false)
return head, err
}
func (s *ReadOnlySource) GetProof(ctx context.Context, address common.Address, blockTag string) (*eth.AccountResult, error) {
var getProofResponse *eth.AccountResult
err := s.rpc.CallContext(ctx, &getProofResponse, "eth_getProof", address, []common.Hash{}, blockTag)
if err == nil && getProofResponse == nil {
err = ethereum.NotFound
}
return getProofResponse, err
}
...@@ -42,7 +42,7 @@ type Metrics struct { ...@@ -42,7 +42,7 @@ type Metrics struct {
RPCClientResponsesTotal *prometheus.CounterVec RPCClientResponsesTotal *prometheus.CounterVec
L1SourceCache *CacheMetrics L1SourceCache *CacheMetrics
// TODO: L2SourceCache *CacheMetrics L2SourceCache *CacheMetrics
DerivationIdle prometheus.Gauge DerivationIdle prometheus.Gauge
...@@ -136,6 +136,7 @@ func NewMetrics(procName string) *Metrics { ...@@ -136,6 +136,7 @@ func NewMetrics(procName string) *Metrics {
}), }),
L1SourceCache: NewCacheMetrics(registry, ns, "l1_source_cache", "L1 Source cache"), 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{ DerivationIdle: promauto.With(registry).NewGauge(prometheus.GaugeOpts{
Namespace: ns, Namespace: ns,
......
...@@ -3,7 +3,6 @@ package node ...@@ -3,7 +3,6 @@ package node
import ( import (
"context" "context"
"fmt" "fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
...@@ -13,20 +12,14 @@ import ( ...@@ -13,20 +12,14 @@ import (
"github.com/ethereum-optimism/optimism/op-node/version" "github.com/ethereum-optimism/optimism/op-node/version"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common" "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/log"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
) )
type l2EthClient interface { 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 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) 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 { type driverClient interface {
...@@ -75,7 +68,7 @@ func (n *nodeAPI) OutputAtBlock(ctx context.Context, number rpc.BlockNumber) ([] ...@@ -75,7 +68,7 @@ func (n *nodeAPI) OutputAtBlock(ctx context.Context, number rpc.BlockNumber) ([]
defer recordDur() defer recordDur()
// TODO: rpc.BlockNumber doesn't support the "safe" tag. Need a new type // 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 { if err != nil {
n.log.Error("failed to get block", "err", err) n.log.Error("failed to get block", "err", err)
return nil, err return nil, err
...@@ -93,13 +86,13 @@ func (n *nodeAPI) OutputAtBlock(ctx context.Context, number rpc.BlockNumber) ([] ...@@ -93,13 +86,13 @@ func (n *nodeAPI) OutputAtBlock(ctx context.Context, number rpc.BlockNumber) ([]
return nil, ethereum.NotFound return nil, ethereum.NotFound
} }
// make sure that the proof (including storage hash) that we retrieved is correct by verifying it against the state-root // 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 { if err := proof.Verify(head.Root()); err != nil {
n.log.Error("invalid withdrawal root detected in block", "stateRoot", head.Root, "blocknum", number, "msg", err) n.log.Error("invalid withdrawal root detected in block", "stateRoot", head.Root(), "blocknum", number, "msg", err)
return nil, fmt.Errorf("invalid withdrawal root hash") return nil, fmt.Errorf("invalid withdrawal root hash")
} }
var l2OutputRootVersion eth.Bytes32 // it's zero for now 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 return []eth.Bytes32{l2OutputRootVersion, l2OutputRoot}, nil
} }
...@@ -123,11 +116,7 @@ func (n *nodeAPI) Version(ctx context.Context) (string, error) { ...@@ -123,11 +116,7 @@ func (n *nodeAPI) Version(ctx context.Context) (string, error) {
} }
func toBlockNumArg(number rpc.BlockNumber) string { func toBlockNumArg(number rpc.BlockNumber) string {
if number == rpc.LatestBlockNumber { // never returns an error
return "latest" out, _ := number.MarshalText()
} return string(out)
if number == rpc.PendingBlockNumber {
return "pending"
}
return hexutil.EncodeUint64(uint64(number.Int64()))
} }
...@@ -11,7 +11,6 @@ import ( ...@@ -11,7 +11,6 @@ import (
"github.com/ethereum-optimism/optimism/op-node/client" "github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth" "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/metrics"
"github.com/ethereum-optimism/optimism/op-node/p2p" "github.com/ethereum-optimism/optimism/op-node/p2p"
"github.com/ethereum-optimism/optimism/op-node/rollup/driver" "github.com/ethereum-optimism/optimism/op-node/rollup/driver"
...@@ -28,9 +27,8 @@ type OpNode struct { ...@@ -28,9 +27,8 @@ type OpNode struct {
metrics *metrics.Metrics metrics *metrics.Metrics
l1HeadsSub ethereum.Subscription // Subscription to get L1 heads (automatically re-subscribes on error) l1HeadsSub ethereum.Subscription // Subscription to get L1 heads (automatically re-subscribes on error)
l1Source *sources.L1Client // L1 Client to fetch data from l1Source *sources.L1Client // L1 Client to fetch data from
l2Engine *driver.Driver // L2 Engine to Sync l2Driver *driver.Driver // L2 Engine to Sync
l2Node client.RPC // L2 Execution Engine RPC connections to close at shutdown l2Source *sources.EngineClient // L2 Execution Engine RPC bindings
l2Client client.Client // L2 client wrapper around eth namespace
server *rpcServer // RPC server hosting the rollup-node API server *rpcServer // RPC server hosting the rollup-node API
p2pNode *p2p.NodeP2P // P2P node functionality p2pNode *p2p.NodeP2P // P2P node functionality
p2pSigner p2p.Signer // p2p gogssip application messages will be signed with this signer 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 ...@@ -139,25 +137,23 @@ func (n *OpNode) initL2(ctx context.Context, cfg *Config, snapshotLog log.Logger
if err != nil { if err != nil {
return fmt.Errorf("failed to setup L2 execution-engine RPC client: %w", err) 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) n.l2Source, err = sources.NewEngineClient(
source, err := l2.NewSource(n.l2Node, n.l2Client, &cfg.Rollup.Genesis, n.log) client.NewInstrumentedRPC(rpcClient, n.metrics), n.log, n.metrics.L2SourceCache,
sources.EngineClientDefaultConfig(&cfg.Rollup),
)
if err != nil { 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 return nil
} }
func (n *OpNode) initRPCServer(ctx context.Context, cfg *Config) error { func (n *OpNode) initRPCServer(ctx context.Context, cfg *Config) error {
// TODO: attach the p2p node ID to the snapshot logger var err error
client, err := l2.NewReadOnlySource(n.l2Node, n.l2Client, &cfg.Rollup.Genesis, n.log) 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
}
n.server, err = newRPCServer(ctx, &cfg.RPC, &cfg.Rollup, client, n.l2Engine, n.log, n.appVersion, n.metrics)
if err != nil { if err != nil {
return err return err
} }
...@@ -165,7 +161,7 @@ func (n *OpNode) initRPCServer(ctx context.Context, cfg *Config) error { ...@@ -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)) n.server.EnableP2P(p2p.NewP2PAPIBackend(n.p2pNode, n.log, n.metrics))
} }
if cfg.RPC.EnableAdmin { 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") n.log.Info("Starting JSON-RPC server")
if err := n.server.Start(); err != nil { if err := n.server.Start(); err != nil {
...@@ -218,7 +214,7 @@ func (n *OpNode) Start(ctx context.Context) error { ...@@ -218,7 +214,7 @@ func (n *OpNode) Start(ctx context.Context) error {
// Request initial head update, default to genesis otherwise // Request initial head update, default to genesis otherwise
reqCtx, reqCancel := context.WithTimeout(ctx, time.Second*10) reqCtx, reqCancel := context.WithTimeout(ctx, time.Second*10)
// start driving engine: sync blocks by deriving them from L1 and driving them into the engine // 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() reqCancel()
if err != nil { if err != nil {
n.log.Error("Could not start a rollup node", "err", err) 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) { ...@@ -234,7 +230,7 @@ func (n *OpNode) OnNewL1Head(ctx context.Context, sig eth.L1BlockRef) {
// Pass on the event to the L2 Engine // Pass on the event to the L2 Engine
ctx, cancel := context.WithTimeout(ctx, time.Second*10) ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel() 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) 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 ...@@ -268,7 +264,7 @@ func (n *OpNode) OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *e
// Pass on the event to the L2 Engine // Pass on the event to the L2 Engine
ctx, cancel := context.WithTimeout(ctx, time.Second*30) ctx, cancel := context.WithTimeout(ctx, time.Second*30)
defer cancel() 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()) 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 { ...@@ -306,16 +302,16 @@ func (n *OpNode) Close() error {
n.l1HeadsSub.Unsubscribe() n.l1HeadsSub.Unsubscribe()
} }
// close L2 engine // close L2 driver
if n.l2Engine != nil { if n.l2Driver != nil {
if err := n.l2Engine.Close(); err != nil { if err := n.l2Driver.Close(); err != nil {
result = multierror.Append(result, fmt.Errorf("failed to close L2 engine driver cleanly: %w", err)) result = multierror.Append(result, fmt.Errorf("failed to close L2 engine driver cleanly: %w", err))
} }
} }
// close L2 node // close L2 engine RPC client
if n.l2Node != nil { if n.l2Source != nil {
n.l2Node.Close() n.l2Source.Close()
} }
// close L1 data source // close L1 data source
......
...@@ -7,11 +7,12 @@ import ( ...@@ -7,11 +7,12 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-node/metrics" "github.com/ethereum-optimism/optimism/op-node/metrics"
"github.com/ethereum-optimism/optimism/op-node/p2p" "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-optimism/optimism/op-node/rollup"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -28,7 +29,7 @@ type rpcServer struct { ...@@ -28,7 +29,7 @@ type rpcServer struct {
appVersion string appVersion string
listenAddr net.Addr listenAddr net.Addr
log log.Logger 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) { 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 ...@@ -3,10 +3,9 @@ package node
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"math/big"
"math/rand" "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/rollup/driver"
"github.com/ethereum-optimism/optimism/op-node/testutils" "github.com/ethereum-optimism/optimism/op-node/testutils"
...@@ -88,9 +87,20 @@ func TestOutputAtBlock(t *testing.T) { ...@@ -88,9 +87,20 @@ func TestOutputAtBlock(t *testing.T) {
// ignore other rollup config info in this test // ignore other rollup config info in this test
} }
l2Client := &mockL2Client{} l2Client := &testutils.MockL2Client{}
l2Client.mock.On("GetBlockHeader", "latest").Return(&header) info := &testutils.MockBlockInfo{
l2Client.mock.On("GetProof", predeploys.L2ToL1MessagePasserAddr, "latest").Return(&result) 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{} drClient := &mockDriverClient{}
...@@ -106,12 +116,12 @@ func TestOutputAtBlock(t *testing.T) { ...@@ -106,12 +116,12 @@ func TestOutputAtBlock(t *testing.T) {
err = client.CallContext(context.Background(), &out, "optimism_outputAtBlock", "latest") err = client.CallContext(context.Background(), &out, "optimism_outputAtBlock", "latest")
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, out, 2) assert.Len(t, out, 2)
l2Client.mock.AssertExpectations(t) l2Client.Mock.AssertExpectations(t)
} }
func TestVersion(t *testing.T) { func TestVersion(t *testing.T) {
log := testlog.Logger(t, log.LvlError) log := testlog.Logger(t, log.LvlError)
l2Client := &mockL2Client{} l2Client := &testutils.MockL2Client{}
drClient := &mockDriverClient{} drClient := &mockDriverClient{}
rpcCfg := &RPCConfig{ rpcCfg := &RPCConfig{
ListenAddr: "localhost", ListenAddr: "localhost",
...@@ -136,7 +146,7 @@ func TestVersion(t *testing.T) { ...@@ -136,7 +146,7 @@ func TestVersion(t *testing.T) {
func TestSyncStatus(t *testing.T) { func TestSyncStatus(t *testing.T) {
log := testlog.Logger(t, log.LvlError) log := testlog.Logger(t, log.LvlError)
l2Client := &mockL2Client{} l2Client := &testutils.MockL2Client{}
drClient := &mockDriverClient{} drClient := &mockDriverClient{}
rng := rand.New(rand.NewSource(1234)) rng := rand.New(rand.NewSource(1234))
status := driver.SyncStatus{ status := driver.SyncStatus{
...@@ -180,27 +190,3 @@ func (c *mockDriverClient) SyncStatus(ctx context.Context) (*driver.SyncStatus, ...@@ -180,27 +190,3 @@ func (c *mockDriverClient) SyncStatus(ctx context.Context) (*driver.SyncStatus,
func (c *mockDriverClient) ResetDerivationPipeline(ctx context.Context) error { func (c *mockDriverClient) ResetDerivationPipeline(ctx context.Context) error {
return c.Mock.MethodCalled("ResetDerivationPipeline").Get(0).(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 { ...@@ -22,7 +22,7 @@ type Engine interface {
NewPayload(ctx context.Context, payload *eth.ExecutionPayload) (*eth.PayloadStatusV1, error) NewPayload(ctx context.Context, payload *eth.ExecutionPayload) (*eth.PayloadStatusV1, error)
PayloadByHash(context.Context, common.Hash) (*eth.ExecutionPayload, error) PayloadByHash(context.Context, common.Hash) (*eth.ExecutionPayload, error)
PayloadByNumber(context.Context, uint64) (*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) L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error)
} }
...@@ -275,11 +275,12 @@ func (eq *EngineQueue) forceNextSafeAttributes(ctx context.Context) 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. // 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. // 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 { 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 { if err != nil {
return NewTemporaryError(fmt.Errorf("failed to find the L2 Head block: %w", err)) 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 { if err != nil {
return NewTemporaryError(fmt.Errorf("failed to find the L2 Heads to start from: %w", err)) return NewTemporaryError(fmt.Errorf("failed to find the L2 Heads to start from: %w", err))
} }
......
...@@ -2,7 +2,6 @@ package driver ...@@ -2,7 +2,6 @@ package driver
import ( import (
"context" "context"
"math/big"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
...@@ -45,8 +44,7 @@ type L1Chain interface { ...@@ -45,8 +44,7 @@ type L1Chain interface {
type L2Chain interface { type L2Chain interface {
derive.Engine derive.Engine
L2BlockRefHead(ctx context.Context) (eth.L2BlockRef, error) L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error)
L2BlockRefByNumber(ctx context.Context, l2Num *big.Int) (eth.L2BlockRef, error)
L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (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 { ...@@ -114,7 +114,7 @@ func (s *state) Start(ctx context.Context) error {
return err return err
} }
s.l1Head = l1Head 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.RecordL1Ref("l1_head", s.l1Head)
s.metrics.RecordL2Ref("l2_unsafe", s.l2Head) 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 { ...@@ -31,12 +31,19 @@ type EthClientConfig struct {
TransactionsCacheSize int TransactionsCacheSize int
// Number of block headers to cache // Number of block headers to cache
HeadersCacheSize int HeadersCacheSize int
// Number of payloads to cache
PayloadsCacheSize int
// If the RPC is untrusted, then we should not use cached information from responses, // If the RPC is untrusted, then we should not use cached information from responses,
// and instead verify against the block-hash. // and instead verify against the block-hash.
// Of real L1 blocks no deposits can be missed/faked, no batches can be missed/faked, // Of real L1 blocks no deposits can be missed/faked, no batches can be missed/faked,
// only the wrong L1 blocks can be retrieved. // only the wrong L1 blocks can be retrieved.
TrustRPC bool 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 { func (c *EthClientConfig) Check() error {
...@@ -49,6 +56,9 @@ func (c *EthClientConfig) Check() error { ...@@ -49,6 +56,9 @@ func (c *EthClientConfig) Check() error {
if c.HeadersCacheSize < 0 { if c.HeadersCacheSize < 0 {
return fmt.Errorf("invalid headers cache size: %d", c.HeadersCacheSize) 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 { if c.MaxConcurrentRequests < 1 {
return fmt.Errorf("expected at least 1 concurrent request, but max is %d", c.MaxConcurrentRequests) return fmt.Errorf("expected at least 1 concurrent request, but max is %d", c.MaxConcurrentRequests)
} }
...@@ -66,6 +76,8 @@ type EthClient struct { ...@@ -66,6 +76,8 @@ type EthClient struct {
trustRPC bool trustRPC bool
mustBePostMerge bool
log log.Logger log log.Logger
// cache receipts in bundles per block hash // cache receipts in bundles per block hash
...@@ -79,6 +91,10 @@ type EthClient struct { ...@@ -79,6 +91,10 @@ type EthClient struct {
// cache block headers of blocks by hash // cache block headers of blocks by hash
// common.Hash -> *HeaderInfo // common.Hash -> *HeaderInfo
headersCache *caching.LRUCache headersCache *caching.LRUCache
// cache payloads by hash
// common.Hash -> *eth.ExecutionPayload
payloadsCache *caching.LRUCache
} }
// NewEthClient wraps a RPC with bindings to fetch ethereum data, // 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 ...@@ -96,6 +112,7 @@ func NewEthClient(client client.RPC, log log.Logger, metrics caching.Metrics, co
receiptsCache: caching.NewLRUCache(metrics, "receipts", config.ReceiptsCacheSize), receiptsCache: caching.NewLRUCache(metrics, "receipts", config.ReceiptsCacheSize),
transactionsCache: caching.NewLRUCache(metrics, "txs", config.TransactionsCacheSize), transactionsCache: caching.NewLRUCache(metrics, "txs", config.TransactionsCacheSize),
headersCache: caching.NewLRUCache(metrics, "headers", config.HeadersCacheSize), headersCache: caching.NewLRUCache(metrics, "headers", config.HeadersCacheSize),
payloadsCache: caching.NewLRUCache(metrics, "payloads", config.PayloadsCacheSize),
}, nil }, nil
} }
...@@ -115,7 +132,7 @@ func (s *EthClient) headerCall(ctx context.Context, method string, id interface{ ...@@ -115,7 +132,7 @@ func (s *EthClient) headerCall(ctx context.Context, method string, id interface{
if header == nil { if header == nil {
return nil, ethereum.NotFound return nil, ethereum.NotFound
} }
info, err := header.Info(s.trustRPC) info, err := header.Info(s.trustRPC, s.mustBePostMerge)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -132,7 +149,7 @@ func (s *EthClient) blockCall(ctx context.Context, method string, id interface{} ...@@ -132,7 +149,7 @@ func (s *EthClient) blockCall(ctx context.Context, method string, id interface{}
if block == nil { if block == nil {
return nil, nil, ethereum.NotFound return nil, nil, ethereum.NotFound
} }
info, txs, err := block.Info(s.trustRPC) info, txs, err := block.Info(s.trustRPC, s.mustBePostMerge)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
...@@ -141,6 +158,23 @@ func (s *EthClient) blockCall(ctx context.Context, method string, id interface{} ...@@ -141,6 +158,23 @@ func (s *EthClient) blockCall(ctx context.Context, method string, id interface{}
return info, txs, nil 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) { func (s *EthClient) InfoByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, error) {
if header, ok := s.headersCache.Get(hash); ok { if header, ok := s.headersCache.Get(hash); ok {
return header.(*HeaderInfo), nil return header.(*HeaderInfo), nil
...@@ -182,6 +216,21 @@ func (s *EthClient) InfoAndTxsByLabel(ctx context.Context, label eth.BlockLabel) ...@@ -182,6 +216,21 @@ func (s *EthClient) InfoAndTxsByLabel(ctx context.Context, label eth.BlockLabel)
return s.blockCall(ctx, "eth_getBlockByNumber", string(label)) 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) { func (s *EthClient) Fetch(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Transactions, eth.ReceiptsFetcher, error) {
info, txs, err := s.InfoAndTxsByHash(ctx, blockHash) info, txs, err := s.InfoAndTxsByHash(ctx, blockHash)
if err != nil { if err != nil {
...@@ -224,7 +273,7 @@ func (s *EthClient) BlockIDRange(ctx context.Context, begin eth.BlockID, max uin ...@@ -224,7 +273,7 @@ func (s *EthClient) BlockIDRange(ctx context.Context, begin eth.BlockID, max uin
if result == nil { if result == nil {
break // no more headers from here break // no more headers from here
} }
info, err := result.Info(s.trustRPC) info, err := result.Info(s.trustRPC, s.mustBePostMerge)
if err != nil { if err != nil {
return nil, fmt.Errorf("bad header data for block %s: %w", headerRequests[i].Args[0], err) return nil, fmt.Errorf("bad header data for block %s: %w", headerRequests[i].Args[0], err)
} }
......
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-node/client" "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"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
...@@ -43,9 +44,11 @@ var testEthClientConfig = &EthClientConfig{ ...@@ -43,9 +44,11 @@ var testEthClientConfig = &EthClientConfig{
ReceiptsCacheSize: 10, ReceiptsCacheSize: 10,
TransactionsCacheSize: 10, TransactionsCacheSize: 10,
HeadersCacheSize: 10, HeadersCacheSize: 10,
PayloadsCacheSize: 10,
MaxRequestsPerBatch: 20, MaxRequestsPerBatch: 20,
MaxConcurrentRequests: 10, MaxConcurrentRequests: 10,
TrustRPC: false, TrustRPC: false,
MustBePostMerge: false,
} }
func randHash() (out common.Hash) { func randHash() (out common.Hash) {
...@@ -73,8 +76,23 @@ func randHeader() (*types.Header, *rpcHeader) { ...@@ -73,8 +76,23 @@ func randHeader() (*types.Header, *rpcHeader) {
BaseFee: big.NewInt(100), BaseFee: big.NewInt(100),
} }
rhdr := &rpcHeader{ rhdr := &rpcHeader{
cache: rpcHeaderCacheInfo{Hash: hdr.Hash()}, ParentHash: hdr.ParentHash,
header: *hdr, 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 return hdr, rhdr
} }
...@@ -82,20 +100,20 @@ func randHeader() (*types.Header, *rpcHeader) { ...@@ -82,20 +100,20 @@ func randHeader() (*types.Header, *rpcHeader) {
func TestEthClient_InfoByHash(t *testing.T) { func TestEthClient_InfoByHash(t *testing.T) {
m := new(mockRPC) m := new(mockRPC)
_, rhdr := randHeader() _, rhdr := randHeader()
expectedInfo, _ := rhdr.Info(true) expectedInfo, _ := rhdr.Info(true, false)
ctx := context.Background() ctx := context.Background()
m.On("CallContext", ctx, new(*rpcHeader), 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 *args[1].(**rpcHeader) = rhdr
}).Return([]error{nil}) }).Return([]error{nil})
s, err := NewEthClient(m, nil, nil, testEthClientConfig) s, err := NewEthClient(m, nil, nil, testEthClientConfig)
require.NoError(t, err) require.NoError(t, err)
info, err := s.InfoByHash(ctx, rhdr.cache.Hash) info, err := s.InfoByHash(ctx, rhdr.Hash)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, info, expectedInfo) require.Equal(t, info, expectedInfo)
m.Mock.AssertExpectations(t) m.Mock.AssertExpectations(t)
// Again, without expecting any calls from the mock, the cache will return the block // 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.NoError(t, err)
require.Equal(t, info, expectedInfo) require.Equal(t, info, expectedInfo)
m.Mock.AssertExpectations(t) m.Mock.AssertExpectations(t)
...@@ -104,16 +122,16 @@ func TestEthClient_InfoByHash(t *testing.T) { ...@@ -104,16 +122,16 @@ func TestEthClient_InfoByHash(t *testing.T) {
func TestEthClient_InfoByNumber(t *testing.T) { func TestEthClient_InfoByNumber(t *testing.T) {
m := new(mockRPC) m := new(mockRPC)
_, rhdr := randHeader() _, rhdr := randHeader()
expectedInfo, _ := rhdr.Info(true) expectedInfo, _ := rhdr.Info(true, false)
n := rhdr.header.Number n := rhdr.Number
ctx := context.Background() ctx := context.Background()
m.On("CallContext", ctx, new(*rpcHeader), 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 *args[1].(**rpcHeader) = rhdr
}).Return([]error{nil}) }).Return([]error{nil})
s, err := NewL1Client(m, nil, nil, L1ClientDefaultConfig(&rollup.Config{SeqWindowSize: 10}, true)) s, err := NewL1Client(m, nil, nil, L1ClientDefaultConfig(&rollup.Config{SeqWindowSize: 10}, true))
require.NoError(t, err) require.NoError(t, err)
info, err := s.InfoByNumber(ctx, n.Uint64()) info, err := s.InfoByNumber(ctx, uint64(n))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, info, expectedInfo) require.Equal(t, info, expectedInfo)
m.Mock.AssertExpectations(t) m.Mock.AssertExpectations(t)
......
...@@ -30,9 +30,11 @@ func L1ClientDefaultConfig(config *rollup.Config, trustRPC bool) *L1ClientConfig ...@@ -30,9 +30,11 @@ func L1ClientDefaultConfig(config *rollup.Config, trustRPC bool) *L1ClientConfig
ReceiptsCacheSize: span, ReceiptsCacheSize: span,
TransactionsCacheSize: span, TransactionsCacheSize: span,
HeadersCacheSize: span, HeadersCacheSize: span,
PayloadsCacheSize: span,
MaxRequestsPerBatch: 20, // TODO: tune batch param MaxRequestsPerBatch: 20, // TODO: tune batch param
MaxConcurrentRequests: 10, MaxConcurrentRequests: 10,
TrustRPC: trustRPC, TrustRPC: trustRPC,
MustBePostMerge: false,
}, },
L1BlockRefsCacheSize: span, 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 ...@@ -2,14 +2,17 @@ package sources
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"math/big" "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-optimism/optimism/op-node/eth"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie"
) )
...@@ -20,6 +23,7 @@ type BatchCallContextFn func(ctx context.Context, b []rpc.BatchElem) error ...@@ -20,6 +23,7 @@ type BatchCallContextFn func(ctx context.Context, b []rpc.BatchElem) error
// - ignore uncle data (does not even exist anymore post-Merge) // - ignore uncle data (does not even exist anymore post-Merge)
// - use cached block hash, if we trust the RPC. // - 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 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, // 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. // and we only need to compute the sender for transactions into the inbox.
...@@ -83,71 +87,175 @@ func (info *HeaderInfo) ReceiptHash() common.Hash { ...@@ -83,71 +87,175 @@ func (info *HeaderInfo) ReceiptHash() common.Hash {
return info.receiptHash 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"` Hash common.Hash `json:"hash"`
} }
type rpcHeader struct { // checkPostMerge checks that the block header meets all criteria to be a valid ExecutionPayloadHeader,
cache rpcHeaderCacheInfo // see EIP-3675 (block header changes) and EIP-4399 (mixHash usage for prev-randao)
header types.Header 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 { func (hdr *rpcHeader) computeBlockHash() common.Hash {
if err := json.Unmarshal(msg, &header.header); err != nil { gethHeader := types.Header{
return err 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) { func (hdr *rpcHeader) Info(trustCache bool, mustBePostMerge bool) (*HeaderInfo, error) {
info := HeaderInfo{ if mustBePostMerge {
hash: header.cache.Hash, if err := hdr.checkPostMerge(); err != nil {
parentHash: header.header.ParentHash, return nil, err
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,
} }
if !trustCache { if !trustCache {
if computed := header.header.Hash(); 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, info.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 return &info, nil
} }
type rpcBlockCacheInfo struct { type rpcBlock struct {
rpcHeader
Transactions []*types.Transaction `json:"transactions"` Transactions []*types.Transaction `json:"transactions"`
} }
type rpcBlock struct { func (block *rpcBlock) verify() error {
header rpcHeader if computed := block.computeBlockHash(); computed != block.Hash {
extra rpcBlockCacheInfo 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 { func (block *rpcBlock) Info(trustCache bool, mustBePostMerge bool) (*HeaderInfo, types.Transactions, error) {
if err := json.Unmarshal(msg, &block.header); err != nil { if mustBePostMerge {
return err 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 // verify the header data
info, err := block.header.Info(trustCache) info, err := block.rpcHeader.Info(trustCache, mustBePostMerge)
if err != nil { 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 return info, block.Transactions, nil
hasher := trie.NewStackTrie(nil) }
computed := types.DeriveSha(types.Transactions(block.extra.Transactions), hasher)
if expected := info.txHash; expected != computed { func (block *rpcBlock) ExecutionPayload(trustCache bool) (*eth.ExecutionPayload, error) {
return nil, nil, fmt.Errorf("failed to verify transactions list: expected transactions root %s but retrieved %s", expected, computed) 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 ...@@ -145,7 +145,10 @@ func (m *FakeChainSource) L1BlockRefByLabel(ctx context.Context, label eth.Block
return m.l1s[m.l1reorg][m.l1head], nil 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) m.log.Trace("L2BlockRefHead", "l2Head", m.l2head, "reorg", m.l2reorg)
if len(m.l2s[m.l2reorg]) == 0 { if len(m.l2s[m.l2reorg]) == 0 {
panic("bad test, no l2 chain") panic("bad test, no l2 chain")
......
...@@ -4,30 +4,10 @@ import ( ...@@ -4,30 +4,10 @@ import (
"context" "context"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/mock"
) )
type MockEngine struct { type MockEngine struct {
mock.Mock MockL2Client
}
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)
} }
func (m *MockEngine) GetPayload(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayload, error) { 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 ...@@ -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) { func (m *MockEngine) ExpectNewPayload(payload *eth.ExecutionPayload, result *eth.PayloadStatusV1, err error) {
m.Mock.On("NewPayload", payload).Once().Return(result, &err) 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 ...@@ -77,6 +77,33 @@ func (m *MockEthClient) ExpectInfoAndTxsByLabel(label eth.BlockLabel, info eth.B
m.Mock.On("InfoAndTxsByLabel", label).Once().Return(info, transactions, &err) 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) { func (m *MockEthClient) Fetch(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Transactions, eth.ReceiptsFetcher, error) {
out := m.Mock.MethodCalled("Fetch", blockHash) out := m.Mock.MethodCalled("Fetch", blockHash)
return *out[0].(*eth.BlockInfo), out[1].(types.Transactions), out[2].(eth.ReceiptsFetcher), *out[3].(*error) 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