triedb/pathdb: improve trienode reader

This commit is contained in:
Gary Rong 2026-01-26 14:35:14 +08:00
parent 9cfb2225dc
commit 5df5020f0d
3 changed files with 185 additions and 59 deletions

View file

@ -284,10 +284,10 @@ func newTrienodeReader(disk ethdb.KeyValueReader, freezer ethdb.AncientReader, r
}
// readTrienode retrieves the trienode data from the specified trienode history.
func (r *trienodeReader) readTrienode(addrHash common.Hash, path string, historyID uint64) ([]byte, error) {
func (r *trienodeReader) readTrienode(addrHash common.Hash, path string, historyID uint64) ([]byte, bool, error) {
tr, err := newTrienodeHistoryReader(historyID, r.freezer)
if err != nil {
return nil, err
return nil, false, err
}
return tr.read(addrHash, path)
}
@ -355,15 +355,19 @@ func (r *trienodeReader) readOptimized(state stateIdent, it HistoryIndexIterator
seq += 1
eg.Go(func() error {
data, found, err := r.readTrienode(state.addressHash, state.path, id)
if err != nil {
term.Store(true)
return err
}
// In optimistic readahead mode, it is theoretically possible to encounter a
// NotFound error, where the trie node does not actually exist and the iterator
// reports a false-positive mutation record. Terminate the iterator if so, as
// all the necessary data (checkpoints and all diffs) required has already been
// fetching.
data, err := r.readTrienode(state.addressHash, state.path, id)
if err != nil {
if !found {
term.Store(true)
log.Debug("Failed to read the trienode", "err", err)
log.Debug("Failed to read the trienode")
return nil
}
full, _, err := decodeNodeFull(data)

View file

@ -606,51 +606,113 @@ func (ir iRange) len() uint32 {
return ir.limit - ir.start
}
// singleTrienodeHistoryReader provides read access to a single trie within the
// trienode history. It stores an offset to the trie's position in the history,
// along with a set of per-node offsets that can be resolved on demand.
type singleTrienodeHistoryReader struct {
id uint64
reader ethdb.AncientReader
valueRange iRange // value range within the global value section
valueInternalOffsets map[string]iRange // value offset within the single trie data
id uint64
reader ethdb.AncientReader
keyData []byte
valueRange iRange
}
// TODO(rjl493456442): This function performs a large number of allocations.
// Given the large data size, a byte pool could be used to mitigate this.
func newSingleTrienodeHistoryReader(id uint64, reader ethdb.AncientReader, keyRange iRange, valueRange iRange) (*singleTrienodeHistoryReader, error) {
// TODO(rjl493456442) the data size is known in advance, allocating the
// dedicated byte slices from the pool.
keyData, err := rawdb.ReadTrienodeHistoryKeySection(reader, id, uint64(keyRange.start), uint64(keyRange.len()))
if err != nil {
return nil, err
}
valueOffsets := make(map[string]iRange)
err = decodeSingle(keyData, func(key []byte, start int, limit int) error {
valueOffsets[string(key)] = iRange{
start: uint32(start) + valueRange.start,
limit: uint32(limit) + valueRange.start,
}
return nil
})
if err != nil {
return nil, err
}
return &singleTrienodeHistoryReader{
id: id,
reader: reader,
valueRange: valueRange,
valueInternalOffsets: valueOffsets,
id: id,
reader: reader,
keyData: keyData,
valueRange: valueRange,
}, nil
}
// read retrieves the trie node data with the provided node path.
func (sr *singleTrienodeHistoryReader) read(path string) ([]byte, error) {
offset, exists := sr.valueInternalOffsets[path]
if !exists {
return nil, fmt.Errorf("trienode %v not found", []byte(path))
// searchSingle searches for a specific trie node identified by the provided
// key within a single trie node chunk.
//
// It returns the node value's offset range (start and limit) within the
// trie node data. An error is returned if the node cannot be found.
func (sr *singleTrienodeHistoryReader) searchSingle(key []byte) (int, int, bool, error) {
keyOffsets, valOffsets, keyLimit, err := decodeRestartTrailer(sr.keyData)
if err != nil {
return 0, 0, false, err
}
return rawdb.ReadTrienodeHistoryValueSection(sr.reader, sr.id, uint64(offset.start), uint64(offset.len()))
// Binary search against the boundary keys for each restart section
var (
boundFind bool
boundValueLen uint64
)
pos := sort.Search(len(keyOffsets), func(i int) bool {
_, nValue, dkey, _, derr := decodeKeyEntry(sr.keyData[keyOffsets[i]:], 0)
if derr != nil {
err = derr
return false
}
n := bytes.Compare(key, dkey)
if n == 0 {
boundFind = true
boundValueLen = nValue
}
return n <= 0
})
if err != nil {
return 0, 0, false, err
}
// The node is found as the boundary of restart section
if boundFind {
start := valOffsets[pos]
limit := valOffsets[pos] + uint32(boundValueLen)
return int(start), int(limit), true, nil
}
// The node is not found as all others have larger key than the target
if pos == 0 {
return 0, 0, false, nil
}
// Search the target node within the restart section
var keyData []byte
if pos == len(keyOffsets) {
keyData = sr.keyData[keyOffsets[pos-1]:keyLimit] // last section
} else {
keyData = sr.keyData[keyOffsets[pos-1]:keyOffsets[pos]] // non-last section
}
var (
nStart int
nLimit int
found bool
)
err = decodeRestartSection(keyData, func(ikey []byte, start, limit int) (bool, error) {
if bytes.Equal(key, ikey) {
nStart = int(valOffsets[pos-1]) + start
nLimit = int(valOffsets[pos-1]) + limit
found = true
return true, nil // abort = true
}
return false, nil // abort = false
})
if err != nil {
return 0, 0, false, err
}
if !found {
return 0, 0, false, nil
}
return nStart, nLimit, true, nil
}
// read retrieves the trie node data with the provided node path.
func (sr *singleTrienodeHistoryReader) read(key []byte) ([]byte, bool, error) {
start, limit, found, err := sr.searchSingle(key)
if err != nil {
return nil, false, err
}
if !found {
return nil, false, nil
}
valStart := uint64(start) + uint64(sr.valueRange.start)
valLen := uint64(limit - start)
value, err := rawdb.ReadTrienodeHistoryValueSection(sr.reader, sr.id, valStart, valLen)
if err != nil {
return nil, false, err
}
return value, true, nil
}
// trienodeHistoryReader provides read access to node data in the trie node history.
@ -714,25 +776,25 @@ func (r *trienodeHistoryReader) decodeHeader() error {
}
// read retrieves the trie node data with the provided TrieID and node path.
func (r *trienodeHistoryReader) read(owner common.Hash, path string) ([]byte, error) {
func (r *trienodeHistoryReader) read(owner common.Hash, path string) ([]byte, bool, error) {
ir, ok := r.iReaders[owner]
if !ok {
keyRange, exists := r.keyRanges[owner]
if !exists {
return nil, fmt.Errorf("trie %x is unknown", owner)
return nil, false, nil // not found
}
valRange, exists := r.valRanges[owner]
if !exists {
return nil, fmt.Errorf("trie %x is unknown", owner)
return nil, false, nil // not found
}
var err error
ir, err = newSingleTrienodeHistoryReader(r.id, r.reader, keyRange, valRange)
if err != nil {
return nil, err
return nil, false, err
}
r.iReaders[owner] = ir
}
return ir.read(path)
return ir.read([]byte(path))
}
// writeTrienodeHistory persists the trienode history associated with the given diff layer.

View file

@ -19,6 +19,7 @@ package pathdb
import (
"bytes"
"encoding/binary"
"fmt"
"math/rand"
"reflect"
"testing"
@ -144,7 +145,7 @@ func TestTrienodeHistoryReader(t *testing.T) {
for _, owner := range h.owners {
nodes := h.nodes[owner]
for key, value := range nodes {
blob, err := tr.read(owner, key)
blob, _, err := tr.read(owner, key)
if err != nil {
t.Fatalf("Failed to read trienode history: %v", err)
}
@ -424,16 +425,17 @@ func TestTrienodeHistoryReaderNonExistentPath(t *testing.T) {
}
// Try to read a non-existent path
_, err = tr.read(testrand.Hash(), "nonexistent")
if err == nil {
t.Fatal("Expected error for non-existent trie owner")
var found bool
_, found, err = tr.read(testrand.Hash(), "nonexistent")
if found || err != nil {
t.Fatal("Expected not found for non-existent trie owner")
}
// Try to read from existing owner but non-existent path
owner := h.owners[0]
_, err = tr.read(owner, "nonexistent-path")
if err == nil {
t.Fatal("Expected error for non-existent path")
_, found, err = tr.read(owner, "nonexistent-path")
if found || err != nil {
t.Fatal("Expected not found for non-existent path")
}
}
@ -464,16 +466,16 @@ func TestTrienodeHistoryReaderNilValues(t *testing.T) {
}
// Test reading nil values
data1, err := tr.read(owner, "nil1")
if err != nil {
data1, found, err := tr.read(owner, "nil1")
if err != nil || !found {
t.Fatalf("Failed to read nil value: %v", err)
}
if len(data1) != 0 {
t.Fatal("Expected nil data for nil value")
}
data2, err := tr.read(owner, "nil2")
if err != nil {
data2, found, err := tr.read(owner, "nil2")
if err != nil || !found {
t.Fatalf("Failed to read nil value: %v", err)
}
if len(data2) != 0 {
@ -481,8 +483,8 @@ func TestTrienodeHistoryReaderNilValues(t *testing.T) {
}
// Test reading non-nil value
data3, err := tr.read(owner, "data1")
if err != nil {
data3, found, err := tr.read(owner, "data1")
if err != nil || !found {
t.Fatalf("Failed to read non-nil value: %v", err)
}
if !bytes.Equal(data3, []byte("some data")) {
@ -498,7 +500,7 @@ func TestTrienodeHistoryReaderNilKey(t *testing.T) {
// Add some nil values
nodes[owner][""] = []byte("some data")
nodes[owner]["data1"] = []byte("some data")
nodes[owner]["data1"] = []byte("some data1")
h := newTrienodeHistory(common.Hash{}, common.Hash{}, 1, nodes)
@ -516,7 +518,7 @@ func TestTrienodeHistoryReaderNilKey(t *testing.T) {
}
// Test reading nil values
data1, err := tr.read(owner, "")
data1, _, err := tr.read(owner, "")
if err != nil {
t.Fatalf("Failed to read nil value: %v", err)
}
@ -525,11 +527,11 @@ func TestTrienodeHistoryReaderNilKey(t *testing.T) {
}
// Test reading non-nil value
data2, err := tr.read(owner, "data1")
data2, _, err := tr.read(owner, "data1")
if err != nil {
t.Fatalf("Failed to read non-nil value: %v", err)
}
if !bytes.Equal(data2, []byte("some data")) {
if !bytes.Equal(data2, []byte("some data1")) {
t.Fatal("Data mismatch for non-nil key")
}
}
@ -691,3 +693,61 @@ func testEncodeDecode(t *testing.T, h *trienodeHistory) {
t.Fatal("Trienode content mismatch")
}
}
func TestSearchSingle(t *testing.T) {
nodes := make(map[common.Hash]map[string][]byte)
ownerA, ownerB := testrand.Hash(), testrand.Hash()
nodes[ownerA] = make(map[string][]byte)
nodes[ownerB] = make(map[string][]byte)
for i := 0; i < trienodeDataBlockRestartLen*2; i++ {
nodes[ownerA][fmt.Sprintf("%d", 2*i+1)] = testrand.Bytes(rand.Intn(5))
nodes[ownerB][fmt.Sprintf("%d", 2*i+1)] = testrand.Bytes(rand.Intn(5))
}
h := newTrienodeHistory(common.Hash{}, common.Hash{}, 1, nodes)
var freezer, _ = rawdb.NewTrienodeFreezer(t.TempDir(), false, false)
defer freezer.Close()
header, keySection, valueSection, _ := h.encode()
if err := rawdb.WriteTrienodeHistory(freezer, 1, header, keySection, valueSection); err != nil {
t.Fatalf("Failed to write trienode history: %v", err)
}
tr, err := newTrienodeHistoryReader(1, freezer)
if err != nil {
t.Fatalf("Failed to construct history reader: %v", err)
}
// Test reading non-existent entry
keys := []string{
"0",
"2",
"30",
"32",
"64",
"1000",
}
for _, key := range keys {
_, found, err := tr.read(ownerA, key)
if err != nil || found {
t.Fatalf("Expected non-existent entry %v", err)
}
_, found, err = tr.read(ownerB, key)
if err != nil || found {
t.Fatalf("Expected non-existent entry %v", err)
}
}
for owner, subnodes := range nodes {
for key, value := range subnodes {
got, found, err := tr.read(owner, key)
if err != nil || !found {
t.Fatal("Failed to read trienode")
}
if bytes.Compare(got, value) != 0 {
t.Fatalf("Unexpected value for key %v, got %v, expected %v", []byte(key), got, value)
}
}
}
}