Commit 696bcc75 authored by Diederik Loerakker's avatar Diederik Loerakker Committed by GitHub

op-node: split l1 source into eth and l1 client (#3288)

* op-node: split l1 source into eth and l1 client

* op-node: fix comment typo
Co-authored-by: default avatarJaved Khan <javed@optimism.io>

* op-node: implement l1 rpc review suggestions
Co-authored-by: default avatarJaved Khan <javed@optimism.io>
Co-authored-by: default avatarmergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
parent 9d15e1ad
package l2
package eth
import (
"bytes"
"errors"
"fmt"
"math/big"
"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/core/types"
"github.com/ethereum/go-ethereum/ethdb/memorydb"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb/memorydb"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
)
func ComputeL2OutputRoot(l2OutputRootVersion eth.Bytes32, blockHash common.Hash, blockRoot common.Hash, storageRoot common.Hash) eth.Bytes32 {
var buf bytes.Buffer
buf.Write(l2OutputRootVersion[:])
buf.Write(blockRoot.Bytes())
buf.Write(storageRoot[:])
buf.Write(blockHash.Bytes())
return eth.Bytes32(crypto.Keccak256Hash(buf.Bytes()))
}
type AccountResult struct {
AccountProof []hexutil.Bytes `json:"accountProof"`
......@@ -82,40 +64,3 @@ func (res *AccountResult) Verify(stateRoot common.Hash) error {
}
return err
}
// BlockToBatch converts a L2 block to batch-data.
// Invalid L2 blocks may return an error.
func BlockToBatch(config *rollup.Config, block *types.Block) (*derive.BatchData, error) {
txs := block.Transactions()
if len(txs) == 0 {
return nil, errors.New("expected at least 1 transaction but found none")
}
if typ := txs[0].Type(); typ != types.DepositTxType {
return nil, fmt.Errorf("expected first tx to be a deposit of L1 info, but got type: %d", typ)
}
// encode non-deposit transactions
var opaqueTxs []hexutil.Bytes
for i, tx := range block.Transactions() {
if tx.Type() == types.DepositTxType {
continue
}
otx, err := tx.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("failed to encode tx %d in block: %v", i, err)
}
opaqueTxs = append(opaqueTxs, otx)
}
// figure out which L1 epoch this L2 block was derived from
l1Info, err := derive.L1InfoDepositTxData(txs[0].Data())
if err != nil {
return nil, fmt.Errorf("invalid L1 info deposit tx in block: %v", err)
}
return &derive.BatchData{BatchV1: derive.BatchV1{
EpochNum: rollup.Epoch(l1Info.Number), // the L1 block number equals the L2 epoch.
EpochHash: l1Info.BlockHash,
Timestamp: block.Time(),
Transactions: opaqueTxs,
}}, nil
}
......@@ -6,9 +6,10 @@ import (
"github.com/ethereum/go-ethereum/common"
)
type L1Info interface {
type BlockInfo interface {
Hash() common.Hash
ParentHash() common.Hash
Coinbase() common.Address
Root() common.Hash // state-root
NumberU64() uint64
Time() uint64
......@@ -16,6 +17,14 @@ type L1Info interface {
MixDigest() common.Hash
BaseFee() *big.Int
ID() BlockID
BlockRef() L1BlockRef
ReceiptHash() common.Hash
}
func InfoToL1BlockRef(info BlockInfo) L1BlockRef {
return L1BlockRef{
Hash: info.Hash(),
Number: info.NumberU64(),
ParentHash: info.ParentHash(),
Time: info.Time(),
}
}
package eth
type BlockLabel string
const (
// Unsafe is:
// - L1: absolute head of the chain
// - L2: absolute head of the chain, not confirmed on L1
Unsafe = "latest"
// Safe is:
// - L1: Justified checkpoint, beacon chain: 1 epoch of 2/3 of the validators attesting the epoch.
// - L2: Derived chain tip from L1 data
Safe = "safe"
// Finalized is:
// - L1: Finalized checkpoint, beacon chain: 2+ justified epochs with "supermajority link" (see FFG docs).
// More about FFG: https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/gasper/
// - L2: Derived chain tip from finalized L1 data
Finalized = "finalized"
)
......@@ -250,8 +250,8 @@ func (s *ReadOnlySource) GetBlockHeader(ctx context.Context, blockTag string) (*
return head, err
}
func (s *ReadOnlySource) GetProof(ctx context.Context, address common.Address, blockTag string) (*AccountResult, error) {
var getProofResponse *AccountResult
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
......
......@@ -7,7 +7,6 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"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/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/driver"
......@@ -23,7 +22,7 @@ import (
type l2EthClient interface {
GetBlockHeader(ctx context.Context, blockTag string) (*types.Header, 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) (*l2.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)
......@@ -100,7 +99,7 @@ func (n *nodeAPI) OutputAtBlock(ctx context.Context, number rpc.BlockNumber) ([]
}
var l2OutputRootVersion eth.Bytes32 // it's zero for now
l2OutputRoot := l2.ComputeL2OutputRoot(l2OutputRootVersion, head.Hash(), head.Root, proof.StorageHash)
l2OutputRoot := rollup.ComputeL2OutputRoot(l2OutputRootVersion, head.Hash(), head.Root, proof.StorageHash)
return []eth.Bytes32{l2OutputRootVersion, l2OutputRoot}, nil
}
......
......@@ -11,11 +11,11 @@ import (
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/l1"
"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"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/event"
......@@ -27,7 +27,7 @@ type OpNode struct {
appVersion string
metrics *metrics.Metrics
l1HeadsSub ethereum.Subscription // Subscription to get L1 heads (automatically re-subscribes on error)
l1Source *l1.Source // Source to fetch data from (also implements the Downloader interface)
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
......@@ -110,7 +110,9 @@ func (n *OpNode) initL1(ctx context.Context, cfg *Config) error {
return fmt.Errorf("failed to get L1 RPC client: %w", err)
}
n.l1Source, err = l1.NewSource(client.NewInstrumentedRPC(l1Node, n.metrics), n.metrics.L1SourceCache, l1.DefaultConfig(&cfg.Rollup, trustRPC))
n.l1Source, err = sources.NewL1Client(
client.NewInstrumentedRPC(l1Node, n.metrics), n.log, n.metrics.L1SourceCache,
sources.L1ClientDefaultConfig(&cfg.Rollup, trustRPC))
if err != nil {
return fmt.Errorf("failed to create L1 source: %v", err)
}
......
......@@ -6,6 +6,8 @@ import (
"math/big"
"math/rand"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum-optimism/optimism/op-node/rollup/driver"
"github.com/ethereum-optimism/optimism/op-node/testutils"
......@@ -23,8 +25,6 @@ import (
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/stretchr/testify/mock"
"github.com/ethereum-optimism/optimism/op-node/l2"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/assert"
......@@ -76,7 +76,7 @@ func TestOutputAtBlock(t *testing.T) {
"nonce": "0x1",
"storageHash": "0xc1917a80cb25ccc50d0d1921525a44fb619b4601194ca726ae32312f08a799f8"
}`
var result l2.AccountResult
var result eth.AccountResult
err = json.Unmarshal([]byte(resultTestData), &result)
assert.NoError(t, err)
......@@ -201,6 +201,6 @@ func (c *mockL2Client) GetBlockHeader(ctx context.Context, blockTag string) (*ty
return c.mock.MethodCalled("GetBlockHeader", blockTag).Get(0).(*types.Header), nil
}
func (c *mockL2Client) GetProof(ctx context.Context, address common.Address, blockTag string) (*l2.AccountResult, error) {
return c.mock.MethodCalled("GetProof", address, blockTag).Get(0).(*l2.AccountResult), 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
}
......@@ -14,8 +14,8 @@ import (
// L1ReceiptsFetcher fetches L1 header info and receipts for the payload attributes derivation (the info tx and deposits)
type L1ReceiptsFetcher interface {
InfoByHash(ctx context.Context, hash common.Hash) (eth.L1Info, error)
Fetch(ctx context.Context, blockHash common.Hash) (eth.L1Info, types.Transactions, eth.ReceiptsFetcher, error)
InfoByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, error)
Fetch(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Transactions, eth.ReceiptsFetcher, error)
}
// PreparePayloadAttributes prepares a PayloadAttributes template that is ready to build a L2 block with deposits only, on top of the given l2Parent, with the given epoch as L1 origin.
......@@ -24,7 +24,7 @@ type L1ReceiptsFetcher interface {
// The severity of the error is returned; a crit=false error means there was a temporary issue, like a failed RPC or time-out.
// A crit=true error means the input arguments are inconsistent or invalid.
func PreparePayloadAttributes(ctx context.Context, cfg *rollup.Config, dl L1ReceiptsFetcher, l2Parent eth.L2BlockRef, timestamp uint64, epoch eth.BlockID) (attrs *eth.PayloadAttributes, err error) {
var l1Info eth.L1Info
var l1Info eth.BlockInfo
var depositTxs []hexutil.Bytes
var seqNumber uint64
......
......@@ -48,7 +48,7 @@ func TestAttributesQueue_Step(t *testing.T) {
DepositContractAddress: common.Address{0xbb},
}
rng := rand.New(rand.NewSource(1234))
l1Info := testutils.RandomL1Info(rng)
l1Info := testutils.RandomBlockInfo(rng)
l1Fetcher := &testutils.MockL1Source{}
defer l1Fetcher.AssertExpectations(t)
......
......@@ -32,7 +32,7 @@ func TestPreparePayloadAttributes(t *testing.T) {
defer l1Fetcher.AssertExpectations(t)
l2Parent := testutils.RandomL2BlockRef(rng)
l2Time := l2Parent.Time + cfg.BlockTime
l1Info := testutils.RandomL1Info(rng)
l1Info := testutils.RandomBlockInfo(rng)
l1Info.InfoNum = l2Parent.L1Origin.Number + 1
epoch := l1Info.ID()
l1Fetcher.ExpectFetch(epoch.Hash, l1Info, nil, nil, nil)
......@@ -46,7 +46,7 @@ func TestPreparePayloadAttributes(t *testing.T) {
defer l1Fetcher.AssertExpectations(t)
l2Parent := testutils.RandomL2BlockRef(rng)
l2Time := l2Parent.Time + cfg.BlockTime
l1Info := testutils.RandomL1Info(rng)
l1Info := testutils.RandomBlockInfo(rng)
l1Info.InfoNum = l2Parent.L1Origin.Number
epoch := l1Info.ID()
_, err := PreparePayloadAttributes(context.Background(), cfg, l1Fetcher, l2Parent, l2Time, epoch)
......@@ -86,7 +86,7 @@ func TestPreparePayloadAttributes(t *testing.T) {
defer l1Fetcher.AssertExpectations(t)
l2Parent := testutils.RandomL2BlockRef(rng)
l2Time := l2Parent.Time + cfg.BlockTime
l1Info := testutils.RandomL1Info(rng)
l1Info := testutils.RandomBlockInfo(rng)
l1Info.InfoParentHash = l2Parent.L1Origin.Hash
l1Info.InfoNum = l2Parent.L1Origin.Number + 1
epoch := l1Info.ID()
......@@ -109,7 +109,7 @@ func TestPreparePayloadAttributes(t *testing.T) {
defer l1Fetcher.AssertExpectations(t)
l2Parent := testutils.RandomL2BlockRef(rng)
l2Time := l2Parent.Time + cfg.BlockTime
l1Info := testutils.RandomL1Info(rng)
l1Info := testutils.RandomBlockInfo(rng)
l1Info.InfoParentHash = l2Parent.L1Origin.Hash
l1Info.InfoNum = l2Parent.L1Origin.Number + 1
......@@ -147,7 +147,7 @@ func TestPreparePayloadAttributes(t *testing.T) {
defer l1Fetcher.AssertExpectations(t)
l2Parent := testutils.RandomL2BlockRef(rng)
l2Time := l2Parent.Time + cfg.BlockTime
l1Info := testutils.RandomL1Info(rng)
l1Info := testutils.RandomBlockInfo(rng)
l1Info.InfoHash = l2Parent.L1Origin.Hash
l1Info.InfoNum = l2Parent.L1Origin.Number
......
......@@ -18,7 +18,7 @@ import (
//
type L1TransactionFetcher interface {
InfoAndTxsByHash(ctx context.Context, hash common.Hash) (eth.L1Info, types.Transactions, error)
InfoAndTxsByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, types.Transactions, error)
}
type DataSlice []eth.Data
......
......@@ -71,7 +71,7 @@ func (ct *calldataTest) Run(t *testing.T, setup *calldataTestSetup) {
}
}
info := testutils.RandomL1Info(rng)
info := testutils.RandomBlockInfo(rng)
l1Src.ExpectInfoAndTxsByHash(info.Hash(), info, txs, ct.err)
defer l1Src.Mock.AssertExpectations(t)
......
......@@ -85,7 +85,7 @@ func L1InfoDepositTxData(data []byte) (L1BlockInfo, error) {
// L1InfoDeposit creates a L1 Info deposit transaction based on the L1 block,
// and the L2 block-height difference with the start of the epoch.
func L1InfoDeposit(seqNumber uint64, block eth.L1Info) (*types.DepositTx, error) {
func L1InfoDeposit(seqNumber uint64, block eth.BlockInfo) (*types.DepositTx, error) {
infoDat := L1BlockInfo{
Number: block.NumberU64(),
Time: block.Time(),
......@@ -117,7 +117,7 @@ func L1InfoDeposit(seqNumber uint64, block eth.L1Info) (*types.DepositTx, error)
}
// L1InfoDepositBytes returns a serialized L1-info attributes transaction.
func L1InfoDepositBytes(seqNumber uint64, l1Info eth.L1Info) ([]byte, error) {
func L1InfoDepositBytes(seqNumber uint64, l1Info eth.BlockInfo) ([]byte, error) {
dep, err := L1InfoDeposit(seqNumber, l1Info)
if err != nil {
return nil, fmt.Errorf("failed to create L1 info tx: %v", err)
......
......@@ -12,39 +12,47 @@ import (
"github.com/stretchr/testify/require"
)
var _ eth.L1Info = (*testutils.MockL1Info)(nil)
var _ eth.BlockInfo = (*testutils.MockBlockInfo)(nil)
type infoTest struct {
name string
mkInfo func(rng *rand.Rand) *testutils.MockL1Info
mkInfo func(rng *rand.Rand) *testutils.MockBlockInfo
seqNr func(rng *rand.Rand) uint64
}
var MockDepositContractAddr = common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeef00000000")
func TestParseL1InfoDepositTxData(t *testing.T) {
randomSeqNr := func(rng *rand.Rand) uint64 {
return rng.Uint64()
}
// Go 1.18 will have native fuzzing for us to use, until then, we cover just the below cases
cases := []infoTest{
{"random", testutils.MakeL1Info(nil)},
{"zero basefee", testutils.MakeL1Info(func(l *testutils.MockL1Info) {
{"random", testutils.MakeBlockInfo(nil), randomSeqNr},
{"zero basefee", testutils.MakeBlockInfo(func(l *testutils.MockBlockInfo) {
l.InfoBaseFee = new(big.Int)
})},
{"zero time", testutils.MakeL1Info(func(l *testutils.MockL1Info) {
}), randomSeqNr},
{"zero time", testutils.MakeBlockInfo(func(l *testutils.MockBlockInfo) {
l.InfoTime = 0
})},
{"zero num", testutils.MakeL1Info(func(l *testutils.MockL1Info) {
}), randomSeqNr},
{"zero num", testutils.MakeBlockInfo(func(l *testutils.MockBlockInfo) {
l.InfoNum = 0
})},
{"zero seq", testutils.MakeL1Info(func(l *testutils.MockL1Info) {
l.InfoSequenceNumber = 0
})},
{"all zero", func(rng *rand.Rand) *testutils.MockL1Info {
return &testutils.MockL1Info{InfoBaseFee: new(big.Int)}
}), randomSeqNr},
{"zero seq", testutils.MakeBlockInfo(nil), func(rng *rand.Rand) uint64 {
return 0
}},
{"all zero", func(rng *rand.Rand) *testutils.MockBlockInfo {
return &testutils.MockBlockInfo{InfoBaseFee: new(big.Int)}
}, func(rng *rand.Rand) uint64 {
return 0
}},
}
for i, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
info := testCase.mkInfo(rand.New(rand.NewSource(int64(1234 + i))))
depTx, err := L1InfoDeposit(info.SequenceNumber(), info)
rng := rand.New(rand.NewSource(int64(1234 + i)))
info := testCase.mkInfo(rng)
seqNr := testCase.seqNr(rng)
depTx, err := L1InfoDeposit(seqNr, info)
require.NoError(t, err)
res, err := L1InfoDepositTxData(depTx.Data)
require.NoError(t, err, "expected valid deposit info")
......@@ -53,6 +61,7 @@ func TestParseL1InfoDepositTxData(t *testing.T) {
assert.True(t, res.BaseFee.Sign() >= 0)
assert.Equal(t, res.BaseFee.Bytes(), info.BaseFee().Bytes())
assert.Equal(t, res.BlockHash, info.Hash())
assert.Equal(t, res.SequenceNumber, seqNr)
})
}
t.Run("no data", func(t *testing.T) {
......
......@@ -11,7 +11,7 @@ import (
)
type L1Fetcher interface {
L1HeadBlockRef(ctx context.Context) (eth.L1BlockRef, error)
L1BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L1BlockRef, error)
L1BlockRefByNumberFetcher
L1BlockRefByHashFetcher
L1ReceiptsFetcher
......
......@@ -5,8 +5,6 @@ import (
"math/big"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/l1"
"github.com/ethereum-optimism/optimism/op-node/l2"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum/go-ethereum/common"
......@@ -36,13 +34,13 @@ type Metrics interface {
}
type Downloader interface {
InfoByHash(ctx context.Context, hash common.Hash) (eth.L1Info, error)
Fetch(ctx context.Context, blockHash common.Hash) (eth.L1Info, types.Transactions, eth.ReceiptsFetcher, error)
InfoByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, error)
Fetch(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Transactions, eth.ReceiptsFetcher, error)
}
type L1Chain interface {
derive.L1Fetcher
L1HeadBlockRef(context.Context) (eth.L1BlockRef, error)
L1BlockRefByLabel(context.Context, eth.BlockLabel) (eth.L1BlockRef, error)
}
type L2Chain interface {
......@@ -73,7 +71,7 @@ type Network interface {
PublishL2Payload(ctx context.Context, payload *eth.ExecutionPayload) error
}
func NewDriver(driverCfg *Config, cfg *rollup.Config, l2 *l2.Source, l1 *l1.Source, network Network, log log.Logger, snapshotLog log.Logger, metrics Metrics) *Driver {
func NewDriver(driverCfg *Config, cfg *rollup.Config, l2 L2Chain, l1 L1Chain, network Network, log log.Logger, snapshotLog log.Logger, metrics Metrics) *Driver {
output := &outputImpl{
Config: cfg,
dl: l1,
......
......@@ -109,7 +109,7 @@ func NewState(driverCfg *Config, log log.Logger, snapshotLog log.Logger, config
// Start starts up the state loop. The context is only for initialization.
// The loop will have been started iff err is not nil.
func (s *state) Start(ctx context.Context) error {
l1Head, err := s.l1.L1HeadBlockRef(ctx)
l1Head, err := s.l1.L1BlockRefByLabel(ctx, eth.Unsafe)
if err != nil {
return err
}
......
package rollup
import (
"bytes"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
func ComputeL2OutputRoot(l2OutputRootVersion eth.Bytes32, blockHash common.Hash, blockRoot common.Hash, storageRoot common.Hash) eth.Bytes32 {
var buf bytes.Buffer
buf.Write(l2OutputRootVersion[:])
buf.Write(blockRoot.Bytes())
buf.Write(storageRoot[:])
buf.Write(blockHash.Bytes())
return eth.Bytes32(crypto.Keccak256Hash(buf.Bytes()))
}
......@@ -48,7 +48,7 @@ import (
)
type L1Chain interface {
L1HeadBlockRef(ctx context.Context) (eth.L1BlockRef, error)
L1BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L1BlockRef, error)
L1BlockRefByNumber(ctx context.Context, number uint64) (eth.L1BlockRef, error)
}
......@@ -66,7 +66,7 @@ const MaxReorgDepth = 500
// or canonical in the L1 chain.
// - `canonical`: true if the block is canonical in the L1 chain.
func isAheadOrCanonical(ctx context.Context, l1 L1Chain, block eth.BlockID) (aheadOrCanonical bool, canonical bool, err error) {
if l1Head, err := l1.L1HeadBlockRef(ctx); err != nil {
if l1Head, err := l1.L1BlockRefByLabel(ctx, eth.Unsafe); err != nil {
return false, false, err
} else if block.Number > l1Head.Number {
return true, false, nil
......
package l1
package sources
import (
"context"
......@@ -23,7 +23,7 @@ type IterativeBatchCall[K any, V any, O any] struct {
makeRequest func(K) (V, rpc.BatchElem)
makeResults func([]K, []V) (O, error)
getBatch batchCallContextFn
getBatch BatchCallContextFn
requestsValues []V
scheduled chan rpc.BatchElem
......@@ -37,7 +37,7 @@ func NewIterativeBatchCall[K any, V any, O any](
requestsKeys []K,
makeRequest func(K) (V, rpc.BatchElem),
makeResults func([]K, []V) (O, error),
getBatch batchCallContextFn,
getBatch BatchCallContextFn,
batchSize int) *IterativeBatchCall[K, V, O] {
if len(requestsKeys) < batchSize {
......
package l1
package sources
import (
"context"
......@@ -7,16 +7,16 @@ import (
"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"
"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 SourceConfig struct {
type EthClientConfig struct {
// Maximum number of requests to make per batch
MaxRequestsPerBatch int
......@@ -39,7 +39,7 @@ type SourceConfig struct {
TrustRPC bool
}
func (c *SourceConfig) Check() error {
func (c *EthClientConfig) Check() error {
if c.ReceiptsCacheSize < 0 {
return fmt.Errorf("invalid receipts cache size: %d", c.ReceiptsCacheSize)
}
......@@ -58,39 +58,16 @@ func (c *SourceConfig) Check() error {
return nil
}
func DefaultConfig(config *rollup.Config, trustRPC bool) *SourceConfig {
// Cache 3/2 worth of sequencing window of receipts and txs
span := int(config.SeqWindowSize) * 3 / 2
if span > 1000 { // sanity cap. If a large sequencing window is configured, do not make the cache too large
span = 1000
}
return &SourceConfig{
// receipts and transactions are cached per block
ReceiptsCacheSize: span,
TransactionsCacheSize: span,
HeadersCacheSize: span,
// TODO: tune batch param
MaxRequestsPerBatch: 20,
MaxConcurrentRequests: 10,
TrustRPC: trustRPC,
}
}
type batchCallContextFn func(ctx context.Context, b []rpc.BatchElem) error
// Source to retrieve L1 data from with optimized batch requests, cached results,
// and flag to not trust the RPC.
type Source struct {
// EthClient retrieves ethereum data with optimized batch requests, cached results, and flag to not trust the RPC.
type EthClient struct {
client client.RPC
batchCall batchCallContextFn
maxBatchSize int
trustRPC bool
log log.Logger
// cache receipts in bundles per block hash
// common.Hash -> types.Receipts
receiptsCache *caching.LRUCache
......@@ -104,18 +81,18 @@ type Source struct {
headersCache *caching.LRUCache
}
// NewSource wraps a RPC with bindings to fetch L1 data, while logging errors, tracking metrics (optional), and caching.
func NewSource(client client.RPC, metrics caching.Metrics, config *SourceConfig) (*Source, error) {
// NewEthClient wraps a RPC with bindings to fetch ethereum data,
// while logging errors, parallel-requests constraint, tracking metrics (optional), and caching.
func NewEthClient(client client.RPC, log log.Logger, metrics caching.Metrics, config *EthClientConfig) (*EthClient, error) {
if err := config.Check(); err != nil {
return nil, fmt.Errorf("bad config, cannot create L1 source: %w", err)
}
client = LimitRPC(client, config.MaxConcurrentRequests)
return &Source{
return &EthClient{
client: client,
batchCall: client.BatchCallContext,
maxBatchSize: config.MaxRequestsPerBatch,
trustRPC: config.TrustRPC,
log: log,
receiptsCache: caching.NewLRUCache(metrics, "receipts", config.ReceiptsCacheSize),
transactionsCache: caching.NewLRUCache(metrics, "txs", config.TransactionsCacheSize),
headersCache: caching.NewLRUCache(metrics, "headers", config.HeadersCacheSize),
......@@ -123,13 +100,13 @@ func NewSource(client client.RPC, metrics caching.Metrics, config *SourceConfig)
}
// SubscribeNewHead subscribes to notifications about the current blockchain head on the given channel.
func (s *Source) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) {
func (s *EthClient) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) {
// Note that *types.Header does not cache the block hash unlike *HeaderInfo, it always recomputes.
// Inefficient if used poorly, but no trust issue.
return s.client.EthSubscribe(ctx, ch, "newHeads")
}
func (s *Source) headerCall(ctx context.Context, method string, id interface{}) (*HeaderInfo, error) {
func (s *EthClient) headerCall(ctx context.Context, method string, id interface{}) (*HeaderInfo, error) {
var header *rpcHeader
err := s.client.CallContext(ctx, &header, method, id, false) // headers are just blocks without txs
if err != nil {
......@@ -142,11 +119,11 @@ func (s *Source) headerCall(ctx context.Context, method string, id interface{})
if err != nil {
return nil, err
}
s.headersCache.Add(info.hash, info)
s.headersCache.Add(info.Hash(), info)
return info, nil
}
func (s *Source) blockCall(ctx context.Context, method string, id interface{}) (*HeaderInfo, types.Transactions, error) {
func (s *EthClient) blockCall(ctx context.Context, method string, id interface{}) (*HeaderInfo, types.Transactions, error) {
var block *rpcBlock
err := s.client.CallContext(ctx, &block, method, id, true)
if err != nil {
......@@ -159,29 +136,34 @@ func (s *Source) blockCall(ctx context.Context, method string, id interface{}) (
if err != nil {
return nil, nil, err
}
s.headersCache.Add(info.hash, info)
s.transactionsCache.Add(info.hash, txs)
s.headersCache.Add(info.Hash(), info)
s.transactionsCache.Add(info.Hash(), txs)
return info, txs, nil
}
func (s *Source) InfoByHash(ctx context.Context, hash common.Hash) (eth.L1Info, error) {
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
}
return s.headerCall(ctx, "eth_getBlockByHash", hash)
}
func (s *Source) InfoByNumber(ctx context.Context, number uint64) (eth.L1Info, error) {
func (s *EthClient) InfoByNumber(ctx context.Context, number uint64) (eth.BlockInfo, error) {
// can't hit the cache when querying by number due to reorgs.
return s.headerCall(ctx, "eth_getBlockByNumber", hexutil.EncodeUint64(number))
}
func (s *Source) InfoHead(ctx context.Context) (eth.L1Info, error) {
func (s *EthClient) InfoByLabel(ctx context.Context, label eth.BlockLabel) (eth.BlockInfo, error) {
// can't hit the cache when querying the head due to reorgs / changes.
return s.headerCall(ctx, "eth_getBlockByNumber", string(label))
}
func (s *EthClient) InfoByRpcNumber(ctx context.Context, num rpc.BlockNumber) (eth.BlockInfo, error) {
// can't hit the cache when querying the head due to reorgs / changes.
return s.headerCall(ctx, "eth_getBlockByNumber", "latest")
return s.headerCall(ctx, "eth_getBlockByNumber", num)
}
func (s *Source) InfoAndTxsByHash(ctx context.Context, hash common.Hash) (eth.L1Info, types.Transactions, error) {
func (s *EthClient) InfoAndTxsByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, types.Transactions, error) {
if header, ok := s.headersCache.Get(hash); ok {
if txs, ok := s.transactionsCache.Get(hash); ok {
return header.(*HeaderInfo), txs.(types.Transactions), nil
......@@ -190,17 +172,17 @@ func (s *Source) InfoAndTxsByHash(ctx context.Context, hash common.Hash) (eth.L1
return s.blockCall(ctx, "eth_getBlockByHash", hash)
}
func (s *Source) InfoAndTxsByNumber(ctx context.Context, number uint64) (eth.L1Info, types.Transactions, error) {
func (s *EthClient) InfoAndTxsByNumber(ctx context.Context, number uint64) (eth.BlockInfo, types.Transactions, error) {
// can't hit the cache when querying by number due to reorgs.
return s.blockCall(ctx, "eth_getBlockByNumber", hexutil.EncodeUint64(number))
}
func (s *Source) InfoAndTxsHead(ctx context.Context) (eth.L1Info, types.Transactions, error) {
func (s *EthClient) InfoAndTxsByLabel(ctx context.Context, label eth.BlockLabel) (eth.BlockInfo, types.Transactions, error) {
// can't hit the cache when querying the head due to reorgs / changes.
return s.blockCall(ctx, "eth_getBlockByNumber", "latest")
return s.blockCall(ctx, "eth_getBlockByNumber", string(label))
}
func (s *Source) Fetch(ctx context.Context, blockHash common.Hash) (eth.L1Info, 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)
if err != nil {
return nil, nil, nil, err
......@@ -212,90 +194,14 @@ func (s *Source) Fetch(ctx context.Context, blockHash common.Hash) (eth.L1Info,
for i := 0; i < len(txs); i++ {
txHashes[i] = txs[i].Hash()
}
r := NewReceiptsFetcher(info.ID(), info.ReceiptHash(), txHashes, s.batchCall, s.maxBatchSize)
r := NewReceiptsFetcher(info.ID(), info.ReceiptHash(), txHashes, s.client.BatchCallContext, s.maxBatchSize)
s.receiptsCache.Add(blockHash, r)
return info, txs, r, nil
}
// FetchAllTransactions fetches transaction lists of a window of blocks, and caches each block and the transactions
func (s *Source) FetchAllTransactions(ctx context.Context, window []eth.BlockID) ([]types.Transactions, error) {
// list of transaction lists
allTxLists := make([]types.Transactions, len(window))
var blockRequests []rpc.BatchElem
var requestIndices []int
for i := 0; i < len(window); i++ {
// if we are shifting the window by 1 block at a time, most of the results should already be in the cache.
txs, ok := s.transactionsCache.Get(window[i].Hash)
if ok {
allTxLists[i] = txs.(types.Transactions)
} else {
blockRequests = append(blockRequests, rpc.BatchElem{
Method: "eth_getBlockByHash",
Args: []interface{}{window[i].Hash, true}, // request block including transactions list
Result: new(rpcBlock),
Error: nil,
})
requestIndices = append(requestIndices, i) // remember the block index this request corresponds to
}
}
if len(blockRequests) > 0 {
if err := s.batchCall(ctx, blockRequests); err != nil {
return nil, err
}
}
// try to cache everything we have before halting on the results with errors
for i := 0; i < len(blockRequests); i++ {
if blockRequests[i].Error == nil {
info, txs, err := blockRequests[i].Result.(*rpcBlock).Info(s.trustRPC)
if err != nil {
return nil, fmt.Errorf("bad block data for block %s: %w", blockRequests[i].Args[0], err)
}
s.headersCache.Add(info.hash, info)
s.transactionsCache.Add(info.hash, txs)
allTxLists[requestIndices[i]] = txs
}
}
for i := 0; i < len(blockRequests); i++ {
if blockRequests[i].Error != nil {
return nil, fmt.Errorf("failed to retrieve transactions of block %s in batch of %d blocks: %w", window[i], len(blockRequests), blockRequests[i].Error)
}
}
return allTxLists, nil
}
func (s *Source) L1HeadBlockRef(ctx context.Context) (eth.L1BlockRef, error) {
head, err := s.InfoHead(ctx)
if err != nil {
return eth.L1BlockRef{}, fmt.Errorf("failed to fetch head header: %w", err)
}
return head.BlockRef(), nil
}
func (s *Source) L1BlockRefByNumber(ctx context.Context, l1Num uint64) (eth.L1BlockRef, error) {
head, err := s.InfoByNumber(ctx, l1Num)
if err != nil {
return eth.L1BlockRef{}, fmt.Errorf("failed to fetch header by num %d: %w", l1Num, err)
}
return head.BlockRef(), nil
}
func (s *Source) L1BlockRefByHash(ctx context.Context, hash common.Hash) (eth.L1BlockRef, error) {
block, err := s.InfoByHash(ctx, hash)
if err != nil {
return eth.L1BlockRef{}, fmt.Errorf("failed to fetch header by hash %v: %w", hash, err)
}
return block.BlockRef(), nil
}
// L1Range returns a range of L1 block beginning just after begin, up to max blocks.
// BlockIDRange returns a range of block IDs from the provided begin up to max blocks after the begin.
// This batch-requests all blocks by number in the range at once, and then verifies the consistency
func (s *Source) L1Range(ctx context.Context, begin eth.BlockID, max uint64) ([]eth.BlockID, error) {
func (s *EthClient) BlockIDRange(ctx context.Context, begin eth.BlockID, max uint64) ([]eth.BlockID, error) {
headerRequests := make([]rpc.BatchElem, max)
for i := uint64(0); i < max; i++ {
headerRequests[i] = rpc.BatchElem{
......@@ -305,7 +211,7 @@ func (s *Source) L1Range(ctx context.Context, begin eth.BlockID, max uint64) ([]
Error: nil,
}
}
if err := s.batchCall(ctx, headerRequests); err != nil {
if err := s.client.BatchCallContext(ctx, headerRequests); err != nil {
return nil, err
}
......@@ -322,14 +228,14 @@ func (s *Source) L1Range(ctx context.Context, begin eth.BlockID, max uint64) ([]
if err != nil {
return nil, fmt.Errorf("bad header data for block %s: %w", headerRequests[i].Args[0], err)
}
s.headersCache.Add(info.hash, info)
s.headersCache.Add(info.Hash(), info)
out = append(out, info.ID())
prev := begin
if i > 0 {
prev = out[i-1]
}
if prev.Hash != info.parentHash {
return nil, fmt.Errorf("inconsistent results from L1 chain range request, block %s not expected parent %s of %s", prev, info.parentHash, info.ID())
if prev.Hash != info.ParentHash() {
return nil, fmt.Errorf("inconsistent results from L1 chain range request, block %s not expected parent %s of %s", prev, info.ParentHash(), info.ID())
}
} else if errors.Is(headerRequests[i].Error, ethereum.NotFound) {
break // no more headers from here
......@@ -340,6 +246,15 @@ func (s *Source) L1Range(ctx context.Context, begin eth.BlockID, max uint64) ([]
return out, nil
}
func (s *Source) Close() {
func (s *EthClient) GetProof(ctx context.Context, address common.Address, blockTag string) (*eth.AccountResult, error) {
var getProofResponse *eth.AccountResult
err := s.client.CallContext(ctx, &getProofResponse, "eth_getProof", address, []common.Hash{}, blockTag)
if err == nil && getProofResponse == nil {
err = ethereum.NotFound
}
return getProofResponse, err
}
func (s *EthClient) Close() {
s.client.Close()
}
package l1
package sources
import (
"context"
......@@ -7,25 +7,19 @@ 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"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type mockRPC struct {
mock.Mock
}
// we catch the optimized version, instead of mocking a lot of split/parallel calls
func (m *mockRPC) batchCall(ctx context.Context, b []rpc.BatchElem) error {
return m.MethodCalled("batchCall", ctx, b).Get(0).([]error)[0]
}
func (m *mockRPC) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error {
return m.MethodCalled("BatchCallContext", ctx, b).Get(0).([]error)[0]
}
......@@ -45,152 +39,82 @@ func (m *mockRPC) Close() {
var _ client.RPC = (*mockRPC)(nil)
var testEthClientConfig = &EthClientConfig{
ReceiptsCacheSize: 10,
TransactionsCacheSize: 10,
HeadersCacheSize: 10,
MaxRequestsPerBatch: 20,
MaxConcurrentRequests: 10,
TrustRPC: false,
}
func randHash() (out common.Hash) {
rand.Read(out[:])
return out
}
func randHeader() *types.Header {
return &types.Header{
func randHeader() (*types.Header, *rpcHeader) {
hdr := &types.Header{
ParentHash: randHash(),
UncleHash: randHash(),
Coinbase: common.Address{},
Root: randHash(),
TxHash: randHash(),
ReceiptHash: randHash(),
Bloom: types.Bloom{},
Difficulty: big.NewInt(42),
Number: big.NewInt(1234),
GasLimit: 0,
GasUsed: 0,
Time: 123456,
Extra: make([]byte, 0),
MixDigest: randHash(),
Nonce: types.BlockNonce{},
BaseFee: big.NewInt(100),
}
}
func randTransaction(i uint64) *types.Transaction {
return types.NewTx(&types.DynamicFeeTx{
ChainID: big.NewInt(999),
Nonce: i,
GasTipCap: big.NewInt(1),
GasFeeCap: big.NewInt(100),
Gas: 21000,
To: &common.Address{0x42},
Value: big.NewInt(0),
})
}
func randTxs(offset uint64, count uint64) types.Transactions {
out := make(types.Transactions, count)
for i := uint64(0); i < count; i++ {
out[i] = randTransaction(offset + i)
}
return out
}
func TestSource_InfoByHash(t *testing.T) {
m := new(mockRPC)
hdr := randHeader()
rhdr := &rpcHeader{
cache: rpcHeaderCacheInfo{Hash: hdr.Hash()},
header: *hdr,
}
return hdr, rhdr
}
func TestEthClient_InfoByHash(t *testing.T) {
m := new(mockRPC)
_, rhdr := randHeader()
expectedInfo, _ := rhdr.Info(true)
h := rhdr.header.Hash()
ctx := context.Background()
m.On("CallContext", ctx, new(*rpcHeader), "eth_getBlockByHash", []interface{}{h, false}).Run(func(args mock.Arguments) {
m.On("CallContext", ctx, new(*rpcHeader),
"eth_getBlockByHash", []interface{}{rhdr.cache.Hash, false}).Run(func(args mock.Arguments) {
*args[1].(**rpcHeader) = rhdr
}).Return([]error{nil})
s, err := NewSource(m, nil, DefaultConfig(&rollup.Config{SeqWindowSize: 10}, true))
assert.NoError(t, err)
info, err := s.InfoByHash(ctx, h)
assert.NoError(t, err)
assert.Equal(t, info, expectedInfo)
s, err := NewEthClient(m, nil, nil, testEthClientConfig)
require.NoError(t, err)
info, err := s.InfoByHash(ctx, rhdr.cache.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, h)
assert.NoError(t, err)
assert.Equal(t, info, expectedInfo)
info, err = s.InfoByHash(ctx, rhdr.cache.Hash)
require.NoError(t, err)
require.Equal(t, info, expectedInfo)
m.Mock.AssertExpectations(t)
}
func TestSource_InfoByNumber(t *testing.T) {
func TestEthClient_InfoByNumber(t *testing.T) {
m := new(mockRPC)
hdr := randHeader()
rhdr := &rpcHeader{
cache: rpcHeaderCacheInfo{Hash: hdr.Hash()},
header: *hdr,
}
_, rhdr := randHeader()
expectedInfo, _ := rhdr.Info(true)
n := hdr.Number.Uint64()
n := rhdr.header.Number
ctx := context.Background()
m.On("CallContext", ctx, new(*rpcHeader), "eth_getBlockByNumber", []interface{}{hexutil.EncodeUint64(n), false}).Run(func(args mock.Arguments) {
m.On("CallContext", ctx, new(*rpcHeader),
"eth_getBlockByNumber", []interface{}{hexutil.EncodeBig(n), false}).Run(func(args mock.Arguments) {
*args[1].(**rpcHeader) = rhdr
}).Return([]error{nil})
s, err := NewSource(m, nil, DefaultConfig(&rollup.Config{SeqWindowSize: 10}, true))
assert.NoError(t, err)
info, err := s.InfoByNumber(ctx, n)
assert.NoError(t, err)
assert.Equal(t, info, expectedInfo)
m.Mock.AssertExpectations(t)
}
func TestSource_FetchAllTransactions(t *testing.T) {
m := new(mockRPC)
ctx := context.Background()
a, b := randHeader(), randHeader()
blocks := []*rpcBlock{
{
header: rpcHeader{
cache: rpcHeaderCacheInfo{
Hash: a.Hash(),
},
header: *a,
},
extra: rpcBlockCacheInfo{
Transactions: randTxs(0, 4),
},
},
{
header: rpcHeader{
cache: rpcHeaderCacheInfo{
Hash: b.Hash(),
},
header: *b,
},
extra: rpcBlockCacheInfo{
Transactions: randTxs(4, 3),
},
},
}
expectedRequest := make([]rpc.BatchElem, len(blocks))
expectedTxLists := make([]types.Transactions, len(blocks))
for i, b := range blocks {
expectedRequest[i] = rpc.BatchElem{Method: "eth_getBlockByHash", Args: []interface{}{b.header.header.Hash(), true}, Result: new(rpcBlock)}
expectedTxLists[i] = b.extra.Transactions
}
m.On("batchCall", ctx, expectedRequest).Run(func(args mock.Arguments) {
batch := args[1].([]rpc.BatchElem)
for i, b := range blocks {
*batch[i].Result.(*rpcBlock) = *b
}
}).Return([]error{nil})
s, err := NewSource(m, nil, DefaultConfig(&rollup.Config{SeqWindowSize: 10}, true))
assert.NoError(t, err)
s.batchCall = m.batchCall // override the optimized batch call
id := func(i int) eth.BlockID {
return eth.BlockID{Hash: blocks[i].header.header.Hash(), Number: blocks[i].header.header.Number.Uint64()}
}
txLists, err := s.FetchAllTransactions(ctx, []eth.BlockID{id(0), id(1)})
assert.NoError(t, err)
assert.Equal(t, txLists, expectedTxLists)
m.Mock.AssertExpectations(t)
// again, but now without expecting any calls (transactions were cached)
txLists, err = s.FetchAllTransactions(ctx, []eth.BlockID{id(0), id(1)})
assert.NoError(t, err)
assert.Equal(t, txLists, expectedTxLists)
s, err := NewL1Client(m, nil, nil, L1ClientDefaultConfig(&rollup.Config{SeqWindowSize: 10}, true))
require.NoError(t, err)
info, err := s.InfoByNumber(ctx, n.Uint64())
require.NoError(t, err)
require.Equal(t, info, expectedInfo)
m.Mock.AssertExpectations(t)
}
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/sources/caching"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
type L1ClientConfig struct {
EthClientConfig
L1BlockRefsCacheSize int
}
func L1ClientDefaultConfig(config *rollup.Config, trustRPC bool) *L1ClientConfig {
// Cache 3/2 worth of sequencing window of receipts and txs
span := int(config.SeqWindowSize) * 3 / 2
if span > 1000 { // sanity cap. If a large sequencing window is configured, do not make the cache too large
span = 1000
}
return &L1ClientConfig{
EthClientConfig: EthClientConfig{
// receipts and transactions are cached per block
ReceiptsCacheSize: span,
TransactionsCacheSize: span,
HeadersCacheSize: span,
MaxRequestsPerBatch: 20, // TODO: tune batch param
MaxConcurrentRequests: 10,
TrustRPC: trustRPC,
},
L1BlockRefsCacheSize: span,
}
}
// L1Client provides typed bindings to retrieve L1 data from an RPC source,
// with optimized batch requests, cached results, and flag to not trust the RPC
// (i.e. to verify all returned contents against corresponding block hashes).
type L1Client struct {
*EthClient
// cache L1BlockRef by hash
// common.Hash -> eth.L1BlockRef
l1BlockRefsCache *caching.LRUCache
}
// NewL1Client wraps a RPC with bindings to fetch L1 data, while logging errors, tracking metrics (optional), and caching.
func NewL1Client(client client.RPC, log log.Logger, metrics caching.Metrics, config *L1ClientConfig) (*L1Client, error) {
ethClient, err := NewEthClient(client, log, metrics, &config.EthClientConfig)
if err != nil {
return nil, err
}
return &L1Client{
EthClient: ethClient,
l1BlockRefsCache: caching.NewLRUCache(metrics, "blockrefs", config.L1BlockRefsCacheSize),
}, nil
}
func (s *L1Client) L1BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L1BlockRef, error) {
info, err := s.InfoByLabel(ctx, label)
if err != nil {
return eth.L1BlockRef{}, fmt.Errorf("failed to fetch head header: %w", err)
}
ref := eth.InfoToL1BlockRef(info)
s.l1BlockRefsCache.Add(ref.Hash, ref)
return ref, nil
}
func (s *L1Client) L1BlockRefByNumber(ctx context.Context, num uint64) (eth.L1BlockRef, error) {
info, err := s.InfoByNumber(ctx, num)
if err != nil {
return eth.L1BlockRef{}, fmt.Errorf("failed to fetch header by num %d: %w", num, err)
}
ref := eth.InfoToL1BlockRef(info)
s.l1BlockRefsCache.Add(ref.Hash, ref)
return ref, nil
}
func (s *L1Client) L1BlockRefByHash(ctx context.Context, hash common.Hash) (eth.L1BlockRef, error) {
if v, ok := s.l1BlockRefsCache.Get(hash); ok {
return v.(eth.L1BlockRef), nil
}
info, err := s.InfoByHash(ctx, hash)
if err != nil {
return eth.L1BlockRef{}, fmt.Errorf("failed to fetch header by hash %v: %w", hash, err)
}
ref := eth.InfoToL1BlockRef(info)
s.l1BlockRefsCache.Add(ref.Hash, ref)
return ref, nil
}
package l1
package sources
import (
"context"
......
package l1
package sources
import (
"fmt"
......@@ -82,7 +82,7 @@ func makeReceiptRequest(txHash common.Hash) (*types.Receipt, rpc.BatchElem) {
}
// NewReceiptsFetcher creates a receipt fetcher that can iteratively fetch the receipts matching the given txs.
func NewReceiptsFetcher(block eth.BlockID, receiptHash common.Hash, txHashes []common.Hash, getBatch batchCallContextFn, batchSize int) eth.ReceiptsFetcher {
func NewReceiptsFetcher(block eth.BlockID, receiptHash common.Hash, txHashes []common.Hash, getBatch BatchCallContextFn, batchSize int) eth.ReceiptsFetcher {
return NewIterativeBatchCall[common.Hash, *types.Receipt, types.Receipts](
txHashes,
makeReceiptRequest,
......
package l1
package sources
import (
"context"
"encoding/json"
"fmt"
"math/big"
......@@ -8,10 +9,13 @@ import (
"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"
)
// Note: we do this ugly typing because we want the best, and the standard bindings are not sufficient:
type BatchCallContextFn func(ctx context.Context, b []rpc.BatchElem) error
// Note: these types are used, instead of the geth types, to enable:
// - batched calls of many block requests (standard bindings do extra uncle-header fetches, cannot be batched nicely)
// - ignore uncle data (does not even exist anymore post-Merge)
// - use cached block hash, if we trust the RPC.
......@@ -22,9 +26,12 @@ import (
//
// This way we minimize RPC calls, enable batching, and can choose to verify what the RPC gives us.
// HeaderInfo contains all the header-info required to implement the eth.BlockInfo interface,
// used in the rollup state-transition, with pre-computed block hash.
type HeaderInfo struct {
hash common.Hash
parentHash common.Hash
coinbase common.Address
root common.Hash
number uint64
time uint64
......@@ -34,7 +41,7 @@ type HeaderInfo struct {
receiptHash common.Hash
}
var _ eth.L1Info = (*HeaderInfo)(nil)
var _ eth.BlockInfo = (*HeaderInfo)(nil)
func (info *HeaderInfo) Hash() common.Hash {
return info.hash
......@@ -44,6 +51,10 @@ func (info *HeaderInfo) ParentHash() common.Hash {
return info.parentHash
}
func (info *HeaderInfo) Coinbase() common.Address {
return info.coinbase
}
func (info *HeaderInfo) Root() common.Hash {
return info.root
}
......@@ -68,15 +79,6 @@ func (info *HeaderInfo) ID() eth.BlockID {
return eth.BlockID{Hash: info.hash, Number: info.number}
}
func (info *HeaderInfo) BlockRef() eth.L1BlockRef {
return eth.L1BlockRef{
Hash: info.hash,
Number: info.number,
ParentHash: info.parentHash,
Time: info.time,
}
}
func (info *HeaderInfo) ReceiptHash() common.Hash {
return info.receiptHash
}
......
......@@ -3,6 +3,7 @@ package testutils
import (
"context"
"errors"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum"
......@@ -132,7 +133,10 @@ func (m *FakeChainSource) L1BlockRefByHash(ctx context.Context, l1Hash common.Ha
return eth.L1BlockRef{}, ethereum.NotFound
}
func (m *FakeChainSource) L1HeadBlockRef(ctx context.Context) (eth.L1BlockRef, error) {
func (m *FakeChainSource) L1BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L1BlockRef, error) {
if label != eth.Unsafe {
return eth.L1BlockRef{}, fmt.Errorf("testutil FakeChainSource does not support L1BlockRefByLabel(%s)", label)
}
m.log.Trace("L1HeadBlockRef", "l1Head", m.l1head, "reorg", m.l1reorg)
l := len(m.l1s[m.l1reorg])
if l == 0 {
......
......@@ -9,57 +9,61 @@ import (
"github.com/ethereum/go-ethereum/core/types"
)
type MockL1Info struct {
type MockBlockInfo struct {
// Prefixed all fields with "Info" to avoid collisions with the interface method names.
InfoHash common.Hash
InfoParentHash common.Hash
InfoRoot common.Hash
InfoNum uint64
InfoTime uint64
InfoMixDigest [32]byte
InfoBaseFee *big.Int
InfoReceiptRoot common.Hash
InfoSequenceNumber uint64
InfoHash common.Hash
InfoParentHash common.Hash
InfoCoinbase common.Address
InfoRoot common.Hash
InfoNum uint64
InfoTime uint64
InfoMixDigest [32]byte
InfoBaseFee *big.Int
InfoReceiptRoot common.Hash
}
func (l *MockL1Info) Hash() common.Hash {
func (l *MockBlockInfo) Hash() common.Hash {
return l.InfoHash
}
func (l *MockL1Info) ParentHash() common.Hash {
func (l *MockBlockInfo) ParentHash() common.Hash {
return l.InfoParentHash
}
func (l *MockL1Info) Root() common.Hash {
func (l *MockBlockInfo) Coinbase() common.Address {
return l.InfoCoinbase
}
func (l *MockBlockInfo) Root() common.Hash {
return l.InfoRoot
}
func (l *MockL1Info) NumberU64() uint64 {
func (l *MockBlockInfo) NumberU64() uint64 {
return l.InfoNum
}
func (l *MockL1Info) Time() uint64 {
func (l *MockBlockInfo) Time() uint64 {
return l.InfoTime
}
func (l *MockL1Info) MixDigest() common.Hash {
func (l *MockBlockInfo) MixDigest() common.Hash {
return l.InfoMixDigest
}
func (l *MockL1Info) BaseFee() *big.Int {
func (l *MockBlockInfo) BaseFee() *big.Int {
return l.InfoBaseFee
}
func (l *MockL1Info) ReceiptHash() common.Hash {
func (l *MockBlockInfo) ReceiptHash() common.Hash {
return l.InfoReceiptRoot
}
func (l *MockL1Info) ID() eth.BlockID {
func (l *MockBlockInfo) ID() eth.BlockID {
return eth.BlockID{Hash: l.InfoHash, Number: l.InfoNum}
}
func (l *MockL1Info) BlockRef() eth.L1BlockRef {
func (l *MockBlockInfo) BlockRef() eth.L1BlockRef {
return eth.L1BlockRef{
Hash: l.InfoHash,
Number: l.InfoNum,
......@@ -68,26 +72,21 @@ func (l *MockL1Info) BlockRef() eth.L1BlockRef {
}
}
func (l *MockL1Info) SequenceNumber() uint64 {
return l.InfoSequenceNumber
}
func RandomL1Info(rng *rand.Rand) *MockL1Info {
return &MockL1Info{
InfoParentHash: RandomHash(rng),
InfoNum: rng.Uint64(),
InfoTime: rng.Uint64(),
InfoHash: RandomHash(rng),
InfoBaseFee: big.NewInt(rng.Int63n(1000_000 * 1e9)), // a million GWEI
InfoReceiptRoot: types.EmptyRootHash,
InfoRoot: RandomHash(rng),
InfoSequenceNumber: rng.Uint64(),
func RandomBlockInfo(rng *rand.Rand) *MockBlockInfo {
return &MockBlockInfo{
InfoParentHash: RandomHash(rng),
InfoNum: rng.Uint64(),
InfoTime: rng.Uint64(),
InfoHash: RandomHash(rng),
InfoBaseFee: big.NewInt(rng.Int63n(1000_000 * 1e9)), // a million GWEI
InfoReceiptRoot: types.EmptyRootHash,
InfoRoot: RandomHash(rng),
}
}
func MakeL1Info(fn func(l *MockL1Info)) func(rng *rand.Rand) *MockL1Info {
return func(rng *rand.Rand) *MockL1Info {
l := RandomL1Info(rng)
func MakeBlockInfo(fn func(l *MockBlockInfo)) func(rng *rand.Rand) *MockBlockInfo {
return func(rng *rand.Rand) *MockBlockInfo {
l := RandomBlockInfo(rng)
if fn != nil {
fn(l)
}
......
package testutils
import (
"context"
"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/stretchr/testify/mock"
)
type MockEthClient struct {
mock.Mock
}
func (m *MockEthClient) InfoByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, error) {
out := m.Mock.MethodCalled("InfoByHash", hash)
return *out[0].(*eth.BlockInfo), *out[1].(*error)
}
func (m *MockEthClient) ExpectInfoByHash(hash common.Hash, info eth.BlockInfo, err error) {
m.Mock.On("InfoByHash", hash).Once().Return(&info, &err)
}
func (m *MockEthClient) InfoByNumber(ctx context.Context, number uint64) (eth.BlockInfo, error) {
out := m.Mock.MethodCalled("InfoByNumber", number)
return *out[0].(*eth.BlockInfo), *out[1].(*error)
}
func (m *MockEthClient) ExpectInfoByNumber(number uint64, info eth.BlockInfo, err error) {
m.Mock.On("InfoByNumber", number).Once().Return(&info, &err)
}
func (m *MockEthClient) InfoByLabel(ctx context.Context, label eth.BlockLabel) (eth.BlockInfo, error) {
out := m.Mock.MethodCalled("InfoByLabel", label)
return *out[0].(*eth.BlockInfo), *out[1].(*error)
}
func (m *MockEthClient) ExpectInfoByLabel(label eth.BlockLabel, info eth.BlockInfo, err error) {
m.Mock.On("InfoByLabel", label).Once().Return(&info, &err)
}
func (m *MockEthClient) InfoByRpcNumber(ctx context.Context, num rpc.BlockNumber) (eth.BlockInfo, error) {
out := m.Mock.MethodCalled("InfoByRpcNumber", num)
return *out[0].(*eth.BlockInfo), *out[1].(*error)
}
func (m *MockEthClient) ExpectInfoByRpcNumber(num rpc.BlockNumber, info eth.BlockInfo, err error) {
m.Mock.On("InfoByRpcNumber", num).Once().Return(&info, &err)
}
func (m *MockEthClient) InfoAndTxsByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, types.Transactions, error) {
out := m.Mock.MethodCalled("InfoAndTxsByHash", hash)
return out[0].(eth.BlockInfo), out[1].(types.Transactions), *out[2].(*error)
}
func (m *MockEthClient) ExpectInfoAndTxsByHash(hash common.Hash, info eth.BlockInfo, transactions types.Transactions, err error) {
m.Mock.On("InfoAndTxsByHash", hash).Once().Return(info, transactions, &err)
}
func (m *MockEthClient) InfoAndTxsByNumber(ctx context.Context, number uint64) (eth.BlockInfo, types.Transactions, error) {
out := m.Mock.MethodCalled("InfoAndTxsByNumber", number)
return out[0].(eth.BlockInfo), out[1].(types.Transactions), *out[2].(*error)
}
func (m *MockEthClient) ExpectInfoAndTxsByNumber(number uint64, info eth.BlockInfo, transactions types.Transactions, err error) {
m.Mock.On("InfoAndTxsByNumber", number).Once().Return(info, transactions, &err)
}
func (m *MockEthClient) InfoAndTxsByLabel(ctx context.Context, label eth.BlockLabel) (eth.BlockInfo, types.Transactions, error) {
out := m.Mock.MethodCalled("InfoAndTxsByLabel", label)
return out[0].(eth.BlockInfo), out[1].(types.Transactions), *out[2].(*error)
}
func (m *MockEthClient) ExpectInfoAndTxsByLabel(label eth.BlockLabel, info eth.BlockInfo, transactions types.Transactions, err error) {
m.Mock.On("InfoAndTxsByLabel", label).Once().Return(info, transactions, &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)
}
func (m *MockEthClient) ExpectFetch(hash common.Hash, info eth.BlockInfo, transactions types.Transactions, receipts types.Receipts, err error) {
m.Mock.On("Fetch", hash).Once().Return(&info, transactions, eth.FetchedReceipts(receipts), &err)
}
func (m *MockEthClient) GetProof(ctx context.Context, address common.Address, blockTag string) (*eth.AccountResult, error) {
return m.Mock.MethodCalled("GetProof", address, blockTag).Get(0).(*eth.AccountResult), nil
}
func (m *MockEthClient) ExpectGetProof(address common.Address, blockTag string, result *eth.AccountResult, err error) {
m.Mock.On("GetProof", address, blockTag).Once().Return(result, &err)
}
......@@ -5,35 +5,28 @@ import (
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/mock"
)
type MockL1Source struct {
mock.Mock
MockEthClient
}
func (m *MockL1Source) InfoByHash(ctx context.Context, hash common.Hash) (eth.L1Info, error) {
out := m.Mock.MethodCalled("InfoByHash", hash)
return *out[0].(*eth.L1Info), *out[1].(*error)
}
func (m *MockL1Source) ExpectInfoByHash(hash common.Hash, info eth.L1Info, err error) {
m.Mock.On("InfoByHash", hash).Once().Return(&info, &err)
func (m *MockL1Source) L1BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L1BlockRef, error) {
out := m.Mock.MethodCalled("L1BlockRefByLabel")
return out[0].(eth.L1BlockRef), *out[1].(*error)
}
func (m *MockL1Source) L1HeadBlockRef(ctx context.Context) (eth.L1BlockRef, error) {
out := m.Mock.MethodCalled("L1HeadBlockRef")
return out[0].(eth.L1BlockRef), *out[1].(*error)
func (m *MockL1Source) ExpectL1BlockRefByLabel(label eth.BlockLabel, ref eth.L1BlockRef, err error) {
m.Mock.On("L1BlockRefByLabel", label).Once().Return(ref, &err)
}
func (m *MockL1Source) L1BlockRefByNumber(ctx context.Context, u uint64) (eth.L1BlockRef, error) {
out := m.Mock.MethodCalled("L1BlockRefByNumber", u)
func (m *MockL1Source) L1BlockRefByNumber(ctx context.Context, num uint64) (eth.L1BlockRef, error) {
out := m.Mock.MethodCalled("L1BlockRefByNumber", num)
return out[0].(eth.L1BlockRef), *out[1].(*error)
}
func (m *MockL1Source) ExpectL1BlockRefByNumber(u uint64, ref eth.L1BlockRef, err error) {
m.Mock.On("L1BlockRefByNumber", u).Once().Return(ref, &err)
func (m *MockL1Source) ExpectL1BlockRefByNumber(num uint64, ref eth.L1BlockRef, err error) {
m.Mock.On("L1BlockRefByNumber", num).Once().Return(ref, &err)
}
func (m *MockL1Source) L1BlockRefByHash(ctx context.Context, hash common.Hash) (eth.L1BlockRef, error) {
......@@ -44,21 +37,3 @@ func (m *MockL1Source) L1BlockRefByHash(ctx context.Context, hash common.Hash) (
func (m *MockL1Source) ExpectL1BlockRefByHash(hash common.Hash, ref eth.L1BlockRef, err error) {
m.Mock.On("L1BlockRefByHash", hash).Once().Return(ref, &err)
}
func (m *MockL1Source) Fetch(ctx context.Context, blockHash common.Hash) (eth.L1Info, types.Transactions, eth.ReceiptsFetcher, error) {
out := m.Mock.MethodCalled("Fetch", blockHash)
return *out[0].(*eth.L1Info), out[1].(types.Transactions), out[2].(eth.ReceiptsFetcher), *out[3].(*error)
}
func (m *MockL1Source) ExpectFetch(hash common.Hash, info eth.L1Info, transactions types.Transactions, receipts types.Receipts, err error) {
m.Mock.On("Fetch", hash).Once().Return(&info, transactions, eth.FetchedReceipts(receipts), &err)
}
func (m *MockL1Source) InfoAndTxsByHash(ctx context.Context, hash common.Hash) (eth.L1Info, types.Transactions, error) {
out := m.Mock.MethodCalled("InfoAndTxsByHash", hash)
return out[0].(eth.L1Info), out[1].(types.Transactions), *out[2].(*error)
}
func (m *MockL1Source) ExpectInfoAndTxsByHash(hash common.Hash, info eth.L1Info, transactions types.Transactions, err error) {
m.Mock.On("InfoAndTxsByHash", hash).Once().Return(info, transactions, &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