diff --git a/op-node/l2/util.go b/op-node/eth/account_proof.go similarity index 55% rename from op-node/l2/util.go rename to op-node/eth/account_proof.go index 34186adc07463..d0703b075ede5 100644 --- a/op-node/l2/util.go +++ b/op-node/eth/account_proof.go @@ -1,36 +1,18 @@ -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 -} diff --git a/op-node/eth/l1info.go b/op-node/eth/block_info.go similarity index 59% rename from op-node/eth/l1info.go rename to op-node/eth/block_info.go index 985f6b69ab70b..c6baf502dde42 100644 --- a/op-node/eth/l1info.go +++ b/op-node/eth/block_info.go @@ -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(), + } +} diff --git a/op-node/eth/label.go b/op-node/eth/label.go new file mode 100644 index 0000000000000..f79629e607ef4 --- /dev/null +++ b/op-node/eth/label.go @@ -0,0 +1,19 @@ +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" +) diff --git a/op-node/l1/source_test.go b/op-node/l1/source_test.go deleted file mode 100644 index ab369d7d17441..0000000000000 --- a/op-node/l1/source_test.go +++ /dev/null @@ -1,196 +0,0 @@ -package l1 - -import ( - "context" - "math/big" - "math/rand" - "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" -) - -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] -} - -func (m *mockRPC) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { - return m.MethodCalled("CallContext", ctx, result, method, args).Get(0).([]error)[0] -} - -func (m *mockRPC) EthSubscribe(ctx context.Context, channel interface{}, args ...interface{}) (*rpc.ClientSubscription, error) { - called := m.MethodCalled("EthSubscribe", channel, args) - return called.Get(0).(*rpc.ClientSubscription), called.Get(1).([]error)[0] -} - -func (m *mockRPC) Close() { - m.MethodCalled("Close") -} - -var _ client.RPC = (*mockRPC)(nil) - -func randHash() (out common.Hash) { - rand.Read(out[:]) - return out -} - -func randHeader() *types.Header { - return &types.Header{ - ParentHash: randHash(), - UncleHash: randHash(), - Coinbase: common.Address{}, - Root: randHash(), - TxHash: randHash(), - ReceiptHash: randHash(), - Number: big.NewInt(1234), - Time: 123456, - MixDigest: randHash(), - 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, - } - 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) { - *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) - 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) - m.Mock.AssertExpectations(t) -} - -func TestSource_InfoByNumber(t *testing.T) { - m := new(mockRPC) - hdr := randHeader() - rhdr := &rpcHeader{ - cache: rpcHeaderCacheInfo{Hash: hdr.Hash()}, - header: *hdr, - } - expectedInfo, _ := rhdr.Info(true) - n := hdr.Number.Uint64() - ctx := context.Background() - m.On("CallContext", ctx, new(*rpcHeader), "eth_getBlockByNumber", []interface{}{hexutil.EncodeUint64(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) - m.Mock.AssertExpectations(t) -} diff --git a/op-node/l2/source.go b/op-node/l2/source.go index be11db35a767a..b78262f3e0325 100644 --- a/op-node/l2/source.go +++ b/op-node/l2/source.go @@ -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 diff --git a/op-node/node/api.go b/op-node/node/api.go index fa880d7b2ffd8..25dcc5e08e648 100644 --- a/op-node/node/api.go +++ b/op-node/node/api.go @@ -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 } diff --git a/op-node/node/node.go b/op-node/node/node.go index edb84badcb460..afbf2c634fd1c 100644 --- a/op-node/node/node.go +++ b/op-node/node/node.go @@ -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) } diff --git a/op-node/node/server_test.go b/op-node/node/server_test.go index 4238ffe9c780c..38cd0c84d7bb2 100644 --- a/op-node/node/server_test.go +++ b/op-node/node/server_test.go @@ -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 } diff --git a/op-node/rollup/derive/attributes.go b/op-node/rollup/derive/attributes.go index 6aada3e8342b2..0a5b8504544a3 100644 --- a/op-node/rollup/derive/attributes.go +++ b/op-node/rollup/derive/attributes.go @@ -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 diff --git a/op-node/rollup/derive/attributes_queue_test.go b/op-node/rollup/derive/attributes_queue_test.go index ca3ab8572d56b..479358ab9223f 100644 --- a/op-node/rollup/derive/attributes_queue_test.go +++ b/op-node/rollup/derive/attributes_queue_test.go @@ -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) diff --git a/op-node/rollup/derive/attributes_test.go b/op-node/rollup/derive/attributes_test.go index 2854cea7d0c2e..6baca9f5d1201 100644 --- a/op-node/rollup/derive/attributes_test.go +++ b/op-node/rollup/derive/attributes_test.go @@ -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 diff --git a/op-node/rollup/derive/calldata_source.go b/op-node/rollup/derive/calldata_source.go index af7cd421f05ef..00a9f89dca36d 100644 --- a/op-node/rollup/derive/calldata_source.go +++ b/op-node/rollup/derive/calldata_source.go @@ -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 diff --git a/op-node/rollup/derive/calldata_source_test.go b/op-node/rollup/derive/calldata_source_test.go index 93aaf633ed620..f4cac29934003 100644 --- a/op-node/rollup/derive/calldata_source_test.go +++ b/op-node/rollup/derive/calldata_source_test.go @@ -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) diff --git a/op-node/rollup/derive/l1_block_info.go b/op-node/rollup/derive/l1_block_info.go index 7256c940176e5..38afe3333a9c1 100644 --- a/op-node/rollup/derive/l1_block_info.go +++ b/op-node/rollup/derive/l1_block_info.go @@ -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) diff --git a/op-node/rollup/derive/l1_block_info_test.go b/op-node/rollup/derive/l1_block_info_test.go index bb6f38b25c67c..9466c41c9ce73 100644 --- a/op-node/rollup/derive/l1_block_info_test.go +++ b/op-node/rollup/derive/l1_block_info_test.go @@ -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) { diff --git a/op-node/rollup/derive/pipeline.go b/op-node/rollup/derive/pipeline.go index 9acdd7e994d88..2fc5006fc69cf 100644 --- a/op-node/rollup/derive/pipeline.go +++ b/op-node/rollup/derive/pipeline.go @@ -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 diff --git a/op-node/rollup/driver/driver.go b/op-node/rollup/driver/driver.go index b667c091bc195..6a82d036fb23a 100644 --- a/op-node/rollup/driver/driver.go +++ b/op-node/rollup/driver/driver.go @@ -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, diff --git a/op-node/rollup/driver/state.go b/op-node/rollup/driver/state.go index 10a5dd329fbea..2558c9cd74b2e 100644 --- a/op-node/rollup/driver/state.go +++ b/op-node/rollup/driver/state.go @@ -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 } diff --git a/op-node/rollup/output_root.go b/op-node/rollup/output_root.go new file mode 100644 index 0000000000000..822b5abcbf6be --- /dev/null +++ b/op-node/rollup/output_root.go @@ -0,0 +1,18 @@ +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())) +} diff --git a/op-node/rollup/sync/start.go b/op-node/rollup/sync/start.go index b687db2456120..155442945d3e1 100644 --- a/op-node/rollup/sync/start.go +++ b/op-node/rollup/sync/start.go @@ -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 diff --git a/op-node/l1/batching.go b/op-node/sources/batching.go similarity index 98% rename from op-node/l1/batching.go rename to op-node/sources/batching.go index 4262982f143e3..ebaa9e5fd5088 100644 --- a/op-node/l1/batching.go +++ b/op-node/sources/batching.go @@ -1,4 +1,4 @@ -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 { diff --git a/op-node/l1/batching_test.go b/op-node/sources/batching_test.go similarity index 99% rename from op-node/l1/batching_test.go rename to op-node/sources/batching_test.go index 63092a41b5cad..1e75f69d74f31 100644 --- a/op-node/l1/batching_test.go +++ b/op-node/sources/batching_test.go @@ -1,4 +1,4 @@ -package l1 +package sources import ( "context" diff --git a/op-node/l1/source.go b/op-node/sources/eth_client.go similarity index 52% rename from op-node/l1/source.go rename to op-node/sources/eth_client.go index 4c2e2e1accbe6..7153fa219bf63 100644 --- a/op-node/l1/source.go +++ b/op-node/sources/eth_client.go @@ -1,4 +1,4 @@ -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() } diff --git a/op-node/sources/eth_client_test.go b/op-node/sources/eth_client_test.go new file mode 100644 index 0000000000000..3ab6814898e74 --- /dev/null +++ b/op-node/sources/eth_client_test.go @@ -0,0 +1,120 @@ +package sources + +import ( + "context" + "math/big" + "math/rand" + "testing" + + "github.com/ethereum-optimism/optimism/op-node/client" + "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/mock" + "github.com/stretchr/testify/require" +) + +type mockRPC struct { + mock.Mock +} + +func (m *mockRPC) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { + return m.MethodCalled("BatchCallContext", ctx, b).Get(0).([]error)[0] +} + +func (m *mockRPC) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { + return m.MethodCalled("CallContext", ctx, result, method, args).Get(0).([]error)[0] +} + +func (m *mockRPC) EthSubscribe(ctx context.Context, channel interface{}, args ...interface{}) (*rpc.ClientSubscription, error) { + called := m.MethodCalled("EthSubscribe", channel, args) + return called.Get(0).(*rpc.ClientSubscription), called.Get(1).([]error)[0] +} + +func (m *mockRPC) Close() { + m.MethodCalled("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, *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), + } + 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) + ctx := context.Background() + 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 := 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, rhdr.cache.Hash) + require.NoError(t, err) + require.Equal(t, info, expectedInfo) + m.Mock.AssertExpectations(t) +} + +func TestEthClient_InfoByNumber(t *testing.T) { + m := new(mockRPC) + _, rhdr := randHeader() + expectedInfo, _ := rhdr.Info(true) + n := rhdr.header.Number + ctx := context.Background() + 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 := 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) +} diff --git a/op-node/sources/l1_client.go b/op-node/sources/l1_client.go new file mode 100644 index 0000000000000..67bfaa6a22d70 --- /dev/null +++ b/op-node/sources/l1_client.go @@ -0,0 +1,96 @@ +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 +} diff --git a/op-node/l1/request_sema.go b/op-node/sources/limit.go similarity index 98% rename from op-node/l1/request_sema.go rename to op-node/sources/limit.go index 2033162e046f3..4e5affda531c3 100644 --- a/op-node/l1/request_sema.go +++ b/op-node/sources/limit.go @@ -1,4 +1,4 @@ -package l1 +package sources import ( "context" diff --git a/op-node/l1/receipts.go b/op-node/sources/receipts.go similarity index 97% rename from op-node/l1/receipts.go rename to op-node/sources/receipts.go index b1d2ab66e9c15..38d7c60f707bf 100644 --- a/op-node/l1/receipts.go +++ b/op-node/sources/receipts.go @@ -1,4 +1,4 @@ -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, diff --git a/op-node/l1/types.go b/op-node/sources/types.go similarity index 88% rename from op-node/l1/types.go rename to op-node/sources/types.go index af0b34f62cec4..61d88f3c00e08 100644 --- a/op-node/l1/types.go +++ b/op-node/sources/types.go @@ -1,6 +1,7 @@ -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 } diff --git a/op-node/testutils/fake_chain.go b/op-node/testutils/fake_chain.go index e46a49c63f063..b62dee0d94855 100644 --- a/op-node/testutils/fake_chain.go +++ b/op-node/testutils/fake_chain.go @@ -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 { diff --git a/op-node/testutils/l1info.go b/op-node/testutils/l1info.go index e14d9d4ad4026..51a46dc0e630c 100644 --- a/op-node/testutils/l1info.go +++ b/op-node/testutils/l1info.go @@ -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) } diff --git a/op-node/testutils/mock_eth_client.go b/op-node/testutils/mock_eth_client.go new file mode 100644 index 0000000000000..9b012b15b1450 --- /dev/null +++ b/op-node/testutils/mock_eth_client.go @@ -0,0 +1,95 @@ +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) +} diff --git a/op-node/testutils/mock_l1.go b/op-node/testutils/mock_l1.go index ca7529acef857..81f2719d57580 100644 --- a/op-node/testutils/mock_l1.go +++ b/op-node/testutils/mock_l1.go @@ -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) -}