From f70aaa8399ccee429804eecf3fc4c6fd8d9e6cab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Irmak?= Date: Tue, 1 Jul 2025 10:18:49 +0300 Subject: [PATCH] ethapi: reduce some of the wasted effort in GetTransactionReceipt (#32021) Towards https://github.com/ethereum/go-ethereum/issues/26974 --------- Co-authored-by: Gary Rong --- core/blockchain.go | 2 +- core/blockchain_reader.go | 49 ++++++++- core/blockchain_test.go | 120 +++++++++++++++++++++-- core/rawdb/accessors_chain.go | 42 ++++++-- core/rawdb/accessors_chain_test.go | 6 ++ core/rawdb/accessors_indexes.go | 107 ++++++++++++++++++-- core/rawdb/accessors_indexes_test.go | 84 +++++++++++++++- core/rawdb/chain_iterator.go | 2 +- core/types/receipt.go | 118 +++++++++++++--------- eth/api_backend.go | 14 ++- eth/tracers/api.go | 4 +- eth/tracers/api_test.go | 4 +- graphql/graphql.go | 2 +- internal/ethapi/api.go | 22 ++--- internal/ethapi/api_test.go | 9 +- internal/ethapi/backend.go | 3 +- internal/ethapi/transaction_args_test.go | 5 +- 17 files changed, 487 insertions(+), 106 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index d8b101f616..2290b6d3cd 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -314,7 +314,7 @@ type BlockChain struct { bodyCache *lru.Cache[common.Hash, *types.Body] bodyRLPCache *lru.Cache[common.Hash, rlp.RawValue] - receiptsCache *lru.Cache[common.Hash, []*types.Receipt] + receiptsCache *lru.Cache[common.Hash, []*types.Receipt] // Receipts cache with all fields derived blockCache *lru.Cache[common.Hash, *types.Block] txLookupLock sync.RWMutex diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index 0734baab35..7626e9e30d 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -18,9 +18,12 @@ package core import ( "errors" + "fmt" + "math/big" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state/snapshot" @@ -213,6 +216,44 @@ func (bc *BlockChain) GetBlocksFromHash(hash common.Hash, n int) (blocks []*type return } +// GetCanonicalReceipt allows fetching a receipt for a transaction that was +// already looked up on the index. Notably, only receipt in canonical chain +// is visible. +func (bc *BlockChain) GetCanonicalReceipt(tx *types.Transaction, blockHash common.Hash, blockNumber, txIndex uint64) (*types.Receipt, error) { + // The receipt retrieved from the cache contains all previously derived fields + if receipts, ok := bc.receiptsCache.Get(blockHash); ok { + if int(txIndex) >= len(receipts) { + return nil, fmt.Errorf("receipt out of index, length: %d, index: %d", len(receipts), txIndex) + } + return receipts[int(txIndex)], nil + } + header := bc.GetHeader(blockHash, blockNumber) + if header == nil { + return nil, fmt.Errorf("block header is not found, %d, %x", blockNumber, blockHash) + } + var blobGasPrice *big.Int + if header.ExcessBlobGas != nil { + blobGasPrice = eip4844.CalcBlobFee(bc.chainConfig, header) + } + receipt, ctx, err := rawdb.ReadCanonicalRawReceipt(bc.db, blockHash, blockNumber, txIndex) + if err != nil { + return nil, err + } + signer := types.MakeSigner(bc.chainConfig, new(big.Int).SetUint64(blockNumber), header.Time) + receipt.DeriveFields(signer, types.DeriveReceiptContext{ + BlockHash: blockHash, + BlockNumber: blockNumber, + BlockTime: header.Time, + BaseFee: header.BaseFee, + BlobGasPrice: blobGasPrice, + GasUsed: ctx.GasUsed, + LogIndex: ctx.LogIndex, + Tx: tx, + TxIndex: uint(txIndex), + }) + return receipt, nil +} + // GetReceiptsByHash retrieves the receipts for all transactions in a given block. func (bc *BlockChain) GetReceiptsByHash(hash common.Hash) types.Receipts { if receipts, ok := bc.receiptsCache.Get(hash); ok { @@ -277,13 +318,15 @@ func (bc *BlockChain) GetAncestor(hash common.Hash, number, ancestor uint64, max return bc.hc.GetAncestor(hash, number, ancestor, maxNonCanonical) } -// GetTransactionLookup retrieves the lookup along with the transaction +// GetCanonicalTransaction retrieves the lookup along with the transaction // itself associate with the given transaction hash. // // A null will be returned if the transaction is not found. This can be due to // the transaction indexer not being finished. The caller must explicitly check // the indexer progress. -func (bc *BlockChain) GetTransactionLookup(hash common.Hash) (*rawdb.LegacyTxLookupEntry, *types.Transaction) { +// +// Notably, only the transaction in the canonical chain is visible. +func (bc *BlockChain) GetCanonicalTransaction(hash common.Hash) (*rawdb.LegacyTxLookupEntry, *types.Transaction) { bc.txLookupLock.RLock() defer bc.txLookupLock.RUnlock() @@ -291,7 +334,7 @@ func (bc *BlockChain) GetTransactionLookup(hash common.Hash) (*rawdb.LegacyTxLoo if item, exist := bc.txLookupCache.Get(hash); exist { return item.lookup, item.transaction } - tx, blockHash, blockNumber, txIndex := rawdb.ReadTransaction(bc.db, hash) + tx, blockHash, blockNumber, txIndex := rawdb.ReadCanonicalTransaction(bc.db, hash) if tx == nil { return nil, nil } diff --git a/core/blockchain_test.go b/core/blockchain_test.go index f47e922e18..5e768fccdf 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -25,10 +25,12 @@ import ( "math/rand" "os" "path" + "reflect" "sync" "testing" "time" + "github.com/davecgh/go-spew/spew" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/beacon" @@ -46,6 +48,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" "github.com/holiman/uint256" + "github.com/stretchr/testify/assert" ) // So we can deterministically seed different blockchains @@ -1004,29 +1007,47 @@ func testChainTxReorgs(t *testing.T, scheme string) { // removed tx for i, tx := range (types.Transactions{pastDrop, freshDrop}) { - if txn, _, _, _ := rawdb.ReadTransaction(db, tx.Hash()); txn != nil { + if txn, _, _, _ := rawdb.ReadCanonicalTransaction(db, tx.Hash()); txn != nil { t.Errorf("drop %d: tx %v found while shouldn't have been", i, txn) } - if rcpt, _, _, _ := rawdb.ReadReceipt(db, tx.Hash(), blockchain.Config()); rcpt != nil { + if rcpt, _, _, _ := rawdb.ReadCanonicalReceipt(db, tx.Hash(), blockchain.Config()); rcpt != nil { t.Errorf("drop %d: receipt %v found while shouldn't have been", i, rcpt) } } // added tx for i, tx := range (types.Transactions{pastAdd, freshAdd, futureAdd}) { - if txn, _, _, _ := rawdb.ReadTransaction(db, tx.Hash()); txn == nil { + if txn, _, _, _ := rawdb.ReadCanonicalTransaction(db, tx.Hash()); txn == nil { t.Errorf("add %d: expected tx to be found", i) } - if rcpt, _, _, _ := rawdb.ReadReceipt(db, tx.Hash(), blockchain.Config()); rcpt == nil { + if rcpt, _, _, index := rawdb.ReadCanonicalReceipt(db, tx.Hash(), blockchain.Config()); rcpt == nil { t.Errorf("add %d: expected receipt to be found", i) + } else if rawRcpt, ctx, _ := rawdb.ReadCanonicalRawReceipt(db, rcpt.BlockHash, rcpt.BlockNumber.Uint64(), index); rawRcpt == nil { + t.Errorf("add %d: expected raw receipt to be found", i) + } else { + if rcpt.GasUsed != ctx.GasUsed { + t.Errorf("add %d, raw gasUsedSoFar doesn't make sense", i) + } + if len(rcpt.Logs) > 0 && rcpt.Logs[0].Index != ctx.LogIndex { + t.Errorf("add %d, raw startingLogIndex doesn't make sense", i) + } } } // shared tx for i, tx := range (types.Transactions{postponed, swapped}) { - if txn, _, _, _ := rawdb.ReadTransaction(db, tx.Hash()); txn == nil { + if txn, _, _, _ := rawdb.ReadCanonicalTransaction(db, tx.Hash()); txn == nil { t.Errorf("share %d: expected tx to be found", i) } - if rcpt, _, _, _ := rawdb.ReadReceipt(db, tx.Hash(), blockchain.Config()); rcpt == nil { + if rcpt, _, _, index := rawdb.ReadCanonicalReceipt(db, tx.Hash(), blockchain.Config()); rcpt == nil { t.Errorf("share %d: expected receipt to be found", i) + } else if rawRcpt, ctx, _ := rawdb.ReadCanonicalRawReceipt(db, rcpt.BlockHash, rcpt.BlockNumber.Uint64(), index); rawRcpt == nil { + t.Errorf("add %d: expected raw receipt to be found", i) + } else { + if rcpt.GasUsed != ctx.GasUsed { + t.Errorf("add %d, raw gasUsedSoFar doesn't make sense", i) + } + if len(rcpt.Logs) > 0 && rcpt.Logs[0].Index != ctx.LogIndex { + t.Errorf("add %d, raw startingLogIndex doesn't make sense", i) + } } } } @@ -4404,6 +4425,93 @@ func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64, if receipts == nil || len(receipts) != 1 { t.Fatalf("Missed block receipts: %d, cutoff: %d", num, cutoffBlock.NumberU64()) } + for indx, receipt := range receipts { + receiptByLookup, err := chain.GetCanonicalReceipt(body.Transactions[indx], receipt.BlockHash, + receipt.BlockNumber.Uint64(), uint64(indx)) + assert.NoError(t, err) + assert.Equal(t, receipt, receiptByLookup) + } + } + } +} + +func TestGetCanonicalReceipt(t *testing.T) { + const chainLength = 64 + + // Configure and generate a sample block chain + var ( + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + address = crypto.PubkeyToAddress(key.PublicKey) + funds = big.NewInt(1000000000000000000) + gspec = &Genesis{ + Config: params.MergedTestChainConfig, + Alloc: types.GenesisAlloc{address: {Balance: funds}}, + BaseFee: big.NewInt(params.InitialBaseFee), + } + signer = types.LatestSigner(gspec.Config) + engine = beacon.New(ethash.NewFaker()) + codeBin = common.FromHex("0x608060405234801561000f575f5ffd5b507f8ae1c8c6e5f91159d0bc1c4b9a47ce45301753843012cbe641e4456bfc73538b33426040516100419291906100ff565b60405180910390a1610139565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100778261004e565b9050919050565b6100878161006d565b82525050565b5f819050919050565b61009f8161008d565b82525050565b5f82825260208201905092915050565b7f436f6e7374727563746f72207761732063616c6c6564000000000000000000005f82015250565b5f6100e96016836100a5565b91506100f4826100b5565b602082019050919050565b5f6060820190506101125f83018561007e565b61011f6020830184610096565b8181036040830152610130816100dd565b90509392505050565b603e806101455f395ff3fe60806040525f5ffdfea2646970667358221220e8bc3c31e3ac337eab702e8fdfc1c71894f4df1af4221bcde4a2823360f403fb64736f6c634300081e0033") + ) + _, blocks, receipts := GenerateChainWithGenesis(gspec, engine, chainLength, func(i int, block *BlockGen) { + // SPDX-License-Identifier: MIT + // pragma solidity ^0.8.0; + // + // contract ConstructorLogger { + // event ConstructorLog(address sender, uint256 timestamp, string message); + // + // constructor() { + // emit ConstructorLog(msg.sender, block.timestamp, "Constructor was called"); + // } + // } + // + // 608060405234801561000f575f5ffd5b507f8ae1c8c6e5f91159d0bc1c4b9a47ce45301753843012cbe641e4456bfc73538b33426040516100419291906100ff565b60405180910390a1610139565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100778261004e565b9050919050565b6100878161006d565b82525050565b5f819050919050565b61009f8161008d565b82525050565b5f82825260208201905092915050565b7f436f6e7374727563746f72207761732063616c6c6564000000000000000000005f82015250565b5f6100e96016836100a5565b91506100f4826100b5565b602082019050919050565b5f6060820190506101125f83018561007e565b61011f6020830184610096565b8181036040830152610130816100dd565b90509392505050565b603e806101455f395ff3fe60806040525f5ffdfea2646970667358221220e8bc3c31e3ac337eab702e8fdfc1c71894f4df1af4221bcde4a2823360f403fb64736f6c634300081e0033 + nonce := block.TxNonce(address) + tx, err := types.SignTx(types.NewContractCreation(nonce, big.NewInt(0), 100_000, block.header.BaseFee, codeBin), signer, key) + if err != nil { + panic(err) + } + block.AddTx(tx) + + tx2, err := types.SignTx(types.NewContractCreation(nonce+1, big.NewInt(0), 100_000, block.header.BaseFee, codeBin), signer, key) + if err != nil { + panic(err) + } + block.AddTx(tx2) + + tx3, err := types.SignTx(types.NewContractCreation(nonce+2, big.NewInt(0), 100_000, block.header.BaseFee, codeBin), signer, key) + if err != nil { + panic(err) + } + block.AddTx(tx3) + }) + + db, _ := rawdb.Open(rawdb.NewMemoryDatabase(), rawdb.OpenOptions{}) + defer db.Close() + options := DefaultConfig().WithStateScheme(rawdb.PathScheme) + chain, _ := NewBlockChain(db, gspec, beacon.New(ethash.NewFaker()), options) + defer chain.Stop() + + chain.InsertReceiptChain(blocks, types.EncodeBlockReceiptLists(receipts), 0) + + for i := 0; i < chainLength; i++ { + block := blocks[i] + blockReceipts := chain.GetReceiptsByHash(block.Hash()) + chain.receiptsCache.Purge() // ugly hack + for txIndex, tx := range block.Body().Transactions { + receipt, err := chain.GetCanonicalReceipt(tx, block.Hash(), block.NumberU64(), uint64(txIndex)) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if !reflect.DeepEqual(receipts[i][txIndex], receipt) { + want := spew.Sdump(receipts[i][txIndex]) + got := spew.Sdump(receipt) + t.Fatalf("Receipt is not matched, want %s, got: %s", want, got) + } + if !reflect.DeepEqual(blockReceipts[txIndex], receipt) { + want := spew.Sdump(blockReceipts[txIndex]) + got := spew.Sdump(receipt) + t.Fatalf("Receipt is not matched, want %s, got: %s", want, got) + } } } } diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index 2386246caf..4426c6a9e7 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -449,9 +449,10 @@ func ReadBodyRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue return data } -// ReadCanonicalBodyRLP retrieves the block body (transactions and uncles) for the canonical -// block at number, in RLP encoding. -func ReadCanonicalBodyRLP(db ethdb.Reader, number uint64) rlp.RawValue { +// ReadCanonicalBodyRLP retrieves the block body (transactions and uncles) for the +// canonical block at number, in RLP encoding. Optionally it takes the block hash +// to avoid looking it up +func ReadCanonicalBodyRLP(db ethdb.Reader, number uint64, hash *common.Hash) rlp.RawValue { var data []byte db.ReadAncients(func(reader ethdb.AncientReaderOp) error { data, _ = reader.Ancient(ChainFreezerBodiesTable, number) @@ -459,10 +460,14 @@ func ReadCanonicalBodyRLP(db ethdb.Reader, number uint64) rlp.RawValue { return nil } // Block is not in ancients, read from leveldb by hash and number. - // Note: ReadCanonicalHash cannot be used here because it also - // calls ReadAncients internally. - hash, _ := db.Get(headerHashKey(number)) - data, _ = db.Get(blockBodyKey(number, common.BytesToHash(hash))) + if hash != nil { + data, _ = db.Get(blockBodyKey(number, *hash)) + } else { + // Note: ReadCanonicalHash cannot be used here because it also + // calls ReadAncients internally. + hashBytes, _ := db.Get(headerHashKey(number)) + data, _ = db.Get(blockBodyKey(number, common.BytesToHash(hashBytes))) + } return nil }) return data @@ -544,6 +549,29 @@ func ReadReceiptsRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawVa return data } +// ReadCanonicalReceiptsRLP retrieves the receipts RLP for the canonical block at +// number, in RLP encoding. Optionally it takes the block hash to avoid looking it up. +func ReadCanonicalReceiptsRLP(db ethdb.Reader, number uint64, hash *common.Hash) rlp.RawValue { + var data []byte + db.ReadAncients(func(reader ethdb.AncientReaderOp) error { + data, _ = reader.Ancient(ChainFreezerReceiptTable, number) + if len(data) > 0 { + return nil + } + // Block is not in ancients, read from leveldb by hash and number. + if hash != nil { + data, _ = db.Get(blockReceiptsKey(number, *hash)) + } else { + // Note: ReadCanonicalHash cannot be used here because it also + // calls ReadAncients internally. + hashBytes, _ := db.Get(headerHashKey(number)) + data, _ = db.Get(blockReceiptsKey(number, common.BytesToHash(hashBytes))) + } + return nil + }) + return data +} + // ReadRawReceipts retrieves all the transaction receipts belonging to a block. // The receipt metadata fields and the Bloom are not guaranteed to be populated, // so they should not be used. Use ReadReceipts instead if the metadata is needed. diff --git a/core/rawdb/accessors_chain_test.go b/core/rawdb/accessors_chain_test.go index 6d46239e27..196f3dac8f 100644 --- a/core/rawdb/accessors_chain_test.go +++ b/core/rawdb/accessors_chain_test.go @@ -441,6 +441,9 @@ func TestAncientStorage(t *testing.T) { if blob := ReadReceiptsRLP(db, hash, number); len(blob) > 0 { t.Fatalf("non existent receipts returned") } + if blob := ReadCanonicalReceiptsRLP(db, number, &hash); len(blob) > 0 { + t.Fatalf("non existent receipts returned") + } // Write and verify the header in the database WriteAncientBlocks(db, []*types.Block{block}, types.EncodeBlockReceiptLists([]types.Receipts{nil})) @@ -454,6 +457,9 @@ func TestAncientStorage(t *testing.T) { if blob := ReadReceiptsRLP(db, hash, number); len(blob) == 0 { t.Fatalf("no receipts returned") } + if blob := ReadCanonicalReceiptsRLP(db, number, &hash); len(blob) == 0 { + t.Fatalf("no receipts returned") + } // Use a fake hash for data retrieval, nothing should be returned. fakeHash := common.BytesToHash([]byte{0x01, 0x02, 0x03}) diff --git a/core/rawdb/accessors_indexes.go b/core/rawdb/accessors_indexes.go index b5491f76ef..f56ff066e9 100644 --- a/core/rawdb/accessors_indexes.go +++ b/core/rawdb/accessors_indexes.go @@ -20,6 +20,7 @@ import ( "bytes" "encoding/binary" "errors" + "fmt" "math/big" "github.com/ethereum/go-ethereum/common" @@ -169,9 +170,10 @@ func findTxInBlockBody(blockbody rlp.RawValue, target common.Hash) (*types.Trans return nil, 0, errors.New("transaction not found") } -// ReadTransaction retrieves a specific transaction from the database, along with -// its added positional metadata. -func ReadTransaction(db ethdb.Reader, hash common.Hash) (*types.Transaction, common.Hash, uint64, uint64) { +// ReadCanonicalTransaction retrieves a specific transaction from the database, along +// with its added positional metadata. Notably, only the transaction in the canonical +// chain is visible. +func ReadCanonicalTransaction(db ethdb.Reader, hash common.Hash) (*types.Transaction, common.Hash, uint64, uint64) { blockNumber := ReadTxLookupEntry(db, hash) if blockNumber == nil { return nil, common.Hash{}, 0, 0 @@ -180,7 +182,7 @@ func ReadTransaction(db ethdb.Reader, hash common.Hash) (*types.Transaction, com if blockHash == (common.Hash{}) { return nil, common.Hash{}, 0, 0 } - bodyRLP := ReadBodyRLP(db, blockHash, *blockNumber) + bodyRLP := ReadCanonicalBodyRLP(db, *blockNumber, &blockHash) if bodyRLP == nil { log.Error("Transaction referenced missing", "number", *blockNumber, "hash", blockHash) return nil, common.Hash{}, 0, 0 @@ -193,9 +195,10 @@ func ReadTransaction(db ethdb.Reader, hash common.Hash) (*types.Transaction, com return tx, blockHash, *blockNumber, txIndex } -// ReadReceipt retrieves a specific transaction receipt from the database, along with -// its added positional metadata. -func ReadReceipt(db ethdb.Reader, hash common.Hash, config *params.ChainConfig) (*types.Receipt, common.Hash, uint64, uint64) { +// ReadCanonicalReceipt retrieves a specific transaction receipt from the database, +// along with its added positional metadata. Notably, only the receipt in the canonical +// chain is visible. +func ReadCanonicalReceipt(db ethdb.Reader, hash common.Hash, config *params.ChainConfig) (*types.Receipt, common.Hash, uint64, uint64) { // Retrieve the context of the receipt based on the transaction hash blockNumber := ReadTxLookupEntry(db, hash) if blockNumber == nil { @@ -220,7 +223,91 @@ func ReadReceipt(db ethdb.Reader, hash common.Hash, config *params.ChainConfig) return nil, common.Hash{}, 0, 0 } -// ReadFilterMapRow retrieves a filter map row at the given mapRowIndex +// extractReceiptFields takes a raw RLP-encoded receipt blob and extracts +// specific fields from it. +func extractReceiptFields(receiptRLP rlp.RawValue) (uint64, uint, error) { + receiptList, _, err := rlp.SplitList(receiptRLP) + if err != nil { + return 0, 0, err + } + // Decode the field: receipt status + // for receipt before the byzantium fork: + // - bytes: post state root + // for receipt after the byzantium fork: + // - bytes: receipt status flag + _, _, rest, err := rlp.Split(receiptList) + if err != nil { + return 0, 0, err + } + // Decode the field: cumulative gas used (type: uint64) + gasUsed, rest, err := rlp.SplitUint64(rest) + if err != nil { + return 0, 0, err + } + // Decode the field: logs (type: rlp list) + logList, _, err := rlp.SplitList(rest) + if err != nil { + return 0, 0, err + } + logCount, err := rlp.CountValues(logList) + if err != nil { + return 0, 0, err + } + return gasUsed, uint(logCount), nil +} + +// RawReceiptContext carries the contextual information that is needed to derive +// a complete receipt from a raw one. +type RawReceiptContext struct { + GasUsed uint64 // Amount of gas used by the associated transaction + LogIndex uint // Starting index of the logs within the block +} + +// ReadCanonicalRawReceipt reads a raw receipt at the specified position. It also +// returns the gas used by the associated transaction and the starting index of +// the logs within the block. The main difference with ReadCanonicalReceipt is +// that the additional positional fields are not directly included in the receipt. +// Notably, only receipts from the canonical chain are visible. +func ReadCanonicalRawReceipt(db ethdb.Reader, blockHash common.Hash, blockNumber, txIndex uint64) (*types.Receipt, RawReceiptContext, error) { + receiptIt, err := rlp.NewListIterator(ReadCanonicalReceiptsRLP(db, blockNumber, &blockHash)) + if err != nil { + return nil, RawReceiptContext{}, err + } + var ( + cumulativeGasUsed uint64 + logIndex uint + ) + for i := uint64(0); i <= txIndex; i++ { + // Unexpected iteration error + if receiptIt.Err() != nil { + return nil, RawReceiptContext{}, receiptIt.Err() + } + // Unexpected end of iteration + if !receiptIt.Next() { + return nil, RawReceiptContext{}, fmt.Errorf("receipt not found, %d, %x, %d", blockNumber, blockHash, txIndex) + } + if i == txIndex { + var stored types.ReceiptForStorage + if err := rlp.DecodeBytes(receiptIt.Value(), &stored); err != nil { + return nil, RawReceiptContext{}, err + } + return (*types.Receipt)(&stored), RawReceiptContext{ + GasUsed: stored.CumulativeGasUsed - cumulativeGasUsed, + LogIndex: logIndex, + }, nil + } else { + gas, logs, err := extractReceiptFields(receiptIt.Value()) + if err != nil { + return nil, RawReceiptContext{}, err + } + cumulativeGasUsed = gas + logIndex += logs + } + } + return nil, RawReceiptContext{}, fmt.Errorf("receipt not found, %d, %x, %d", blockNumber, blockHash, txIndex) +} + +// ReadFilterMapExtRow retrieves a filter map row at the given mapRowIndex // (see filtermaps.mapRowIndex for the storage index encoding). // Note that zero length rows are not stored in the database and therefore all // non-existent entries are interpreted as empty rows and return no error. @@ -247,7 +334,7 @@ func ReadFilterMapExtRow(db ethdb.KeyValueReader, mapRowIndex uint64, bitLength return nil, err } if len(encRow)%byteLength != 0 { - return nil, errors.New("Invalid encoded extended filter row length") + return nil, errors.New("invalid encoded extended filter row length") } row := make([]uint32, len(encRow)/byteLength) var b [4]byte @@ -318,7 +405,7 @@ func ReadFilterMapBaseRows(db ethdb.KeyValueReader, mapRowIndex uint64, rowCount return rows, nil } -// WriteFilterMapRow stores a filter map row at the given mapRowIndex or deletes +// WriteFilterMapExtRow stores a filter map row at the given mapRowIndex or deletes // any existing entry if the row is empty. func WriteFilterMapExtRow(db ethdb.KeyValueWriter, mapRowIndex uint64, row []uint32, bitLength uint) { byteLength := int(bitLength) / 8 diff --git a/core/rawdb/accessors_indexes_test.go b/core/rawdb/accessors_indexes_test.go index b768deed9b..a812fefeaa 100644 --- a/core/rawdb/accessors_indexes_test.go +++ b/core/rawdb/accessors_indexes_test.go @@ -17,6 +17,7 @@ package rawdb import ( + "errors" "math/big" "testing" @@ -88,7 +89,7 @@ func TestLookupStorage(t *testing.T) { // Check that no transactions entries are in a pristine database for i, tx := range txs { - if txn, _, _, _ := ReadTransaction(db, tx.Hash()); txn != nil { + if txn, _, _, _ := ReadCanonicalTransaction(db, tx.Hash()); txn != nil { t.Fatalf("tx #%d [%x]: non existent transaction returned: %v", i, tx.Hash(), txn) } } @@ -98,7 +99,7 @@ func TestLookupStorage(t *testing.T) { tc.writeTxLookupEntriesByBlock(db, block) for i, tx := range txs { - if txn, hash, number, index := ReadTransaction(db, tx.Hash()); txn == nil { + if txn, hash, number, index := ReadCanonicalTransaction(db, tx.Hash()); txn == nil { t.Fatalf("tx #%d [%x]: transaction not found", i, tx.Hash()) } else { if hash != block.Hash() || number != block.NumberU64() || index != uint64(i) { @@ -112,7 +113,7 @@ func TestLookupStorage(t *testing.T) { // Delete the transactions and check purge for i, tx := range txs { DeleteTxLookupEntry(db, tx.Hash()) - if txn, _, _, _ := ReadTransaction(db, tx.Hash()); txn != nil { + if txn, _, _, _ := ReadCanonicalTransaction(db, tx.Hash()); txn != nil { t.Fatalf("tx #%d [%x]: deleted transaction returned: %v", i, tx.Hash(), txn) } } @@ -219,3 +220,80 @@ func TestFindTxInBlockBody(t *testing.T) { } } } + +func TestExtractReceiptFields(t *testing.T) { + receiptWithPostState := types.ReceiptForStorage(types.Receipt{ + Type: types.LegacyTxType, + PostState: []byte{0x1, 0x2, 0x3}, + CumulativeGasUsed: 100, + }) + receiptWithPostStateBlob, _ := rlp.EncodeToBytes(&receiptWithPostState) + + receiptNoLogs := types.ReceiptForStorage(types.Receipt{ + Type: types.LegacyTxType, + Status: types.ReceiptStatusSuccessful, + CumulativeGasUsed: 100, + }) + receiptNoLogBlob, _ := rlp.EncodeToBytes(&receiptNoLogs) + + receiptWithLogs := types.ReceiptForStorage(types.Receipt{ + Type: types.LegacyTxType, + Status: types.ReceiptStatusSuccessful, + CumulativeGasUsed: 100, + Logs: []*types.Log{ + { + Address: common.BytesToAddress([]byte{0x1}), + Topics: []common.Hash{ + common.BytesToHash([]byte{0x1}), + }, + Data: []byte{0x1}, + }, + { + Address: common.BytesToAddress([]byte{0x2}), + Topics: []common.Hash{ + common.BytesToHash([]byte{0x2}), + }, + Data: []byte{0x2}, + }, + }, + }) + receiptWithLogBlob, _ := rlp.EncodeToBytes(&receiptWithLogs) + + invalidReceipt := types.ReceiptForStorage(types.Receipt{ + Type: types.LegacyTxType, + Status: types.ReceiptStatusSuccessful, + CumulativeGasUsed: 100, + }) + invalidReceiptBlob, _ := rlp.EncodeToBytes(&invalidReceipt) + invalidReceiptBlob[len(invalidReceiptBlob)-1] = 0xf + + var cases = []struct { + logs rlp.RawValue + expErr error + expGasUsed uint64 + expLogs uint + }{ + {receiptWithPostStateBlob, nil, 100, 0}, + {receiptNoLogBlob, nil, 100, 0}, + {receiptWithLogBlob, nil, 100, 2}, + {invalidReceiptBlob, rlp.ErrExpectedList, 100, 0}, + } + for _, c := range cases { + gasUsed, logs, err := extractReceiptFields(c.logs) + if c.expErr != nil { + if !errors.Is(err, c.expErr) { + t.Fatalf("Unexpected error, want: %v, got: %v", c.expErr, err) + } + } else { + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if gasUsed != c.expGasUsed { + t.Fatalf("Unexpected gas used, want %d, got %d", c.expGasUsed, gasUsed) + } + if logs != c.expLogs { + t.Fatalf("Unexpected logs, want %d, got %d", c.expLogs, logs) + } + } + } +} diff --git a/core/rawdb/chain_iterator.go b/core/rawdb/chain_iterator.go index ecbc44e1f1..e7c89ca8d9 100644 --- a/core/rawdb/chain_iterator.go +++ b/core/rawdb/chain_iterator.go @@ -118,7 +118,7 @@ func iterateTransactions(db ethdb.Database, from uint64, to uint64, reverse bool } defer close(rlpCh) for n != end { - data := ReadCanonicalBodyRLP(db, n) + data := ReadCanonicalBodyRLP(db, n, nil) // Feed the block to the aggregator, or abort on interrupt select { case rlpCh <- &numberRlp{n, data}: diff --git a/core/types/receipt.go b/core/types/receipt.go index 3227d77986..5b6669f274 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -258,6 +258,62 @@ func (r *Receipt) Size() common.StorageSize { return size } +// DeriveReceiptContext holds the contextual information needed to derive a receipt +type DeriveReceiptContext struct { + BlockHash common.Hash + BlockNumber uint64 + BlockTime uint64 + BaseFee *big.Int + BlobGasPrice *big.Int + GasUsed uint64 + LogIndex uint // Number of logs in the block until this receipt + Tx *Transaction + TxIndex uint +} + +// DeriveFields fills the receipt with computed fields based on consensus +// data and contextual infos like containing block and transactions. +func (r *Receipt) DeriveFields(signer Signer, context DeriveReceiptContext) { + // The transaction type and hash can be retrieved from the transaction itself + r.Type = context.Tx.Type() + r.TxHash = context.Tx.Hash() + r.GasUsed = context.GasUsed + r.EffectiveGasPrice = context.Tx.inner.effectiveGasPrice(new(big.Int), context.BaseFee) + + // EIP-4844 blob transaction fields + if context.Tx.Type() == BlobTxType { + r.BlobGasUsed = context.Tx.BlobGas() + r.BlobGasPrice = context.BlobGasPrice + } + + // Block location fields + r.BlockHash = context.BlockHash + r.BlockNumber = new(big.Int).SetUint64(context.BlockNumber) + r.TransactionIndex = context.TxIndex + + // The contract address can be derived from the transaction itself + if context.Tx.To() == nil { + // Deriving the signer is expensive, only do if it's actually needed + from, _ := Sender(signer, context.Tx) + r.ContractAddress = crypto.CreateAddress(from, context.Tx.Nonce()) + } else { + r.ContractAddress = common.Address{} + } + // The derived log fields can simply be set from the block and transaction + logIndex := context.LogIndex + for j := 0; j < len(r.Logs); j++ { + r.Logs[j].BlockNumber = context.BlockNumber + r.Logs[j].BlockHash = context.BlockHash + r.Logs[j].BlockTimestamp = context.BlockTime + r.Logs[j].TxHash = r.TxHash + r.Logs[j].TxIndex = context.TxIndex + r.Logs[j].Index = logIndex + logIndex++ + } + // Also derive the Bloom if not derived yet + r.Bloom = CreateBloom(r) +} + // ReceiptForStorage is a wrapper around a Receipt with RLP serialization // that omits the Bloom field. The Bloom field is recomputed by DeriveFields. type ReceiptForStorage Receipt @@ -323,58 +379,30 @@ func (rs Receipts) EncodeIndex(i int, w *bytes.Buffer) { // DeriveFields fills the receipts with their computed fields based on consensus // data and contextual infos like containing block and transactions. -func (rs Receipts) DeriveFields(config *params.ChainConfig, hash common.Hash, number uint64, time uint64, baseFee *big.Int, blobGasPrice *big.Int, txs []*Transaction) error { - signer := MakeSigner(config, new(big.Int).SetUint64(number), time) +func (rs Receipts) DeriveFields(config *params.ChainConfig, blockHash common.Hash, blockNumber uint64, blockTime uint64, baseFee *big.Int, blobGasPrice *big.Int, txs []*Transaction) error { + signer := MakeSigner(config, new(big.Int).SetUint64(blockNumber), blockTime) logIndex := uint(0) if len(txs) != len(rs) { return errors.New("transaction and receipt count mismatch") } for i := 0; i < len(rs); i++ { - // The transaction type and hash can be retrieved from the transaction itself - rs[i].Type = txs[i].Type() - rs[i].TxHash = txs[i].Hash() - rs[i].EffectiveGasPrice = txs[i].inner.effectiveGasPrice(new(big.Int), baseFee) - - // EIP-4844 blob transaction fields - if txs[i].Type() == BlobTxType { - rs[i].BlobGasUsed = txs[i].BlobGas() - rs[i].BlobGasPrice = blobGasPrice + var cumulativeGasUsed uint64 + if i > 0 { + cumulativeGasUsed = rs[i-1].CumulativeGasUsed } - - // block location fields - rs[i].BlockHash = hash - rs[i].BlockNumber = new(big.Int).SetUint64(number) - rs[i].TransactionIndex = uint(i) - - // The contract address can be derived from the transaction itself - if txs[i].To() == nil { - // Deriving the signer is expensive, only do if it's actually needed - from, _ := Sender(signer, txs[i]) - rs[i].ContractAddress = crypto.CreateAddress(from, txs[i].Nonce()) - } else { - rs[i].ContractAddress = common.Address{} - } - - // The used gas can be calculated based on previous r - if i == 0 { - rs[i].GasUsed = rs[i].CumulativeGasUsed - } else { - rs[i].GasUsed = rs[i].CumulativeGasUsed - rs[i-1].CumulativeGasUsed - } - - // The derived log fields can simply be set from the block and transaction - for j := 0; j < len(rs[i].Logs); j++ { - rs[i].Logs[j].BlockNumber = number - rs[i].Logs[j].BlockHash = hash - rs[i].Logs[j].BlockTimestamp = time - rs[i].Logs[j].TxHash = rs[i].TxHash - rs[i].Logs[j].TxIndex = uint(i) - rs[i].Logs[j].Index = logIndex - logIndex++ - } - // also derive the Bloom if not derived yet - rs[i].Bloom = CreateBloom(rs[i]) + rs[i].DeriveFields(signer, DeriveReceiptContext{ + BlockHash: blockHash, + BlockNumber: blockNumber, + BlockTime: blockTime, + BaseFee: baseFee, + BlobGasPrice: blobGasPrice, + GasUsed: rs[i].CumulativeGasUsed - cumulativeGasUsed, + LogIndex: logIndex, + Tx: txs[i], + TxIndex: uint(i), + }) + logIndex += uint(len(rs[i].Logs)) } return nil } diff --git a/eth/api_backend.go b/eth/api_backend.go index bd90f611f1..3ae73e78af 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -282,6 +282,10 @@ func (b *EthAPIBackend) GetReceipts(ctx context.Context, hash common.Hash) (type return b.eth.blockchain.GetReceiptsByHash(hash), nil } +func (b *EthAPIBackend) GetCanonicalReceipt(tx *types.Transaction, blockHash common.Hash, blockNumber, blockIndex uint64) (*types.Receipt, error) { + return b.eth.blockchain.GetCanonicalReceipt(tx, blockHash, blockNumber, blockIndex) +} + func (b *EthAPIBackend) GetLogs(ctx context.Context, hash common.Hash, number uint64) ([][]*types.Log, error) { return rawdb.ReadLogs(b.eth.chainDb, hash, number), nil } @@ -354,14 +358,16 @@ func (b *EthAPIBackend) GetPoolTransaction(hash common.Hash) *types.Transaction return b.eth.txPool.Get(hash) } -// GetTransaction retrieves the lookup along with the transaction itself associate -// with the given transaction hash. +// GetCanonicalTransaction retrieves the lookup along with the transaction itself +// associate with the given transaction hash. // // A null will be returned if the transaction is not found. The transaction is not // existent from the node's perspective. This can be due to the transaction indexer // not being finished. The caller must explicitly check the indexer progress. -func (b *EthAPIBackend) GetTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) { - lookup, tx := b.eth.blockchain.GetTransactionLookup(txHash) +// +// Notably, only the transaction in the canonical chain is visible. +func (b *EthAPIBackend) GetCanonicalTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) { + lookup, tx := b.eth.blockchain.GetCanonicalTransaction(txHash) if lookup == nil || tx == nil { return false, nil, common.Hash{}, 0, 0 } diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 6b01976084..6962023343 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -82,7 +82,7 @@ type Backend interface { HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) - GetTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) + GetCanonicalTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) TxIndexDone() bool RPCGasCap() uint64 ChainConfig() *params.ChainConfig @@ -863,7 +863,7 @@ func containsTx(block *types.Block, hash common.Hash) bool { // TraceTransaction returns the structured logs created during the execution of EVM // and returns them as a JSON object. func (api *API) TraceTransaction(ctx context.Context, hash common.Hash, config *TraceConfig) (interface{}, error) { - found, _, blockHash, blockNumber, index := api.backend.GetTransaction(hash) + found, _, blockHash, blockNumber, index := api.backend.GetCanonicalTransaction(hash) if !found { // Warn in case tx indexer is not done. if !api.backend.TxIndexDone() { diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go index 1826c78582..076e6fd8d4 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -117,8 +117,8 @@ func (b *testBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumber) return b.chain.GetBlockByNumber(uint64(number)), nil } -func (b *testBackend) GetTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) { - tx, hash, blockNumber, index := rawdb.ReadTransaction(b.chaindb, txHash) +func (b *testBackend) GetCanonicalTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) { + tx, hash, blockNumber, index := rawdb.ReadCanonicalTransaction(b.chaindb, txHash) return tx != nil, tx, hash, blockNumber, index } diff --git a/graphql/graphql.go b/graphql/graphql.go index 6738cd913b..0b2a77a3c4 100644 --- a/graphql/graphql.go +++ b/graphql/graphql.go @@ -229,7 +229,7 @@ func (t *Transaction) resolve(ctx context.Context) (*types.Transaction, *Block) return t.tx, t.block } // Try to return an already finalized transaction - found, tx, blockHash, _, index := t.r.backend.GetTransaction(t.hash) + found, tx, blockHash, _, index := t.r.backend.GetCanonicalTransaction(t.hash) if found { t.tx = tx blockNrOrHash := rpc.BlockNumberOrHashWithHash(blockHash, false) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 9151249308..21c35b7665 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1334,7 +1334,7 @@ func (api *TransactionAPI) GetTransactionCount(ctx context.Context, address comm // GetTransactionByHash returns the transaction for the given hash func (api *TransactionAPI) GetTransactionByHash(ctx context.Context, hash common.Hash) (*RPCTransaction, error) { // Try to return an already finalized transaction - found, tx, blockHash, blockNumber, index := api.b.GetTransaction(hash) + found, tx, blockHash, blockNumber, index := api.b.GetCanonicalTransaction(hash) if !found { // No finalized transaction, try to retrieve it from the pool if tx := api.b.GetPoolTransaction(hash); tx != nil { @@ -1357,7 +1357,7 @@ func (api *TransactionAPI) GetTransactionByHash(ctx context.Context, hash common // GetRawTransactionByHash returns the bytes of the transaction for the given hash. func (api *TransactionAPI) GetRawTransactionByHash(ctx context.Context, hash common.Hash) (hexutil.Bytes, error) { // Retrieve a finalized transaction, or a pooled otherwise - found, tx, _, _, _ := api.b.GetTransaction(hash) + found, tx, _, _, _ := api.b.GetCanonicalTransaction(hash) if !found { if tx = api.b.GetPoolTransaction(hash); tx != nil { return tx.MarshalBinary() @@ -1374,7 +1374,7 @@ func (api *TransactionAPI) GetRawTransactionByHash(ctx context.Context, hash com // GetTransactionReceipt returns the transaction receipt for the given transaction hash. func (api *TransactionAPI) GetTransactionReceipt(ctx context.Context, hash common.Hash) (map[string]interface{}, error) { - found, tx, blockHash, blockNumber, index := api.b.GetTransaction(hash) + found, tx, blockHash, blockNumber, index := api.b.GetCanonicalTransaction(hash) if !found { // Make sure indexer is done. if !api.b.TxIndexDone() { @@ -1383,22 +1383,12 @@ func (api *TransactionAPI) GetTransactionReceipt(ctx context.Context, hash commo // No such tx. return nil, nil } - header, err := api.b.HeaderByHash(ctx, blockHash) + receipt, err := api.b.GetCanonicalReceipt(tx, blockHash, blockNumber, index) if err != nil { return nil, err } - receipts, err := api.b.GetReceipts(ctx, blockHash) - if err != nil { - return nil, err - } - if uint64(len(receipts)) <= index { - return nil, nil - } - receipt := receipts[index] - // Derive the sender. - signer := types.MakeSigner(api.b.ChainConfig(), header.Number, header.Time) - return marshalReceipt(receipt, blockHash, blockNumber, signer, tx, int(index)), nil + return marshalReceipt(receipt, blockHash, blockNumber, api.signer, tx, int(index)), nil } // marshalReceipt marshals a transaction receipt into a JSON object. @@ -1781,7 +1771,7 @@ func (api *DebugAPI) GetRawReceipts(ctx context.Context, blockNrOrHash rpc.Block // GetRawTransaction returns the bytes of the transaction for the given hash. func (api *DebugAPI) GetRawTransaction(ctx context.Context, hash common.Hash) (hexutil.Bytes, error) { // Retrieve a finalized transaction, or a pooled otherwise - found, tx, _, _, _ := api.b.GetTransaction(hash) + found, tx, _, _, _ := api.b.GetCanonicalTransaction(hash) if !found { if tx = api.b.GetPoolTransaction(hash); tx != nil { return tx.MarshalBinary() diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 4d1834d757..1c9be836fc 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -586,9 +586,12 @@ func (b testBackend) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) even func (b testBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error { panic("implement me") } -func (b testBackend) GetTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) { - tx, blockHash, blockNumber, index := rawdb.ReadTransaction(b.db, txHash) - return true, tx, blockHash, blockNumber, index +func (b testBackend) GetCanonicalTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) { + tx, blockHash, blockNumber, index := rawdb.ReadCanonicalTransaction(b.db, txHash) + return tx != nil, tx, blockHash, blockNumber, index +} +func (b testBackend) GetCanonicalReceipt(tx *types.Transaction, blockHash common.Hash, blockNumber, blockIndex uint64) (*types.Receipt, error) { + return b.chain.GetCanonicalReceipt(tx, blockHash, blockNumber, blockIndex) } func (b testBackend) TxIndexDone() bool { return true diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 49c3a37560..f709a1fcdc 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -68,13 +68,14 @@ type Backend interface { StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error) Pending() (*types.Block, types.Receipts, *state.StateDB) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) + GetCanonicalReceipt(tx *types.Transaction, blockHash common.Hash, blockNumber, blockIndex uint64) (*types.Receipt, error) GetEVM(ctx context.Context, state *state.StateDB, header *types.Header, vmConfig *vm.Config, blockCtx *vm.BlockContext) *vm.EVM SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription // Transaction pool API SendTx(ctx context.Context, signedTx *types.Transaction) error - GetTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) + GetCanonicalTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) TxIndexDone() bool GetPoolTransactions() (types.Transactions, error) GetPoolTransaction(txHash common.Hash) *types.Transaction diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index 9b86e452a5..30791f32b5 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -369,6 +369,9 @@ func (b *backendMock) Pending() (*types.Block, types.Receipts, *state.StateDB) { func (b *backendMock) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) { return nil, nil } +func (b *backendMock) GetCanonicalReceipt(tx *types.Transaction, blockHash common.Hash, blockNumber, blockIndex uint64) (*types.Receipt, error) { + return nil, nil +} func (b *backendMock) GetLogs(ctx context.Context, blockHash common.Hash, number uint64) ([][]*types.Log, error) { return nil, nil } @@ -380,7 +383,7 @@ func (b *backendMock) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) eve return nil } func (b *backendMock) SendTx(ctx context.Context, signedTx *types.Transaction) error { return nil } -func (b *backendMock) GetTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) { +func (b *backendMock) GetCanonicalTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) { return false, nil, [32]byte{}, 0, 0 } func (b *backendMock) TxIndexDone() bool { return true }