From e94123acc2bc8283764b26f3423f5e026515c0f4 Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Tue, 15 Jul 2025 15:48:36 +0200 Subject: [PATCH] core/rawdb: reduce allocations in rawdb.ReadHeaderNumber (#31913) This is something interesting I came across during my benchmarks, we spent ~3.8% of all allocations allocating the header number on the heap. ``` (pprof) list GetHeaderByHash Total: 38197204475 ROUTINE ======================== github.com/ethereum/go-ethereum/core.(*BlockChain).GetHeaderByHash in github.com/ethereum/go-ethereum/core/blockchain_reader.go 0 5786566117 (flat, cum) 15.15% of Total . . 79:func (bc *BlockChain) GetHeaderByHash(hash common.Hash) *types.Header { . 5786566117 80: return bc.hc.GetHeaderByHash(hash) . . 81:} . . 82: . . 83:// GetHeaderByNumber retrieves a block header from the database by number, . . 84:// caching it (associated with its hash) if found. . . 85:func (bc *BlockChain) GetHeaderByNumber(number uint64) *types.Header { ROUTINE ======================== github.com/ethereum/go-ethereum/core.(*HeaderChain).GetHeaderByHash in github.com/ethereum/go-ethereum/core/headerchain.go 0 5786566117 (flat, cum) 15.15% of Total . . 404:func (hc *HeaderChain) GetHeaderByHash(hash common.Hash) *types.Header { . 1471264309 405: number := hc.GetBlockNumber(hash) . . 406: if number == nil { . . 407: return nil . . 408: } . 4315301808 409: return hc.GetHeader(hash, *number) . . 410:} . . 411: . . 412:// HasHeader checks if a block header is present in the database or not. . . 413:// In theory, if header is present in the database, all relative components . . 414:// like td and hash->number should be present too. (pprof) list GetBlockNumber Total: 38197204475 ROUTINE ======================== github.com/ethereum/go-ethereum/core.(*HeaderChain).GetBlockNumber in github.com/ethereum/go-ethereum/core/headerchain.go 94438817 1471264309 (flat, cum) 3.85% of Total . . 100:func (hc *HeaderChain) GetBlockNumber(hash common.Hash) *uint64 { 94438817 94438817 101: if cached, ok := hc.numberCache.Get(hash); ok { . . 102: return &cached . . 103: } . 1376270828 104: number := rawdb.ReadHeaderNumber(hc.chainDb, hash) . . 105: if number != nil { . 554664 106: hc.numberCache.Add(hash, *number) . . 107: } . . 108: return number . . 109:} . . 110: . . 111:type headerWriteResult struct { (pprof) list ReadHeaderNumber Total: 38197204475 ROUTINE ======================== github.com/ethereum/go-ethereum/core/rawdb.ReadHeaderNumber in github.com/ethereum/go-ethereum/core/rawdb/accessors_chain.go 204606513 1376270828 (flat, cum) 3.60% of Total . . 146:func ReadHeaderNumber(db ethdb.KeyValueReader, hash common.Hash) *uint64 { 109577863 1281242178 147: data, _ := db.Get(headerNumberKey(hash)) . . 148: if len(data) != 8 { . . 149: return nil . . 150: } 95028650 95028650 151: number := binary.BigEndian.Uint64(data) . . 152: return &number . . 153:} . . 154: . . 155:// WriteHeaderNumber stores the hash->number mapping. . . 156:func WriteHeaderNumber(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { ``` Opening this to discuss the idea, I know that rawdb.EmptyNumber is not a great name for the variable, open to suggestions --- cmd/geth/chaincmd.go | 4 +-- core/blockchain_reader.go | 45 ++++++++++++++++--------------- core/blockchain_test.go | 6 ++--- core/headerchain.go | 18 ++++++------- core/rawdb/accessors_chain.go | 18 ++++++------- core/rawdb/accessors_indexes.go | 6 ++++- core/rawdb/chain_freezer.go | 12 ++++----- core/rawdb/database.go | 7 ++++- core/txindexer.go | 6 ++--- eth/filters/filter_system_test.go | 24 ++++++++--------- 10 files changed, 79 insertions(+), 67 deletions(-) diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index acd7b5c230..44e11dbf06 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -576,8 +576,8 @@ func parseDumpConfig(ctx *cli.Context, db ethdb.Database) (*state.DumpConfig, co arg := ctx.Args().First() if hashish(arg) { hash := common.HexToHash(arg) - if number := rawdb.ReadHeaderNumber(db, hash); number != nil { - header = rawdb.ReadHeader(db, hash, *number) + if number, ok := rawdb.ReadHeaderNumber(db, hash); ok { + header = rawdb.ReadHeader(db, hash, number) } else { return nil, common.Hash{}, fmt.Errorf("block %x not found", hash) } diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index 7626e9e30d..4894523b0e 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -91,7 +91,10 @@ func (bc *BlockChain) GetHeaderByNumber(number uint64) *types.Header { // GetBlockNumber retrieves the block number associated with a block hash. func (bc *BlockChain) GetBlockNumber(hash common.Hash) *uint64 { - return bc.hc.GetBlockNumber(hash) + if num, ok := bc.hc.GetBlockNumber(hash); ok { + return &num + } + return nil } // GetHeadersFrom returns a contiguous segment of headers, in rlp-form, going @@ -107,11 +110,11 @@ func (bc *BlockChain) GetBody(hash common.Hash) *types.Body { if cached, ok := bc.bodyCache.Get(hash); ok { return cached } - number := bc.hc.GetBlockNumber(hash) - if number == nil { + number, ok := bc.hc.GetBlockNumber(hash) + if !ok { return nil } - body := rawdb.ReadBody(bc.db, hash, *number) + body := rawdb.ReadBody(bc.db, hash, number) if body == nil { return nil } @@ -127,11 +130,11 @@ func (bc *BlockChain) GetBodyRLP(hash common.Hash) rlp.RawValue { if cached, ok := bc.bodyRLPCache.Get(hash); ok { return cached } - number := bc.hc.GetBlockNumber(hash) - if number == nil { + number, ok := bc.hc.GetBlockNumber(hash) + if !ok { return nil } - body := rawdb.ReadBodyRLP(bc.db, hash, *number) + body := rawdb.ReadBodyRLP(bc.db, hash, number) if len(body) == 0 { return nil } @@ -180,11 +183,11 @@ func (bc *BlockChain) GetBlock(hash common.Hash, number uint64) *types.Block { // GetBlockByHash retrieves a block from the database by hash, caching it if found. func (bc *BlockChain) GetBlockByHash(hash common.Hash) *types.Block { - number := bc.hc.GetBlockNumber(hash) - if number == nil { + number, ok := bc.hc.GetBlockNumber(hash) + if !ok { return nil } - return bc.GetBlock(hash, *number) + return bc.GetBlock(hash, number) } // GetBlockByNumber retrieves a block from the database by number, caching it @@ -200,18 +203,18 @@ func (bc *BlockChain) GetBlockByNumber(number uint64) *types.Block { // GetBlocksFromHash returns the block corresponding to hash and up to n-1 ancestors. // [deprecated by eth/62] func (bc *BlockChain) GetBlocksFromHash(hash common.Hash, n int) (blocks []*types.Block) { - number := bc.hc.GetBlockNumber(hash) - if number == nil { + number, ok := bc.hc.GetBlockNumber(hash) + if !ok { return nil } for i := 0; i < n; i++ { - block := bc.GetBlock(hash, *number) + block := bc.GetBlock(hash, number) if block == nil { break } blocks = append(blocks, block) hash = block.ParentHash() - *number-- + number-- } return } @@ -259,15 +262,15 @@ func (bc *BlockChain) GetReceiptsByHash(hash common.Hash) types.Receipts { if receipts, ok := bc.receiptsCache.Get(hash); ok { return receipts } - number := rawdb.ReadHeaderNumber(bc.db, hash) - if number == nil { + number, ok := rawdb.ReadHeaderNumber(bc.db, hash) + if !ok { return nil } - header := bc.GetHeader(hash, *number) + header := bc.GetHeader(hash, number) if header == nil { return nil } - receipts := rawdb.ReadReceipts(bc.db, hash, *number, header.Time, bc.chainConfig) + receipts := rawdb.ReadReceipts(bc.db, hash, number, header.Time, bc.chainConfig) if receipts == nil { return nil } @@ -286,11 +289,11 @@ func (bc *BlockChain) GetRawReceipts(hash common.Hash, number uint64) types.Rece // GetReceiptsRLP retrieves the receipts of a block. func (bc *BlockChain) GetReceiptsRLP(hash common.Hash) rlp.RawValue { - number := rawdb.ReadHeaderNumber(bc.db, hash) - if number == nil { + number, ok := rawdb.ReadHeaderNumber(bc.db, hash) + if !ok { return nil } - return rawdb.ReadReceiptsRLP(bc.db, hash, *number) + return rawdb.ReadReceiptsRLP(bc.db, hash, number) } // GetUnclesInChain retrieves all the uncles from a given block backwards until diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 5e768fccdf..b749798f9c 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -782,13 +782,13 @@ func testFastVsFullChains(t *testing.T, scheme string) { } // Check that hash-to-number mappings are present in all databases. - if m := rawdb.ReadHeaderNumber(fastDb, hash); m == nil || *m != num { + if m, ok := rawdb.ReadHeaderNumber(fastDb, hash); !ok || m != num { t.Errorf("block #%d [%x]: wrong hash-to-number mapping in fastdb: %v", num, hash, m) } - if m := rawdb.ReadHeaderNumber(ancientDb, hash); m == nil || *m != num { + if m, ok := rawdb.ReadHeaderNumber(ancientDb, hash); !ok || m != num { t.Errorf("block #%d [%x]: wrong hash-to-number mapping in ancientdb: %v", num, hash, m) } - if m := rawdb.ReadHeaderNumber(archiveDb, hash); m == nil || *m != num { + if m, ok := rawdb.ReadHeaderNumber(archiveDb, hash); !ok || m != num { t.Errorf("block #%d [%x]: wrong hash-to-number mapping in archivedb: %v", num, hash, m) } } diff --git a/core/headerchain.go b/core/headerchain.go index 6e70dfa865..a33222e9fd 100644 --- a/core/headerchain.go +++ b/core/headerchain.go @@ -97,15 +97,15 @@ func NewHeaderChain(chainDb ethdb.Database, config *params.ChainConfig, engine c // GetBlockNumber retrieves the block number belonging to the given hash // from the cache or database -func (hc *HeaderChain) GetBlockNumber(hash common.Hash) *uint64 { +func (hc *HeaderChain) GetBlockNumber(hash common.Hash) (uint64, bool) { if cached, ok := hc.numberCache.Get(hash); ok { - return &cached + return cached, true } - number := rawdb.ReadHeaderNumber(hc.chainDb, hash) - if number != nil { - hc.numberCache.Add(hash, *number) + number, ok := rawdb.ReadHeaderNumber(hc.chainDb, hash) + if ok { + hc.numberCache.Add(hash, number) } - return number + return number, ok } type headerWriteResult struct { @@ -402,11 +402,11 @@ func (hc *HeaderChain) GetHeader(hash common.Hash, number uint64) *types.Header // GetHeaderByHash retrieves a block header from the database by hash, caching it if // found. func (hc *HeaderChain) GetHeaderByHash(hash common.Hash) *types.Header { - number := hc.GetBlockNumber(hash) - if number == nil { + number, ok := hc.GetBlockNumber(hash) + if !ok { return nil } - return hc.GetHeader(hash, *number) + return hc.GetHeader(hash, number) } // HasHeader checks if a block header is present in the database or not. diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index 4426c6a9e7..0782a0e7da 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -143,13 +143,13 @@ func ReadAllCanonicalHashes(db ethdb.Iteratee, from uint64, to uint64, limit int } // ReadHeaderNumber returns the header number assigned to a hash. -func ReadHeaderNumber(db ethdb.KeyValueReader, hash common.Hash) *uint64 { +func ReadHeaderNumber(db ethdb.KeyValueReader, hash common.Hash) (uint64, bool) { data, _ := db.Get(headerNumberKey(hash)) if len(data) != 8 { - return nil + return 0, false } number := binary.BigEndian.Uint64(data) - return &number + return number, true } // WriteHeaderNumber stores the hash->number mapping. @@ -935,11 +935,11 @@ func ReadHeadHeader(db ethdb.Reader) *types.Header { if headHeaderHash == (common.Hash{}) { return nil } - headHeaderNumber := ReadHeaderNumber(db, headHeaderHash) - if headHeaderNumber == nil { + headHeaderNumber, ok := ReadHeaderNumber(db, headHeaderHash) + if !ok { return nil } - return ReadHeader(db, headHeaderHash, *headHeaderNumber) + return ReadHeader(db, headHeaderHash, headHeaderNumber) } // ReadHeadBlock returns the current canonical head block. @@ -948,9 +948,9 @@ func ReadHeadBlock(db ethdb.Reader) *types.Block { if headBlockHash == (common.Hash{}) { return nil } - headBlockNumber := ReadHeaderNumber(db, headBlockHash) - if headBlockNumber == nil { + headBlockNumber, ok := ReadHeaderNumber(db, headBlockHash) + if !ok { return nil } - return ReadBlock(db, headBlockHash, *headBlockNumber) + return ReadBlock(db, headBlockHash, headBlockNumber) } diff --git a/core/rawdb/accessors_indexes.go b/core/rawdb/accessors_indexes.go index f71bbb500b..a725f144d4 100644 --- a/core/rawdb/accessors_indexes.go +++ b/core/rawdb/accessors_indexes.go @@ -41,7 +41,11 @@ func DecodeTxLookupEntry(data []byte, db ethdb.Reader) *uint64 { } // Database v4-v5 tx lookup format just stores the hash if len(data) == common.HashLength { - return ReadHeaderNumber(db, common.BytesToHash(data)) + number, ok := ReadHeaderNumber(db, common.BytesToHash(data)) + if !ok { + return nil + } + return &number } // Finally try database v3 tx lookup format var entry LegacyTxLookupEntry diff --git a/core/rawdb/chain_freezer.go b/core/rawdb/chain_freezer.go index 3a5218c023..4834354b22 100644 --- a/core/rawdb/chain_freezer.go +++ b/core/rawdb/chain_freezer.go @@ -107,12 +107,12 @@ func (f *chainFreezer) readHeadNumber(db ethdb.KeyValueReader) uint64 { log.Error("Head block is not reachable") return 0 } - number := ReadHeaderNumber(db, hash) - if number == nil { + number, ok := ReadHeaderNumber(db, hash) + if !ok { log.Error("Number of head block is missing") return 0 } - return *number + return number } // readFinalizedNumber returns the number of finalized block. 0 is returned @@ -122,12 +122,12 @@ func (f *chainFreezer) readFinalizedNumber(db ethdb.KeyValueReader) uint64 { if hash == (common.Hash{}) { return 0 } - number := ReadHeaderNumber(db, hash) - if number == nil { + number, ok := ReadHeaderNumber(db, hash) + if !ok { log.Error("Number of finalized block is missing") return 0 } - return *number + return number } // freezeThreshold returns the threshold for chain freezing. It's determined diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 9681c39c58..2ebdf360b5 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -268,7 +268,12 @@ func Open(db ethdb.KeyValueStore, opts OpenOptions) (ethdb.Database, error) { if kvhash, _ := db.Get(headerHashKey(frozen)); len(kvhash) == 0 { // Subsequent header after the freezer limit is missing from the database. // Reject startup if the database has a more recent head. - if head := *ReadHeaderNumber(db, ReadHeadHeaderHash(db)); head > frozen-1 { + head, ok := ReadHeaderNumber(db, ReadHeadHeaderHash(db)) + if !ok { + printChainMetadata(db) + return nil, fmt.Errorf("could not read header number, hash %v", ReadHeadHeaderHash(db)) + } + if head > frozen-1 { // Find the smallest block stored in the key-value store // in range of [frozen, head] var number uint64 diff --git a/core/txindexer.go b/core/txindexer.go index 587118ed7f..b2a94a6ead 100644 --- a/core/txindexer.go +++ b/core/txindexer.go @@ -217,11 +217,11 @@ func (indexer *txIndexer) resolveHead() uint64 { if headBlockHash == (common.Hash{}) { return 0 } - headBlockNumber := rawdb.ReadHeaderNumber(indexer.db, headBlockHash) - if headBlockNumber == nil { + headBlockNumber, ok := rawdb.ReadHeaderNumber(indexer.db, headBlockHash) + if !ok { return 0 } - return *headBlockNumber + return headBlockNumber } // loop is the scheduler of the indexer, assigning indexing/unindexing tasks depending diff --git a/eth/filters/filter_system_test.go b/eth/filters/filter_system_test.go index 10f82364b5..3e686ca2eb 100644 --- a/eth/filters/filter_system_test.go +++ b/eth/filters/filter_system_test.go @@ -92,18 +92,18 @@ func (b *testBackend) HeaderByNumber(ctx context.Context, blockNr rpc.BlockNumbe switch blockNr { case rpc.LatestBlockNumber: hash = rawdb.ReadHeadBlockHash(b.db) - number := rawdb.ReadHeaderNumber(b.db, hash) - if number == nil { + number, ok := rawdb.ReadHeaderNumber(b.db, hash) + if !ok { return nil, nil } - num = *number + num = number case rpc.FinalizedBlockNumber: hash = rawdb.ReadFinalizedBlockHash(b.db) - number := rawdb.ReadHeaderNumber(b.db, hash) - if number == nil { + number, ok := rawdb.ReadHeaderNumber(b.db, hash) + if !ok { return nil, nil } - num = *number + num = number case rpc.SafeBlockNumber: return nil, errors.New("safe block not found") default: @@ -114,11 +114,11 @@ func (b *testBackend) HeaderByNumber(ctx context.Context, blockNr rpc.BlockNumbe } func (b *testBackend) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { - number := rawdb.ReadHeaderNumber(b.db, hash) - if number == nil { + number, ok := rawdb.ReadHeaderNumber(b.db, hash) + if !ok { return nil, nil } - return rawdb.ReadHeader(b.db, hash, *number), nil + return rawdb.ReadHeader(b.db, hash, number), nil } func (b *testBackend) GetBody(ctx context.Context, hash common.Hash, number rpc.BlockNumber) (*types.Body, error) { @@ -129,9 +129,9 @@ func (b *testBackend) GetBody(ctx context.Context, hash common.Hash, number rpc. } func (b *testBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) { - if number := rawdb.ReadHeaderNumber(b.db, hash); number != nil { - if header := rawdb.ReadHeader(b.db, hash, *number); header != nil { - return rawdb.ReadReceipts(b.db, hash, *number, header.Time, params.TestChainConfig), nil + if number, ok := rawdb.ReadHeaderNumber(b.db, hash); ok { + if header := rawdb.ReadHeader(b.db, hash, number); header != nil { + return rawdb.ReadReceipts(b.db, hash, number, header.Time, params.TestChainConfig), nil } } return nil, nil