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 ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"math/big" "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"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto" "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" "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 { type AccountResult struct {
AccountProof []hexutil.Bytes `json:"accountProof"` AccountProof []hexutil.Bytes `json:"accountProof"`
...@@ -82,40 +64,3 @@ func (res *AccountResult) Verify(stateRoot common.Hash) error { ...@@ -82,40 +64,3 @@ func (res *AccountResult) Verify(stateRoot common.Hash) error {
} }
return err 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 ( ...@@ -6,9 +6,10 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
) )
type L1Info interface { type BlockInfo interface {
Hash() common.Hash Hash() common.Hash
ParentHash() common.Hash ParentHash() common.Hash
Coinbase() common.Address
Root() common.Hash // state-root Root() common.Hash // state-root
NumberU64() uint64 NumberU64() uint64
Time() uint64 Time() uint64
...@@ -16,6 +17,14 @@ type L1Info interface { ...@@ -16,6 +17,14 @@ type L1Info interface {
MixDigest() common.Hash MixDigest() common.Hash
BaseFee() *big.Int BaseFee() *big.Int
ID() BlockID ID() BlockID
BlockRef() L1BlockRef
ReceiptHash() common.Hash 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) (* ...@@ -250,8 +250,8 @@ func (s *ReadOnlySource) GetBlockHeader(ctx context.Context, blockTag string) (*
return head, err return head, err
} }
func (s *ReadOnlySource) GetProof(ctx context.Context, address common.Address, blockTag string) (*AccountResult, error) { func (s *ReadOnlySource) GetProof(ctx context.Context, address common.Address, blockTag string) (*eth.AccountResult, error) {
var getProofResponse *AccountResult var getProofResponse *eth.AccountResult
err := s.rpc.CallContext(ctx, &getProofResponse, "eth_getProof", address, []common.Hash{}, blockTag) err := s.rpc.CallContext(ctx, &getProofResponse, "eth_getProof", address, []common.Hash{}, blockTag)
if err == nil && getProofResponse == nil { if err == nil && getProofResponse == nil {
err = ethereum.NotFound err = ethereum.NotFound
......
...@@ -7,7 +7,6 @@ import ( ...@@ -7,7 +7,6 @@ import (
"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"
"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/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/driver" "github.com/ethereum-optimism/optimism/op-node/rollup/driver"
...@@ -23,7 +22,7 @@ import ( ...@@ -23,7 +22,7 @@ import (
type l2EthClient interface { type l2EthClient interface {
GetBlockHeader(ctx context.Context, blockTag string) (*types.Header, error) 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 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) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error)
L2BlockRefByNumber(ctx context.Context, l2Num *big.Int) (eth.L2BlockRef, 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) ([] ...@@ -100,7 +99,7 @@ func (n *nodeAPI) OutputAtBlock(ctx context.Context, number rpc.BlockNumber) ([]
} }
var l2OutputRootVersion eth.Bytes32 // it's zero for now 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 return []eth.Bytes32{l2OutputRootVersion, l2OutputRoot}, nil
} }
......
...@@ -11,11 +11,11 @@ import ( ...@@ -11,11 +11,11 @@ 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/l1"
"github.com/ethereum-optimism/optimism/op-node/l2" "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"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
...@@ -27,7 +27,7 @@ type OpNode struct { ...@@ -27,7 +27,7 @@ type OpNode struct {
appVersion string appVersion string
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 *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 l2Engine *driver.Driver // L2 Engine to Sync
l2Node client.RPC // L2 Execution Engine RPC connections to close at shutdown l2Node client.RPC // L2 Execution Engine RPC connections to close at shutdown
l2Client client.Client // L2 client wrapper around eth namespace l2Client client.Client // L2 client wrapper around eth namespace
...@@ -110,7 +110,9 @@ func (n *OpNode) initL1(ctx context.Context, cfg *Config) error { ...@@ -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) 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 { if err != nil {
return fmt.Errorf("failed to create L1 source: %v", err) return fmt.Errorf("failed to create L1 source: %v", err)
} }
......
...@@ -6,6 +6,8 @@ import ( ...@@ -6,6 +6,8 @@ import (
"math/big" "math/big"
"math/rand" "math/rand"
"github.com/ethereum/go-ethereum/common"
"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"
...@@ -23,8 +25,6 @@ import ( ...@@ -23,8 +25,6 @@ import (
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/stretchr/testify/mock" "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/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
...@@ -76,7 +76,7 @@ func TestOutputAtBlock(t *testing.T) { ...@@ -76,7 +76,7 @@ func TestOutputAtBlock(t *testing.T) {
"nonce": "0x1", "nonce": "0x1",
"storageHash": "0xc1917a80cb25ccc50d0d1921525a44fb619b4601194ca726ae32312f08a799f8" "storageHash": "0xc1917a80cb25ccc50d0d1921525a44fb619b4601194ca726ae32312f08a799f8"
}` }`
var result l2.AccountResult var result eth.AccountResult
err = json.Unmarshal([]byte(resultTestData), &result) err = json.Unmarshal([]byte(resultTestData), &result)
assert.NoError(t, err) assert.NoError(t, err)
...@@ -201,6 +201,6 @@ func (c *mockL2Client) GetBlockHeader(ctx context.Context, blockTag string) (*ty ...@@ -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 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) { func (c *mockL2Client) GetProof(ctx context.Context, address common.Address, blockTag string) (*eth.AccountResult, error) {
return c.mock.MethodCalled("GetProof", address, blockTag).Get(0).(*l2.AccountResult), nil return c.mock.MethodCalled("GetProof", address, blockTag).Get(0).(*eth.AccountResult), nil
} }
...@@ -14,8 +14,8 @@ import ( ...@@ -14,8 +14,8 @@ import (
// L1ReceiptsFetcher fetches L1 header info and receipts for the payload attributes derivation (the info tx and deposits) // L1ReceiptsFetcher fetches L1 header info and receipts for the payload attributes derivation (the info tx and deposits)
type L1ReceiptsFetcher interface { type L1ReceiptsFetcher interface {
InfoByHash(ctx context.Context, hash common.Hash) (eth.L1Info, error) InfoByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, error)
Fetch(ctx context.Context, blockHash common.Hash) (eth.L1Info, types.Transactions, eth.ReceiptsFetcher, 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. // 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 { ...@@ -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. // 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. // 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) { 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 depositTxs []hexutil.Bytes
var seqNumber uint64 var seqNumber uint64
......
...@@ -48,7 +48,7 @@ func TestAttributesQueue_Step(t *testing.T) { ...@@ -48,7 +48,7 @@ func TestAttributesQueue_Step(t *testing.T) {
DepositContractAddress: common.Address{0xbb}, DepositContractAddress: common.Address{0xbb},
} }
rng := rand.New(rand.NewSource(1234)) rng := rand.New(rand.NewSource(1234))
l1Info := testutils.RandomL1Info(rng) l1Info := testutils.RandomBlockInfo(rng)
l1Fetcher := &testutils.MockL1Source{} l1Fetcher := &testutils.MockL1Source{}
defer l1Fetcher.AssertExpectations(t) defer l1Fetcher.AssertExpectations(t)
......
...@@ -32,7 +32,7 @@ func TestPreparePayloadAttributes(t *testing.T) { ...@@ -32,7 +32,7 @@ func TestPreparePayloadAttributes(t *testing.T) {
defer l1Fetcher.AssertExpectations(t) defer l1Fetcher.AssertExpectations(t)
l2Parent := testutils.RandomL2BlockRef(rng) l2Parent := testutils.RandomL2BlockRef(rng)
l2Time := l2Parent.Time + cfg.BlockTime l2Time := l2Parent.Time + cfg.BlockTime
l1Info := testutils.RandomL1Info(rng) l1Info := testutils.RandomBlockInfo(rng)
l1Info.InfoNum = l2Parent.L1Origin.Number + 1 l1Info.InfoNum = l2Parent.L1Origin.Number + 1
epoch := l1Info.ID() epoch := l1Info.ID()
l1Fetcher.ExpectFetch(epoch.Hash, l1Info, nil, nil, nil) l1Fetcher.ExpectFetch(epoch.Hash, l1Info, nil, nil, nil)
...@@ -46,7 +46,7 @@ func TestPreparePayloadAttributes(t *testing.T) { ...@@ -46,7 +46,7 @@ func TestPreparePayloadAttributes(t *testing.T) {
defer l1Fetcher.AssertExpectations(t) defer l1Fetcher.AssertExpectations(t)
l2Parent := testutils.RandomL2BlockRef(rng) l2Parent := testutils.RandomL2BlockRef(rng)
l2Time := l2Parent.Time + cfg.BlockTime l2Time := l2Parent.Time + cfg.BlockTime
l1Info := testutils.RandomL1Info(rng) l1Info := testutils.RandomBlockInfo(rng)
l1Info.InfoNum = l2Parent.L1Origin.Number l1Info.InfoNum = l2Parent.L1Origin.Number
epoch := l1Info.ID() epoch := l1Info.ID()
_, err := PreparePayloadAttributes(context.Background(), cfg, l1Fetcher, l2Parent, l2Time, epoch) _, err := PreparePayloadAttributes(context.Background(), cfg, l1Fetcher, l2Parent, l2Time, epoch)
...@@ -86,7 +86,7 @@ func TestPreparePayloadAttributes(t *testing.T) { ...@@ -86,7 +86,7 @@ func TestPreparePayloadAttributes(t *testing.T) {
defer l1Fetcher.AssertExpectations(t) defer l1Fetcher.AssertExpectations(t)
l2Parent := testutils.RandomL2BlockRef(rng) l2Parent := testutils.RandomL2BlockRef(rng)
l2Time := l2Parent.Time + cfg.BlockTime l2Time := l2Parent.Time + cfg.BlockTime
l1Info := testutils.RandomL1Info(rng) l1Info := testutils.RandomBlockInfo(rng)
l1Info.InfoParentHash = l2Parent.L1Origin.Hash l1Info.InfoParentHash = l2Parent.L1Origin.Hash
l1Info.InfoNum = l2Parent.L1Origin.Number + 1 l1Info.InfoNum = l2Parent.L1Origin.Number + 1
epoch := l1Info.ID() epoch := l1Info.ID()
...@@ -109,7 +109,7 @@ func TestPreparePayloadAttributes(t *testing.T) { ...@@ -109,7 +109,7 @@ func TestPreparePayloadAttributes(t *testing.T) {
defer l1Fetcher.AssertExpectations(t) defer l1Fetcher.AssertExpectations(t)
l2Parent := testutils.RandomL2BlockRef(rng) l2Parent := testutils.RandomL2BlockRef(rng)
l2Time := l2Parent.Time + cfg.BlockTime l2Time := l2Parent.Time + cfg.BlockTime
l1Info := testutils.RandomL1Info(rng) l1Info := testutils.RandomBlockInfo(rng)
l1Info.InfoParentHash = l2Parent.L1Origin.Hash l1Info.InfoParentHash = l2Parent.L1Origin.Hash
l1Info.InfoNum = l2Parent.L1Origin.Number + 1 l1Info.InfoNum = l2Parent.L1Origin.Number + 1
...@@ -147,7 +147,7 @@ func TestPreparePayloadAttributes(t *testing.T) { ...@@ -147,7 +147,7 @@ func TestPreparePayloadAttributes(t *testing.T) {
defer l1Fetcher.AssertExpectations(t) defer l1Fetcher.AssertExpectations(t)
l2Parent := testutils.RandomL2BlockRef(rng) l2Parent := testutils.RandomL2BlockRef(rng)
l2Time := l2Parent.Time + cfg.BlockTime l2Time := l2Parent.Time + cfg.BlockTime
l1Info := testutils.RandomL1Info(rng) l1Info := testutils.RandomBlockInfo(rng)
l1Info.InfoHash = l2Parent.L1Origin.Hash l1Info.InfoHash = l2Parent.L1Origin.Hash
l1Info.InfoNum = l2Parent.L1Origin.Number l1Info.InfoNum = l2Parent.L1Origin.Number
......
...@@ -18,7 +18,7 @@ import ( ...@@ -18,7 +18,7 @@ import (
// //
type L1TransactionFetcher interface { 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 type DataSlice []eth.Data
......
...@@ -71,7 +71,7 @@ func (ct *calldataTest) Run(t *testing.T, setup *calldataTestSetup) { ...@@ -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) l1Src.ExpectInfoAndTxsByHash(info.Hash(), info, txs, ct.err)
defer l1Src.Mock.AssertExpectations(t) defer l1Src.Mock.AssertExpectations(t)
......
...@@ -85,7 +85,7 @@ func L1InfoDepositTxData(data []byte) (L1BlockInfo, error) { ...@@ -85,7 +85,7 @@ func L1InfoDepositTxData(data []byte) (L1BlockInfo, error) {
// L1InfoDeposit creates a L1 Info deposit transaction based on the L1 block, // L1InfoDeposit creates a L1 Info deposit transaction based on the L1 block,
// and the L2 block-height difference with the start of the epoch. // 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{ infoDat := L1BlockInfo{
Number: block.NumberU64(), Number: block.NumberU64(),
Time: block.Time(), Time: block.Time(),
...@@ -117,7 +117,7 @@ func L1InfoDeposit(seqNumber uint64, block eth.L1Info) (*types.DepositTx, error) ...@@ -117,7 +117,7 @@ func L1InfoDeposit(seqNumber uint64, block eth.L1Info) (*types.DepositTx, error)
} }
// L1InfoDepositBytes returns a serialized L1-info attributes transaction. // 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) dep, err := L1InfoDeposit(seqNumber, l1Info)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create L1 info tx: %v", err) return nil, fmt.Errorf("failed to create L1 info tx: %v", err)
......
...@@ -12,39 +12,47 @@ import ( ...@@ -12,39 +12,47 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var _ eth.L1Info = (*testutils.MockL1Info)(nil) var _ eth.BlockInfo = (*testutils.MockBlockInfo)(nil)
type infoTest struct { type infoTest struct {
name string 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") var MockDepositContractAddr = common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeef00000000")
func TestParseL1InfoDepositTxData(t *testing.T) { 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 // Go 1.18 will have native fuzzing for us to use, until then, we cover just the below cases
cases := []infoTest{ cases := []infoTest{
{"random", testutils.MakeL1Info(nil)}, {"random", testutils.MakeBlockInfo(nil), randomSeqNr},
{"zero basefee", testutils.MakeL1Info(func(l *testutils.MockL1Info) { {"zero basefee", testutils.MakeBlockInfo(func(l *testutils.MockBlockInfo) {
l.InfoBaseFee = new(big.Int) l.InfoBaseFee = new(big.Int)
})}, }), randomSeqNr},
{"zero time", testutils.MakeL1Info(func(l *testutils.MockL1Info) { {"zero time", testutils.MakeBlockInfo(func(l *testutils.MockBlockInfo) {
l.InfoTime = 0 l.InfoTime = 0
})}, }), randomSeqNr},
{"zero num", testutils.MakeL1Info(func(l *testutils.MockL1Info) { {"zero num", testutils.MakeBlockInfo(func(l *testutils.MockBlockInfo) {
l.InfoNum = 0 l.InfoNum = 0
})}, }), randomSeqNr},
{"zero seq", testutils.MakeL1Info(func(l *testutils.MockL1Info) { {"zero seq", testutils.MakeBlockInfo(nil), func(rng *rand.Rand) uint64 {
l.InfoSequenceNumber = 0 return 0
})}, }},
{"all zero", func(rng *rand.Rand) *testutils.MockL1Info { {"all zero", func(rng *rand.Rand) *testutils.MockBlockInfo {
return &testutils.MockL1Info{InfoBaseFee: new(big.Int)} return &testutils.MockBlockInfo{InfoBaseFee: new(big.Int)}
}, func(rng *rand.Rand) uint64 {
return 0
}}, }},
} }
for i, testCase := range cases { for i, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
info := testCase.mkInfo(rand.New(rand.NewSource(int64(1234 + i)))) rng := rand.New(rand.NewSource(int64(1234 + i)))
depTx, err := L1InfoDeposit(info.SequenceNumber(), info) info := testCase.mkInfo(rng)
seqNr := testCase.seqNr(rng)
depTx, err := L1InfoDeposit(seqNr, info)
require.NoError(t, err) require.NoError(t, err)
res, err := L1InfoDepositTxData(depTx.Data) res, err := L1InfoDepositTxData(depTx.Data)
require.NoError(t, err, "expected valid deposit info") require.NoError(t, err, "expected valid deposit info")
...@@ -53,6 +61,7 @@ func TestParseL1InfoDepositTxData(t *testing.T) { ...@@ -53,6 +61,7 @@ func TestParseL1InfoDepositTxData(t *testing.T) {
assert.True(t, res.BaseFee.Sign() >= 0) assert.True(t, res.BaseFee.Sign() >= 0)
assert.Equal(t, res.BaseFee.Bytes(), info.BaseFee().Bytes()) assert.Equal(t, res.BaseFee.Bytes(), info.BaseFee().Bytes())
assert.Equal(t, res.BlockHash, info.Hash()) assert.Equal(t, res.BlockHash, info.Hash())
assert.Equal(t, res.SequenceNumber, seqNr)
}) })
} }
t.Run("no data", func(t *testing.T) { t.Run("no data", func(t *testing.T) {
......
...@@ -11,7 +11,7 @@ import ( ...@@ -11,7 +11,7 @@ import (
) )
type L1Fetcher interface { type L1Fetcher interface {
L1HeadBlockRef(ctx context.Context) (eth.L1BlockRef, error) L1BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L1BlockRef, error)
L1BlockRefByNumberFetcher L1BlockRefByNumberFetcher
L1BlockRefByHashFetcher L1BlockRefByHashFetcher
L1ReceiptsFetcher L1ReceiptsFetcher
......
...@@ -5,8 +5,6 @@ import ( ...@@ -5,8 +5,6 @@ import (
"math/big" "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/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"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -36,13 +34,13 @@ type Metrics interface { ...@@ -36,13 +34,13 @@ type Metrics interface {
} }
type Downloader interface { type Downloader interface {
InfoByHash(ctx context.Context, hash common.Hash) (eth.L1Info, error) InfoByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, error)
Fetch(ctx context.Context, blockHash common.Hash) (eth.L1Info, types.Transactions, eth.ReceiptsFetcher, error) Fetch(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Transactions, eth.ReceiptsFetcher, error)
} }
type L1Chain interface { type L1Chain interface {
derive.L1Fetcher derive.L1Fetcher
L1HeadBlockRef(context.Context) (eth.L1BlockRef, error) L1BlockRefByLabel(context.Context, eth.BlockLabel) (eth.L1BlockRef, error)
} }
type L2Chain interface { type L2Chain interface {
...@@ -73,7 +71,7 @@ type Network interface { ...@@ -73,7 +71,7 @@ type Network interface {
PublishL2Payload(ctx context.Context, payload *eth.ExecutionPayload) error 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{ output := &outputImpl{
Config: cfg, Config: cfg,
dl: l1, dl: l1,
......
...@@ -109,7 +109,7 @@ func NewState(driverCfg *Config, log log.Logger, snapshotLog log.Logger, config ...@@ -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. // Start starts up the state loop. The context is only for initialization.
// The loop will have been started iff err is not nil. // The loop will have been started iff err is not nil.
func (s *state) Start(ctx context.Context) error { 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 { if err != nil {
return err 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 ( ...@@ -48,7 +48,7 @@ import (
) )
type L1Chain interface { 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) L1BlockRefByNumber(ctx context.Context, number uint64) (eth.L1BlockRef, error)
} }
...@@ -66,7 +66,7 @@ const MaxReorgDepth = 500 ...@@ -66,7 +66,7 @@ const MaxReorgDepth = 500
// or canonical in the L1 chain. // or canonical in the L1 chain.
// - `canonical`: true if the block is 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) { 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 return false, false, err
} else if block.Number > l1Head.Number { } else if block.Number > l1Head.Number {
return true, false, nil return true, false, nil
......
package l1 package sources
import ( import (
"context" "context"
...@@ -23,7 +23,7 @@ type IterativeBatchCall[K any, V any, O any] struct { ...@@ -23,7 +23,7 @@ type IterativeBatchCall[K any, V any, O any] struct {
makeRequest func(K) (V, rpc.BatchElem) makeRequest func(K) (V, rpc.BatchElem)
makeResults func([]K, []V) (O, error) makeResults func([]K, []V) (O, error)
getBatch batchCallContextFn getBatch BatchCallContextFn
requestsValues []V requestsValues []V
scheduled chan rpc.BatchElem scheduled chan rpc.BatchElem
...@@ -37,7 +37,7 @@ func NewIterativeBatchCall[K any, V any, O any]( ...@@ -37,7 +37,7 @@ func NewIterativeBatchCall[K any, V any, O any](
requestsKeys []K, requestsKeys []K,
makeRequest func(K) (V, rpc.BatchElem), makeRequest func(K) (V, rpc.BatchElem),
makeResults func([]K, []V) (O, error), makeResults func([]K, []V) (O, error),
getBatch batchCallContextFn, getBatch BatchCallContextFn,
batchSize int) *IterativeBatchCall[K, V, O] { batchSize int) *IterativeBatchCall[K, V, O] {
if len(requestsKeys) < batchSize { if len(requestsKeys) < batchSize {
......
package l1 package sources
import ( import (
"context" "context"
......
package l1 package sources
import ( import (
"context" "context"
...@@ -7,25 +7,19 @@ import ( ...@@ -7,25 +7,19 @@ 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"
"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/rpc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
) )
type mockRPC struct { type mockRPC struct {
mock.Mock 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 { func (m *mockRPC) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error {
return m.MethodCalled("BatchCallContext", ctx, b).Get(0).([]error)[0] return m.MethodCalled("BatchCallContext", ctx, b).Get(0).([]error)[0]
} }
...@@ -45,152 +39,82 @@ func (m *mockRPC) Close() { ...@@ -45,152 +39,82 @@ func (m *mockRPC) Close() {
var _ client.RPC = (*mockRPC)(nil) 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) { func randHash() (out common.Hash) {
rand.Read(out[:]) rand.Read(out[:])
return out return out
} }
func randHeader() *types.Header { func randHeader() (*types.Header, *rpcHeader) {
return &types.Header{ hdr := &types.Header{
ParentHash: randHash(), ParentHash: randHash(),
UncleHash: randHash(), UncleHash: randHash(),
Coinbase: common.Address{}, Coinbase: common.Address{},
Root: randHash(), Root: randHash(),
TxHash: randHash(), TxHash: randHash(),
ReceiptHash: randHash(), ReceiptHash: randHash(),
Bloom: types.Bloom{},
Difficulty: big.NewInt(42),
Number: big.NewInt(1234), Number: big.NewInt(1234),
GasLimit: 0,
GasUsed: 0,
Time: 123456, Time: 123456,
Extra: make([]byte, 0),
MixDigest: randHash(), MixDigest: randHash(),
Nonce: types.BlockNonce{},
BaseFee: big.NewInt(100), 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{ rhdr := &rpcHeader{
cache: rpcHeaderCacheInfo{Hash: hdr.Hash()}, cache: rpcHeaderCacheInfo{Hash: hdr.Hash()},
header: *hdr, header: *hdr,
} }
return hdr, rhdr
}
func TestEthClient_InfoByHash(t *testing.T) {
m := new(mockRPC)
_, rhdr := randHeader()
expectedInfo, _ := rhdr.Info(true) expectedInfo, _ := rhdr.Info(true)
h := rhdr.header.Hash()
ctx := context.Background() 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 *args[1].(**rpcHeader) = rhdr
}).Return([]error{nil}) }).Return([]error{nil})
s, err := NewSource(m, nil, DefaultConfig(&rollup.Config{SeqWindowSize: 10}, true)) s, err := NewEthClient(m, nil, nil, testEthClientConfig)
assert.NoError(t, err) require.NoError(t, err)
info, err := s.InfoByHash(ctx, h) info, err := s.InfoByHash(ctx, rhdr.cache.Hash)
assert.NoError(t, err) require.NoError(t, err)
assert.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, h) info, err = s.InfoByHash(ctx, rhdr.cache.Hash)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, info, expectedInfo) require.Equal(t, info, expectedInfo)
m.Mock.AssertExpectations(t) m.Mock.AssertExpectations(t)
} }
func TestSource_InfoByNumber(t *testing.T) { func TestEthClient_InfoByNumber(t *testing.T) {
m := new(mockRPC) m := new(mockRPC)
hdr := randHeader() _, rhdr := randHeader()
rhdr := &rpcHeader{
cache: rpcHeaderCacheInfo{Hash: hdr.Hash()},
header: *hdr,
}
expectedInfo, _ := rhdr.Info(true) expectedInfo, _ := rhdr.Info(true)
n := hdr.Number.Uint64() n := rhdr.header.Number
ctx := context.Background() 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 *args[1].(**rpcHeader) = rhdr
}).Return([]error{nil}) }).Return([]error{nil})
s, err := NewSource(m, nil, DefaultConfig(&rollup.Config{SeqWindowSize: 10}, true)) s, err := NewL1Client(m, nil, nil, L1ClientDefaultConfig(&rollup.Config{SeqWindowSize: 10}, true))
assert.NoError(t, err) require.NoError(t, err)
info, err := s.InfoByNumber(ctx, n) info, err := s.InfoByNumber(ctx, n.Uint64())
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, info, expectedInfo) require.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)
m.Mock.AssertExpectations(t) 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 ( import (
"context" "context"
......
package l1 package sources
import ( import (
"fmt" "fmt"
...@@ -82,7 +82,7 @@ func makeReceiptRequest(txHash common.Hash) (*types.Receipt, rpc.BatchElem) { ...@@ -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. // 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]( return NewIterativeBatchCall[common.Hash, *types.Receipt, types.Receipts](
txHashes, txHashes,
makeReceiptRequest, makeReceiptRequest,
......
package l1 package sources
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math/big" "math/big"
...@@ -8,10 +9,13 @@ import ( ...@@ -8,10 +9,13 @@ import (
"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"
) )
// 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) // - 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) // - 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.
...@@ -22,9 +26,12 @@ import ( ...@@ -22,9 +26,12 @@ import (
// //
// This way we minimize RPC calls, enable batching, and can choose to verify what the RPC gives us. // 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 { type HeaderInfo struct {
hash common.Hash hash common.Hash
parentHash common.Hash parentHash common.Hash
coinbase common.Address
root common.Hash root common.Hash
number uint64 number uint64
time uint64 time uint64
...@@ -34,7 +41,7 @@ type HeaderInfo struct { ...@@ -34,7 +41,7 @@ type HeaderInfo struct {
receiptHash common.Hash receiptHash common.Hash
} }
var _ eth.L1Info = (*HeaderInfo)(nil) var _ eth.BlockInfo = (*HeaderInfo)(nil)
func (info *HeaderInfo) Hash() common.Hash { func (info *HeaderInfo) Hash() common.Hash {
return info.hash return info.hash
...@@ -44,6 +51,10 @@ func (info *HeaderInfo) ParentHash() common.Hash { ...@@ -44,6 +51,10 @@ func (info *HeaderInfo) ParentHash() common.Hash {
return info.parentHash return info.parentHash
} }
func (info *HeaderInfo) Coinbase() common.Address {
return info.coinbase
}
func (info *HeaderInfo) Root() common.Hash { func (info *HeaderInfo) Root() common.Hash {
return info.root return info.root
} }
...@@ -68,15 +79,6 @@ func (info *HeaderInfo) ID() eth.BlockID { ...@@ -68,15 +79,6 @@ func (info *HeaderInfo) ID() eth.BlockID {
return eth.BlockID{Hash: info.hash, Number: info.number} 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 { func (info *HeaderInfo) ReceiptHash() common.Hash {
return info.receiptHash return info.receiptHash
} }
......
...@@ -3,6 +3,7 @@ package testutils ...@@ -3,6 +3,7 @@ package testutils
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"math/big" "math/big"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
...@@ -132,7 +133,10 @@ func (m *FakeChainSource) L1BlockRefByHash(ctx context.Context, l1Hash common.Ha ...@@ -132,7 +133,10 @@ func (m *FakeChainSource) L1BlockRefByHash(ctx context.Context, l1Hash common.Ha
return eth.L1BlockRef{}, ethereum.NotFound 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) m.log.Trace("L1HeadBlockRef", "l1Head", m.l1head, "reorg", m.l1reorg)
l := len(m.l1s[m.l1reorg]) l := len(m.l1s[m.l1reorg])
if l == 0 { if l == 0 {
......
...@@ -9,57 +9,61 @@ import ( ...@@ -9,57 +9,61 @@ import (
"github.com/ethereum/go-ethereum/core/types" "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. // Prefixed all fields with "Info" to avoid collisions with the interface method names.
InfoHash common.Hash InfoHash common.Hash
InfoParentHash common.Hash InfoParentHash common.Hash
InfoRoot common.Hash InfoCoinbase common.Address
InfoNum uint64 InfoRoot common.Hash
InfoTime uint64 InfoNum uint64
InfoMixDigest [32]byte InfoTime uint64
InfoBaseFee *big.Int InfoMixDigest [32]byte
InfoReceiptRoot common.Hash InfoBaseFee *big.Int
InfoSequenceNumber uint64 InfoReceiptRoot common.Hash
} }
func (l *MockL1Info) Hash() common.Hash { func (l *MockBlockInfo) Hash() common.Hash {
return l.InfoHash return l.InfoHash
} }
func (l *MockL1Info) ParentHash() common.Hash { func (l *MockBlockInfo) ParentHash() common.Hash {
return l.InfoParentHash 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 return l.InfoRoot
} }
func (l *MockL1Info) NumberU64() uint64 { func (l *MockBlockInfo) NumberU64() uint64 {
return l.InfoNum return l.InfoNum
} }
func (l *MockL1Info) Time() uint64 { func (l *MockBlockInfo) Time() uint64 {
return l.InfoTime return l.InfoTime
} }
func (l *MockL1Info) MixDigest() common.Hash { func (l *MockBlockInfo) MixDigest() common.Hash {
return l.InfoMixDigest return l.InfoMixDigest
} }
func (l *MockL1Info) BaseFee() *big.Int { func (l *MockBlockInfo) BaseFee() *big.Int {
return l.InfoBaseFee return l.InfoBaseFee
} }
func (l *MockL1Info) ReceiptHash() common.Hash { func (l *MockBlockInfo) ReceiptHash() common.Hash {
return l.InfoReceiptRoot return l.InfoReceiptRoot
} }
func (l *MockL1Info) ID() eth.BlockID { func (l *MockBlockInfo) ID() eth.BlockID {
return eth.BlockID{Hash: l.InfoHash, Number: l.InfoNum} return eth.BlockID{Hash: l.InfoHash, Number: l.InfoNum}
} }
func (l *MockL1Info) BlockRef() eth.L1BlockRef { func (l *MockBlockInfo) BlockRef() eth.L1BlockRef {
return eth.L1BlockRef{ return eth.L1BlockRef{
Hash: l.InfoHash, Hash: l.InfoHash,
Number: l.InfoNum, Number: l.InfoNum,
...@@ -68,26 +72,21 @@ func (l *MockL1Info) BlockRef() eth.L1BlockRef { ...@@ -68,26 +72,21 @@ func (l *MockL1Info) BlockRef() eth.L1BlockRef {
} }
} }
func (l *MockL1Info) SequenceNumber() uint64 { func RandomBlockInfo(rng *rand.Rand) *MockBlockInfo {
return l.InfoSequenceNumber return &MockBlockInfo{
} InfoParentHash: RandomHash(rng),
InfoNum: rng.Uint64(),
func RandomL1Info(rng *rand.Rand) *MockL1Info { InfoTime: rng.Uint64(),
return &MockL1Info{ InfoHash: RandomHash(rng),
InfoParentHash: RandomHash(rng), InfoBaseFee: big.NewInt(rng.Int63n(1000_000 * 1e9)), // a million GWEI
InfoNum: rng.Uint64(), InfoReceiptRoot: types.EmptyRootHash,
InfoTime: rng.Uint64(), InfoRoot: RandomHash(rng),
InfoHash: RandomHash(rng),
InfoBaseFee: big.NewInt(rng.Int63n(1000_000 * 1e9)), // a million GWEI
InfoReceiptRoot: types.EmptyRootHash,
InfoRoot: RandomHash(rng),
InfoSequenceNumber: rng.Uint64(),
} }
} }
func MakeL1Info(fn func(l *MockL1Info)) func(rng *rand.Rand) *MockL1Info { func MakeBlockInfo(fn func(l *MockBlockInfo)) func(rng *rand.Rand) *MockBlockInfo {
return func(rng *rand.Rand) *MockL1Info { return func(rng *rand.Rand) *MockBlockInfo {
l := RandomL1Info(rng) l := RandomBlockInfo(rng)
if fn != nil { if fn != nil {
fn(l) 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 ( ...@@ -5,35 +5,28 @@ import (
"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/stretchr/testify/mock"
) )
type MockL1Source struct { type MockL1Source struct {
mock.Mock MockEthClient
} }
func (m *MockL1Source) InfoByHash(ctx context.Context, hash common.Hash) (eth.L1Info, error) { func (m *MockL1Source) L1BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L1BlockRef, error) {
out := m.Mock.MethodCalled("InfoByHash", hash) out := m.Mock.MethodCalled("L1BlockRefByLabel")
return *out[0].(*eth.L1Info), *out[1].(*error) return out[0].(eth.L1BlockRef), *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) L1HeadBlockRef(ctx context.Context) (eth.L1BlockRef, error) { func (m *MockL1Source) ExpectL1BlockRefByLabel(label eth.BlockLabel, ref eth.L1BlockRef, err error) {
out := m.Mock.MethodCalled("L1HeadBlockRef") m.Mock.On("L1BlockRefByLabel", label).Once().Return(ref, &err)
return out[0].(eth.L1BlockRef), *out[1].(*error)
} }
func (m *MockL1Source) L1BlockRefByNumber(ctx context.Context, u uint64) (eth.L1BlockRef, error) { func (m *MockL1Source) L1BlockRefByNumber(ctx context.Context, num uint64) (eth.L1BlockRef, error) {
out := m.Mock.MethodCalled("L1BlockRefByNumber", u) out := m.Mock.MethodCalled("L1BlockRefByNumber", num)
return out[0].(eth.L1BlockRef), *out[1].(*error) return out[0].(eth.L1BlockRef), *out[1].(*error)
} }
func (m *MockL1Source) ExpectL1BlockRefByNumber(u uint64, ref eth.L1BlockRef, err error) { func (m *MockL1Source) ExpectL1BlockRefByNumber(num uint64, ref eth.L1BlockRef, err error) {
m.Mock.On("L1BlockRefByNumber", u).Once().Return(ref, &err) m.Mock.On("L1BlockRefByNumber", num).Once().Return(ref, &err)
} }
func (m *MockL1Source) L1BlockRefByHash(ctx context.Context, hash common.Hash) (eth.L1BlockRef, error) { 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) ( ...@@ -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) { func (m *MockL1Source) ExpectL1BlockRefByHash(hash common.Hash, ref eth.L1BlockRef, err error) {
m.Mock.On("L1BlockRefByHash", hash).Once().Return(ref, &err) 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