triedb/pathdb: introduce extension to history index structure (#33399)

It's a PR based on #33303 and introduces an approach for trienode
history indexing.

---

In the current archive node design, resolving a historical trie node at
a specific block
involves the following steps:

- Look up the corresponding trie node index and locate the first entry
whose state ID
   is greater than the target state ID.
- Resolve the trie node from the associated trienode history object.

A naive approach would be to store mutation records for every trie node,
similar to
how flat state mutations are recorded. However, the total number of trie
nodes is
extremely large (approximately 2.4 billion), and the vast majority of
them are rarely
modified. Creating an index entry for each individual trie node would be
very wasteful
in both storage and indexing overhead. To address this, we aggregate
multiple trie
nodes into chunks and index mutations at the chunk level instead. 

---

For a storage trie, the trie is vertically partitioned into multiple sub
tries, each spanning
three consecutive levels. The top three levels (1 + 16 + 256 nodes) form
the first chunk,
and every subsequent three-level segment forms another chunk.

```
Original trie structure

Level 0               [ ROOT ]                               1 node
Level 1        [0] [1] [2] ... [f]                          16 nodes
Level 2     [00] [01] ... [0f] [10] ... [ff]               256 nodes
Level 3   [000] [001] ... [00f] [010] ... [fff]           4096 nodes
Level 4   [0000] ... [000f] [0010] ... [001f] ... [ffff] 65536 nodes

Vertical split into chunks (3 levels per chunk)

Level0             [ ROOT ]                     1 chunk
Level3        [000]   ...     [fff]          4096 chunks
Level6   [000000]    ...    [fffffff]    16777216 chunks  
```

Within each chunk, there are 273 nodes in total, regardless of the
chunk's depth in the trie.

```
Level 0           [ 0 ]                         1 node
Level 1        [ 1 ] … [ 16 ]                  16 nodes
Level 2     [ 17 ] … … [ 272 ]                256 nodes
```

Each chunk is uniquely identified by the path prefix of the root node of
its corresponding
sub-trie. Within a chunk, nodes are identified by a numeric index
ranging from 0 to 272.

For example, suppose that at block 100, the nodes with paths `[]`,
`[0]`, `[f]`, `[00]`, and `[ff]`
are modified. The mutation record for chunk 0 is then appended with the
following entry:

`[100 → [0, 1, 16, 17, 272]]`, `272` is the numeric ID of path `[ff]`.

Furthermore, due to the structural properties of the Merkle Patricia
Trie, if a child node
is modified, all of its ancestors along the same path must also be
updated. As a result,
in the above example, recording mutations for nodes `00` and `ff` alone
is sufficient,
as this implicitly indicates that their ancestor nodes `[]`, `[0]` and
`[f]` were also
modified at block 100.

--- 

Query processing is slightly more complicated. Since trie nodes are
indexed at the chunk
level, each individual trie node lookup requires an additional filtering
step to ensure that
a given mutation record actually corresponds to the target trie node.

As mentioned earlier, mutation records store only the numeric
identifiers of leaf nodes,
while ancestor nodes are omitted for storage efficiency. Consequently,
when querying
an ancestor node, additional checks are required to determine whether
the mutation
record implicitly represents a modification to that ancestor.

Moreover, since trie nodes are indexed at the chunk level, some trie
nodes may be
updated frequently, causing their mutation records to dominate the
index. Queries
targeting rarely modified trie nodes would then scan a large amount of
irrelevant
index data, significantly degrading performance.

To address this issue, a bitmap is introduced for each index block and
stored in the
chunk's metadata. Before loading a specific index block, the bitmap is
checked to
determine whether the block contains mutation records relevant to the
target trie node.
If the bitmap indicates that the block does not contain such records,
the block is skipped entirely.
This commit is contained in:
rjl493456442 2026-01-08 16:57:35 +08:00 committed by GitHub
parent a32851fac9
commit d5efd34010
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1117 additions and 509 deletions

View file

@ -25,22 +25,28 @@ import (
"github.com/ethereum/go-ethereum/ethdb"
)
// parseIndex parses the index data with the supplied byte stream. The index data
// is a list of fixed-sized metadata. Empty metadata is regarded as invalid.
func parseIndex(blob []byte) ([]*indexBlockDesc, error) {
// parseIndex parses the index data from the provided byte stream. The index data
// is a sequence of fixed-size metadata entries, and any empty metadata entry is
// considered invalid.
//
// Each metadata entry consists of two components: the indexBlockDesc and an
// optional extension bitmap. The bitmap length may vary across different categories,
// but must remain consistent within the same category.
func parseIndex(blob []byte, bitmapSize int) ([]*indexBlockDesc, error) {
if len(blob) == 0 {
return nil, errors.New("empty state history index")
}
if len(blob)%indexBlockDescSize != 0 {
return nil, fmt.Errorf("corrupted state index, len: %d", len(blob))
size := indexBlockDescSize + bitmapSize
if len(blob)%size != 0 {
return nil, fmt.Errorf("corrupted state index, len: %d, bitmap size: %d", len(blob), bitmapSize)
}
var (
lastID uint32
descList []*indexBlockDesc
)
for i := 0; i < len(blob)/indexBlockDescSize; i++ {
for i := 0; i < len(blob)/size; i++ {
var desc indexBlockDesc
desc.decode(blob[i*indexBlockDescSize : (i+1)*indexBlockDescSize])
desc.decode(blob[i*size : (i+1)*size])
if desc.empty() {
return nil, errors.New("empty state history index block")
}
@ -69,33 +75,35 @@ func parseIndex(blob []byte) ([]*indexBlockDesc, error) {
// indexReader is the structure to look up the state history index records
// associated with the specific state element.
type indexReader struct {
db ethdb.KeyValueReader
descList []*indexBlockDesc
readers map[uint32]*blockReader
state stateIdent
db ethdb.KeyValueReader
descList []*indexBlockDesc
readers map[uint32]*blockReader
state stateIdent
bitmapSize int
}
// loadIndexData loads the index data associated with the specified state.
func loadIndexData(db ethdb.KeyValueReader, state stateIdent) ([]*indexBlockDesc, error) {
func loadIndexData(db ethdb.KeyValueReader, state stateIdent, bitmapSize int) ([]*indexBlockDesc, error) {
blob := readStateIndex(state, db)
if len(blob) == 0 {
return nil, nil
}
return parseIndex(blob)
return parseIndex(blob, bitmapSize)
}
// newIndexReader constructs a index reader for the specified state. Reader with
// empty data is allowed.
func newIndexReader(db ethdb.KeyValueReader, state stateIdent) (*indexReader, error) {
descList, err := loadIndexData(db, state)
func newIndexReader(db ethdb.KeyValueReader, state stateIdent, bitmapSize int) (*indexReader, error) {
descList, err := loadIndexData(db, state, bitmapSize)
if err != nil {
return nil, err
}
return &indexReader{
descList: descList,
readers: make(map[uint32]*blockReader),
db: db,
state: state,
descList: descList,
readers: make(map[uint32]*blockReader),
db: db,
state: state,
bitmapSize: bitmapSize,
}, nil
}
@ -106,11 +114,9 @@ func (r *indexReader) refresh() error {
// may have been modified by additional elements written to the disk.
if len(r.descList) != 0 {
last := r.descList[len(r.descList)-1]
if !last.full() {
delete(r.readers, last.id)
}
delete(r.readers, last.id)
}
descList, err := loadIndexData(r.db, r.state)
descList, err := loadIndexData(r.db, r.state, r.bitmapSize)
if err != nil {
return err
}
@ -118,26 +124,10 @@ func (r *indexReader) refresh() error {
return nil
}
// newIterator creates an iterator for traversing the index entries.
func (r *indexReader) newIterator() *indexIterator {
return newIndexIterator(r.descList, func(id uint32) (*blockReader, error) {
br, ok := r.readers[id]
if !ok {
var err error
br, err = newBlockReader(readStateIndexBlock(r.state, r.db, id))
if err != nil {
return nil, err
}
r.readers[id] = br
}
return br, nil
})
}
// readGreaterThan locates the first element that is greater than the specified
// id. If no such element is found, MaxUint64 is returned.
func (r *indexReader) readGreaterThan(id uint64) (uint64, error) {
it := r.newIterator()
it := r.newIterator(nil)
found := it.SeekGT(id)
if err := it.Error(); err != nil {
return 0, err
@ -155,31 +145,33 @@ func (r *indexReader) readGreaterThan(id uint64) (uint64, error) {
// history ids) is stored in these second-layer index blocks, which are size
// limited.
type indexWriter struct {
descList []*indexBlockDesc // The list of index block descriptions
bw *blockWriter // The live index block writer
frozen []*blockWriter // The finalized index block writers, waiting for flush
lastID uint64 // The ID of the latest tracked history
state stateIdent
db ethdb.KeyValueReader
descList []*indexBlockDesc // The list of index block descriptions
bw *blockWriter // The live index block writer
frozen []*blockWriter // The finalized index block writers, waiting for flush
lastID uint64 // The ID of the latest tracked history
state stateIdent // The identifier of the state being indexed
bitmapSize int // The size of optional extension bitmap
db ethdb.KeyValueReader
}
// newIndexWriter constructs the index writer for the specified state. Additionally,
// it takes an integer as the limit and prunes all existing elements above that ID.
// It's essential as the recovery mechanism after unclean shutdown during the history
// indexing.
func newIndexWriter(db ethdb.KeyValueReader, state stateIdent, limit uint64) (*indexWriter, error) {
func newIndexWriter(db ethdb.KeyValueReader, state stateIdent, limit uint64, bitmapSize int) (*indexWriter, error) {
blob := readStateIndex(state, db)
if len(blob) == 0 {
desc := newIndexBlockDesc(0)
bw, _ := newBlockWriter(nil, desc, 0 /* useless if the block is empty */)
desc := newIndexBlockDesc(0, bitmapSize)
bw, _ := newBlockWriter(nil, desc, 0 /* useless if the block is empty */, bitmapSize != 0)
return &indexWriter{
descList: []*indexBlockDesc{desc},
bw: bw,
state: state,
db: db,
descList: []*indexBlockDesc{desc},
bw: bw,
state: state,
db: db,
bitmapSize: bitmapSize,
}, nil
}
descList, err := parseIndex(blob)
descList, err := parseIndex(blob, bitmapSize)
if err != nil {
return nil, err
}
@ -197,30 +189,31 @@ func newIndexWriter(db ethdb.KeyValueReader, state stateIdent, limit uint64) (*i
// Construct the writer for the last block. All elements in this block
// that exceed the limit will be truncated.
bw, err := newBlockWriter(indexBlock, lastDesc, limit)
bw, err := newBlockWriter(indexBlock, lastDesc, limit, bitmapSize != 0)
if err != nil {
return nil, err
}
return &indexWriter{
descList: descList,
lastID: bw.last(),
bw: bw,
state: state,
db: db,
descList: descList,
lastID: bw.last(),
bw: bw,
state: state,
db: db,
bitmapSize: bitmapSize,
}, nil
}
// append adds the new element into the index writer.
func (w *indexWriter) append(id uint64) error {
func (w *indexWriter) append(id uint64, ext []uint16) error {
if id <= w.lastID {
return fmt.Errorf("append element out of order, last: %d, this: %d", w.lastID, id)
}
if w.bw.full() {
if w.bw.estimateFull(ext) {
if err := w.rotate(); err != nil {
return err
}
}
if err := w.bw.append(id); err != nil {
if err := w.bw.append(id, ext); err != nil {
return err
}
w.lastID = id
@ -233,10 +226,10 @@ func (w *indexWriter) append(id uint64) error {
func (w *indexWriter) rotate() error {
var (
err error
desc = newIndexBlockDesc(w.bw.desc.id + 1)
desc = newIndexBlockDesc(w.bw.desc.id+1, w.bitmapSize)
)
w.frozen = append(w.frozen, w.bw)
w.bw, err = newBlockWriter(nil, desc, 0 /* useless if the block is empty */)
w.bw, err = newBlockWriter(nil, desc, 0 /* useless if the block is empty */, w.bitmapSize != 0)
if err != nil {
return err
}
@ -268,7 +261,8 @@ func (w *indexWriter) finish(batch ethdb.Batch) {
}
w.frozen = nil // release all the frozen writers
buf := make([]byte, 0, indexBlockDescSize*len(descList))
size := indexBlockDescSize + w.bitmapSize
buf := make([]byte, 0, size*len(descList))
for _, desc := range descList {
buf = append(buf, desc.encode()...)
}
@ -277,30 +271,32 @@ func (w *indexWriter) finish(batch ethdb.Batch) {
// indexDeleter is responsible for deleting index data for a specific state.
type indexDeleter struct {
descList []*indexBlockDesc // The list of index block descriptions
bw *blockWriter // The live index block writer
dropped []uint32 // The list of index block id waiting for deleting
lastID uint64 // The ID of the latest tracked history
state stateIdent
db ethdb.KeyValueReader
descList []*indexBlockDesc // The list of index block descriptions
bw *blockWriter // The live index block writer
dropped []uint32 // The list of index block id waiting for deleting
lastID uint64 // The ID of the latest tracked history
state stateIdent // The identifier of the state being indexed
bitmapSize int // The size of optional extension bitmap
db ethdb.KeyValueReader
}
// newIndexDeleter constructs the index deleter for the specified state.
func newIndexDeleter(db ethdb.KeyValueReader, state stateIdent, limit uint64) (*indexDeleter, error) {
func newIndexDeleter(db ethdb.KeyValueReader, state stateIdent, limit uint64, bitmapSize int) (*indexDeleter, error) {
blob := readStateIndex(state, db)
if len(blob) == 0 {
// TODO(rjl493456442) we can probably return an error here,
// deleter with no data is meaningless.
desc := newIndexBlockDesc(0)
bw, _ := newBlockWriter(nil, desc, 0 /* useless if the block is empty */)
desc := newIndexBlockDesc(0, bitmapSize)
bw, _ := newBlockWriter(nil, desc, 0 /* useless if the block is empty */, bitmapSize != 0)
return &indexDeleter{
descList: []*indexBlockDesc{desc},
bw: bw,
state: state,
db: db,
descList: []*indexBlockDesc{desc},
bw: bw,
state: state,
bitmapSize: bitmapSize,
db: db,
}, nil
}
descList, err := parseIndex(blob)
descList, err := parseIndex(blob, bitmapSize)
if err != nil {
return nil, err
}
@ -318,16 +314,17 @@ func newIndexDeleter(db ethdb.KeyValueReader, state stateIdent, limit uint64) (*
// Construct the writer for the last block. All elements in this block
// that exceed the limit will be truncated.
bw, err := newBlockWriter(indexBlock, lastDesc, limit)
bw, err := newBlockWriter(indexBlock, lastDesc, limit, bitmapSize != 0)
if err != nil {
return nil, err
}
return &indexDeleter{
descList: descList,
lastID: bw.last(),
bw: bw,
state: state,
db: db,
descList: descList,
lastID: bw.last(),
bw: bw,
state: state,
bitmapSize: bitmapSize,
db: db,
}, nil
}
@ -364,7 +361,7 @@ func (d *indexDeleter) pop(id uint64) error {
// Open the previous block writer for deleting
lastDesc := d.descList[len(d.descList)-1]
indexBlock := readStateIndexBlock(d.state, d.db, lastDesc.id)
bw, err := newBlockWriter(indexBlock, lastDesc, lastDesc.max)
bw, err := newBlockWriter(indexBlock, lastDesc, lastDesc.max, d.bitmapSize != 0)
if err != nil {
return err
}
@ -390,7 +387,8 @@ func (d *indexDeleter) finish(batch ethdb.Batch) {
if d.empty() {
deleteStateIndex(d.state, batch)
} else {
buf := make([]byte, 0, indexBlockDescSize*len(d.descList))
size := indexBlockDescSize + d.bitmapSize
buf := make([]byte, 0, size*len(d.descList))
for _, desc := range d.descList {
buf = append(buf, desc.encode()...)
}

View file

@ -17,6 +17,7 @@
package pathdb
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
@ -26,23 +27,27 @@ import (
)
const (
indexBlockDescSize = 14 // The size of index block descriptor
indexBlockEntriesCap = 4096 // The maximum number of entries can be grouped in a block
indexBlockRestartLen = 256 // The restart interval length of index block
historyIndexBatch = 8 * 1024 * 1024 // The number of state history indexes for constructing or deleting as batch
indexBlockDescSize = 14 // The size of index block descriptor
indexBlockMaxSize = 4096 // The maximum size of a single index block
indexBlockRestartLen = 256 // The restart interval length of index block
)
// indexBlockDesc represents a descriptor for an index block, which contains a
// list of state mutation records associated with a specific state (either an
// account or a storage slot).
type indexBlockDesc struct {
max uint64 // The maximum state ID retained within the block
entries uint16 // The number of state mutation records retained within the block
id uint32 // The id of the index block
max uint64 // The maximum state ID retained within the block
entries uint16 // The number of state mutation records retained within the block
id uint32 // The id of the index block
extBitmap []byte // Optional fixed-size bitmap for the included extension elements
}
func newIndexBlockDesc(id uint32) *indexBlockDesc {
return &indexBlockDesc{id: id}
func newIndexBlockDesc(id uint32, bitmapSize int) *indexBlockDesc {
var bitmap []byte
if bitmapSize > 0 {
bitmap = make([]byte, bitmapSize)
}
return &indexBlockDesc{id: id, extBitmap: bitmap}
}
// empty indicates whether the block is empty with no element retained.
@ -50,26 +55,33 @@ func (d *indexBlockDesc) empty() bool {
return d.entries == 0
}
// full indicates whether the number of elements in the block exceeds the
// preconfigured limit.
func (d *indexBlockDesc) full() bool {
return d.entries >= indexBlockEntriesCap
}
// encode packs index block descriptor into byte stream.
func (d *indexBlockDesc) encode() []byte {
var buf [indexBlockDescSize]byte
buf := make([]byte, indexBlockDescSize+len(d.extBitmap))
binary.BigEndian.PutUint64(buf[0:8], d.max)
binary.BigEndian.PutUint16(buf[8:10], d.entries)
binary.BigEndian.PutUint32(buf[10:14], d.id)
copy(buf[indexBlockDescSize:], d.extBitmap)
return buf[:]
}
// decode unpacks index block descriptor from byte stream.
// decode unpacks index block descriptor from byte stream. It's safe to mutate
// the provided byte stream after the function call.
func (d *indexBlockDesc) decode(blob []byte) {
d.max = binary.BigEndian.Uint64(blob[:8])
d.entries = binary.BigEndian.Uint16(blob[8:10])
d.id = binary.BigEndian.Uint32(blob[10:14])
d.extBitmap = bytes.Clone(blob[indexBlockDescSize:])
}
// copy returns a deep-copied object.
func (d *indexBlockDesc) copy() *indexBlockDesc {
return &indexBlockDesc{
max: d.max,
entries: d.entries,
id: d.id,
extBitmap: bytes.Clone(d.extBitmap),
}
}
// parseIndexBlock parses the index block with the supplied byte stream.
@ -97,20 +109,38 @@ func (d *indexBlockDesc) decode(blob []byte) {
// A uint16 can cover offsets in the range [0, 65536), which is more than enough
// to store 4096 integers.
//
// Each chunk begins with the full value of the first integer, followed by
// subsequent integers representing the differences between the current value
// and the preceding one. Integers are encoded with variable-size for best
// storage efficiency. Each chunk can be illustrated as below.
// Each chunk begins with a full integer value for the first element, followed
// by subsequent integers encoded as differences (deltas) from their preceding
// values. All integers use variable-length encoding for optimal space efficiency.
//
// Restart ---> +----------------+
// | Full integer |
// +----------------+
// | Diff with prev |
// +----------------+
// | ... |
// +----------------+
// | Diff with prev |
// +----------------+
// In the updated format, each element in the chunk may optionally include an
// "extension" section. If an extension is present, it starts with a var-size
// integer indicating the length of the remaining extension payload, followed by
// that many bytes. If no extension is present, the element format is identical
// to the original version (i.e., only the integer or delta value is encoded).
//
// In the trienode history index, the extension field contains the list of
// trie node IDs that fall within this range. For the given state transition,
// these IDs represent the specific nodes in this range that were mutated.
//
// Whether an element includes an extension is determined by the block reader
// based on the specification. Conceptually, a chunk is structured as:
//
// Restart ---> +----------------+
// | Full integer |
// +----------------+
// | (Extension?) |
// +----------------+
// | Diff with prev |
// +----------------+
// | (Extension?) |
// +----------------+
// | ... |
// +----------------+
// | Diff with prev |
// +----------------+
// | (Extension?) |
// +----------------+
//
// Empty index block is regarded as invalid.
func parseIndexBlock(blob []byte) ([]uint16, []byte, error) {
@ -148,24 +178,26 @@ func parseIndexBlock(blob []byte) ([]uint16, []byte, error) {
type blockReader struct {
restarts []uint16
data []byte
hasExt bool
}
// newBlockReader constructs the block reader with the supplied block data.
func newBlockReader(blob []byte) (*blockReader, error) {
func newBlockReader(blob []byte, hasExt bool) (*blockReader, error) {
restarts, data, err := parseIndexBlock(blob)
if err != nil {
return nil, err
}
return &blockReader{
restarts: restarts,
data: data, // safe to own the slice
data: data, // safe to own the slice
hasExt: hasExt, // flag whether extension should be resolved
}, nil
}
// readGreaterThan locates the first element in the block that is greater than
// the specified value. If no such element is found, MaxUint64 is returned.
func (br *blockReader) readGreaterThan(id uint64) (uint64, error) {
it := newBlockIterator(br.data, br.restarts)
it := br.newIterator(nil)
found := it.SeekGT(id)
if err := it.Error(); err != nil {
return 0, err
@ -180,17 +212,19 @@ type blockWriter struct {
desc *indexBlockDesc // Descriptor of the block
restarts []uint16 // Offsets into the data slice, marking the start of each section
data []byte // Aggregated encoded data slice
hasExt bool // Flag whether the extension field for each element exists
}
// newBlockWriter constructs a block writer. In addition to the existing data
// and block description, it takes an element ID and prunes all existing elements
// above that ID. It's essential as the recovery mechanism after unclean shutdown
// during the history indexing.
func newBlockWriter(blob []byte, desc *indexBlockDesc, limit uint64) (*blockWriter, error) {
func newBlockWriter(blob []byte, desc *indexBlockDesc, limit uint64, hasExt bool) (*blockWriter, error) {
if len(blob) == 0 {
return &blockWriter{
desc: desc,
data: make([]byte, 0, 1024),
desc: desc,
data: make([]byte, 0, 1024),
hasExt: hasExt,
}, nil
}
restarts, data, err := parseIndexBlock(blob)
@ -201,6 +235,7 @@ func newBlockWriter(blob []byte, desc *indexBlockDesc, limit uint64) (*blockWrit
desc: desc,
restarts: restarts,
data: data, // safe to own the slice
hasExt: hasExt,
}
var trimmed int
for !writer.empty() && writer.last() > limit {
@ -215,9 +250,26 @@ func newBlockWriter(blob []byte, desc *indexBlockDesc, limit uint64) (*blockWrit
return writer, nil
}
// setBitmap applies the given extension elements into the bitmap.
func (b *blockWriter) setBitmap(ext []uint16) {
for _, n := range ext {
// Node ID zero is intentionally filtered out. Any element in this range
// can indicate that the sub-tree's root node was mutated, so storing zero
// is redundant and saves one byte for bitmap.
if n != 0 {
setBit(b.desc.extBitmap, int(n-1))
}
}
}
// append adds a new element to the block. The new element must be greater than
// the previous one. The provided ID is assumed to always be greater than 0.
func (b *blockWriter) append(id uint64) error {
//
// ext refers to the optional extension field attached to the appended element.
// This extension mechanism is used by trie-node history and represents a list of
// trie node IDs that fall within the range covered by the index element
// (typically corresponding to a sub-trie in trie-node history).
func (b *blockWriter) append(id uint64, ext []uint16) error {
if id == 0 {
return errors.New("invalid zero id")
}
@ -244,13 +296,29 @@ func (b *blockWriter) append(id uint64) error {
// element.
b.data = binary.AppendUvarint(b.data, id-b.desc.max)
}
// Extension validation
if (len(ext) == 0) != !b.hasExt {
if len(ext) == 0 {
return errors.New("missing extension")
}
return errors.New("unexpected extension")
}
// Append the extension if it is not nil. The extension is prefixed with a
// length indicator, and the block reader MUST understand this scheme and
// decode the extension accordingly.
if len(ext) > 0 {
b.setBitmap(ext)
enc := encodeIDs(ext)
b.data = binary.AppendUvarint(b.data, uint64(len(enc)))
b.data = append(b.data, enc...)
}
b.desc.entries++
b.desc.max = id
return nil
}
// scanSection traverses the specified section and terminates if fn returns true.
func (b *blockWriter) scanSection(section int, fn func(uint64, int) bool) {
func (b *blockWriter) scanSection(section int, fn func(uint64, int, []uint16) bool) error {
var (
value uint64
start = int(b.restarts[section])
@ -269,28 +337,47 @@ func (b *blockWriter) scanSection(section int, fn func(uint64, int) bool) {
} else {
value += x
}
if fn(value, pos) {
return
// Resolve the extension if exists
var (
err error
ext []uint16
extLen int
)
if b.hasExt {
l, ln := binary.Uvarint(b.data[pos+n:])
extLen = ln + int(l)
ext, err = decodeIDs(b.data[pos+n+ln : pos+n+extLen])
}
if err != nil {
return err
}
if fn(value, pos, ext) {
return nil
}
// Shift to next position
pos += n
pos += extLen
}
return nil
}
// sectionLast returns the last element in the specified section.
func (b *blockWriter) sectionLast(section int) uint64 {
func (b *blockWriter) sectionLast(section int) (uint64, error) {
var n uint64
b.scanSection(section, func(v uint64, _ int) bool {
if err := b.scanSection(section, func(v uint64, _ int, _ []uint16) bool {
n = v
return false
})
return n
}); err != nil {
return 0, err
}
return n, nil
}
// sectionSearch looks up the specified value in the given section,
// the position and the preceding value will be returned if found.
// It assumes that the preceding element exists in the section.
func (b *blockWriter) sectionSearch(section int, n uint64) (found bool, prev uint64, pos int) {
b.scanSection(section, func(v uint64, p int) bool {
func (b *blockWriter) sectionSearch(section int, n uint64) (found bool, prev uint64, pos int, err error) {
if err := b.scanSection(section, func(v uint64, p int, _ []uint16) bool {
if n == v {
pos = p
found = true
@ -298,8 +385,24 @@ func (b *blockWriter) sectionSearch(section int, n uint64) (found bool, prev uin
}
prev = v
return false // continue iteration
})
return found, prev, pos
}); err != nil {
return false, 0, 0, err
}
return found, prev, pos, nil
}
// rebuildBitmap scans the entire block and rebuilds the bitmap.
func (b *blockWriter) rebuildBitmap() error {
clear(b.desc.extBitmap)
for i := 0; i < len(b.restarts); i++ {
if err := b.scanSection(i, func(v uint64, p int, ext []uint16) bool {
b.setBitmap(ext)
return false // continue iteration
}); err != nil {
return err
}
}
return nil
}
// pop removes the last element from the block. The assumption is held that block
@ -315,6 +418,7 @@ func (b *blockWriter) pop(id uint64) error {
if b.desc.entries == 1 {
b.desc.max = 0
b.desc.entries = 0
clear(b.desc.extBitmap)
b.restarts = nil
b.data = b.data[:0]
return nil
@ -324,28 +428,36 @@ func (b *blockWriter) pop(id uint64) error {
if b.desc.entries%indexBlockRestartLen == 1 {
b.data = b.data[:b.restarts[len(b.restarts)-1]]
b.restarts = b.restarts[:len(b.restarts)-1]
b.desc.max = b.sectionLast(len(b.restarts) - 1)
last, err := b.sectionLast(len(b.restarts) - 1)
if err != nil {
return err
}
b.desc.max = last
b.desc.entries -= 1
return nil
return b.rebuildBitmap()
}
// Look up the element preceding the one to be popped, in order to update
// the maximum element in the block.
found, prev, pos := b.sectionSearch(len(b.restarts)-1, id)
found, prev, pos, err := b.sectionSearch(len(b.restarts)-1, id)
if err != nil {
return err
}
if !found {
return fmt.Errorf("pop element is not found, last: %d, this: %d", b.desc.max, id)
}
b.desc.max = prev
b.data = b.data[:pos]
b.desc.entries -= 1
return nil
return b.rebuildBitmap()
}
func (b *blockWriter) empty() bool {
return b.desc.empty()
}
func (b *blockWriter) full() bool {
return b.desc.full()
func (b *blockWriter) estimateFull(ext []uint16) bool {
size := 8 + 2*len(ext)
return len(b.data)+size > indexBlockMaxSize
}
// last returns the last element in the block. It should only be called when

View file

@ -17,6 +17,7 @@
package pathdb
import (
"bytes"
"math"
"math/rand"
"slices"
@ -24,16 +25,36 @@ import (
"testing"
)
func randomExt(bitmapSize int, n int) []uint16 {
if bitmapSize == 0 {
return nil
}
var (
limit = bitmapSize * 8
extList []uint16
)
for i := 0; i < n; i++ {
extList = append(extList, uint16(rand.Intn(limit+1)))
}
return extList
}
func TestBlockReaderBasic(t *testing.T) {
testBlockReaderBasic(t, 0)
testBlockReaderBasic(t, 2)
testBlockReaderBasic(t, 34)
}
func testBlockReaderBasic(t *testing.T, bitmapSize int) {
elements := []uint64{
1, 5, 10, 11, 20,
}
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0), 0)
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0, bitmapSize), 0, bitmapSize != 0)
for i := 0; i < len(elements); i++ {
bw.append(elements[i])
bw.append(elements[i], randomExt(bitmapSize, 5))
}
br, err := newBlockReader(bw.finish())
br, err := newBlockReader(bw.finish(), bitmapSize != 0)
if err != nil {
t.Fatalf("Failed to construct the block reader, %v", err)
}
@ -60,18 +81,24 @@ func TestBlockReaderBasic(t *testing.T) {
}
func TestBlockReaderLarge(t *testing.T) {
testBlockReaderLarge(t, 0)
testBlockReaderLarge(t, 2)
testBlockReaderLarge(t, 34)
}
func testBlockReaderLarge(t *testing.T, bitmapSize int) {
var elements []uint64
for i := 0; i < 1000; i++ {
elements = append(elements, rand.Uint64())
}
slices.Sort(elements)
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0), 0)
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0, bitmapSize), 0, bitmapSize != 0)
for i := 0; i < len(elements); i++ {
bw.append(elements[i])
bw.append(elements[i], randomExt(bitmapSize, 5))
}
br, err := newBlockReader(bw.finish())
br, err := newBlockReader(bw.finish(), bitmapSize != 0)
if err != nil {
t.Fatalf("Failed to construct the block reader, %v", err)
}
@ -95,26 +122,32 @@ func TestBlockReaderLarge(t *testing.T) {
}
func TestBlockWriterBasic(t *testing.T) {
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0), 0)
testBlockWriteBasic(t, 0)
testBlockWriteBasic(t, 2)
testBlockWriteBasic(t, 34)
}
func testBlockWriteBasic(t *testing.T, bitmapSize int) {
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0, bitmapSize), 0, bitmapSize != 0)
if !bw.empty() {
t.Fatal("expected empty block")
}
bw.append(2)
if err := bw.append(1); err == nil {
bw.append(2, randomExt(bitmapSize, 5))
if err := bw.append(1, randomExt(bitmapSize, 5)); err == nil {
t.Fatal("out-of-order insertion is not expected")
}
var maxElem uint64
for i := 0; i < 10; i++ {
bw.append(uint64(i + 3))
bw.append(uint64(i+3), randomExt(bitmapSize, 5))
maxElem = uint64(i + 3)
}
bw, err := newBlockWriter(bw.finish(), newIndexBlockDesc(0), maxElem)
bw, err := newBlockWriter(bw.finish(), newIndexBlockDesc(0, bitmapSize), maxElem, bitmapSize != 0)
if err != nil {
t.Fatalf("Failed to construct the block writer, %v", err)
}
for i := 0; i < 10; i++ {
if err := bw.append(uint64(i + 100)); err != nil {
if err := bw.append(uint64(i+100), randomExt(bitmapSize, 5)); err != nil {
t.Fatalf("Failed to append value %d: %v", i, err)
}
}
@ -122,58 +155,38 @@ func TestBlockWriterBasic(t *testing.T) {
}
func TestBlockWriterWithLimit(t *testing.T) {
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0), 0)
testBlockWriterWithLimit(t, 0)
testBlockWriterWithLimit(t, 2)
testBlockWriterWithLimit(t, 34)
}
var maxElem uint64
for i := 0; i < indexBlockRestartLen*2; i++ {
bw.append(uint64(i + 1))
maxElem = uint64(i + 1)
}
func testBlockWriterWithLimit(t *testing.T, bitmapSize int) {
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0, bitmapSize), 0, bitmapSize != 0)
suites := []struct {
limit uint64
expMax uint64
}{
// nothing to truncate
{
maxElem, maxElem,
},
// truncate the last element
{
maxElem - 1, maxElem - 1,
},
// truncation around the restart boundary
{
uint64(indexBlockRestartLen + 1),
uint64(indexBlockRestartLen + 1),
},
// truncation around the restart boundary
{
uint64(indexBlockRestartLen),
uint64(indexBlockRestartLen),
},
{
uint64(1), uint64(1),
},
// truncate the entire block, it's in theory invalid
{
uint64(0), uint64(0),
},
var bitmaps [][]byte
for i := 0; i < indexBlockRestartLen+2; i++ {
bw.append(uint64(i+1), randomExt(bitmapSize, 5))
bitmaps = append(bitmaps, bytes.Clone(bw.desc.extBitmap))
}
for i, suite := range suites {
desc := *bw.desc
block, err := newBlockWriter(bw.finish(), &desc, suite.limit)
for i := 0; i < indexBlockRestartLen+2; i++ {
limit := uint64(i + 1)
desc := bw.desc.copy()
block, err := newBlockWriter(bytes.Clone(bw.finish()), desc, limit, bitmapSize != 0)
if err != nil {
t.Fatalf("Failed to construct the block writer, %v", err)
}
if block.desc.max != suite.expMax {
t.Fatalf("Test %d, unexpected max value, got %d, want %d", i, block.desc.max, suite.expMax)
if block.desc.max != limit {
t.Fatalf("Test %d, unexpected max value, got %d, want %d", i, block.desc.max, limit)
}
if !bytes.Equal(desc.extBitmap, bitmaps[i]) {
t.Fatalf("Test %d, unexpected bitmap, got: %v, want: %v", i, block.desc.extBitmap, bitmaps[i])
}
// Re-fill the elements
var maxElem uint64
for elem := suite.limit + 1; elem < indexBlockRestartLen*4; elem++ {
if err := block.append(elem); err != nil {
for elem := limit + 1; elem < indexBlockRestartLen+4; elem++ {
if err := block.append(elem, randomExt(bitmapSize, 5)); err != nil {
t.Fatalf("Failed to append value %d: %v", elem, err)
}
maxElem = elem
@ -185,9 +198,15 @@ func TestBlockWriterWithLimit(t *testing.T) {
}
func TestBlockWriterDelete(t *testing.T) {
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0), 0)
testBlockWriterDelete(t, 0)
testBlockWriterDelete(t, 2)
testBlockWriterDelete(t, 34)
}
func testBlockWriterDelete(t *testing.T, bitmapSize int) {
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0, bitmapSize), 0, bitmapSize != 0)
for i := 0; i < 10; i++ {
bw.append(uint64(i + 1))
bw.append(uint64(i+1), randomExt(bitmapSize, 5))
}
// Pop unknown id, the request should be rejected
if err := bw.pop(100); err == nil {
@ -209,12 +228,18 @@ func TestBlockWriterDelete(t *testing.T) {
}
func TestBlcokWriterDeleteWithData(t *testing.T) {
testBlcokWriterDeleteWithData(t, 0)
testBlcokWriterDeleteWithData(t, 2)
testBlcokWriterDeleteWithData(t, 34)
}
func testBlcokWriterDeleteWithData(t *testing.T, bitmapSize int) {
elements := []uint64{
1, 5, 10, 11, 20,
}
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0), 0)
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0, bitmapSize), 0, bitmapSize != 0)
for i := 0; i < len(elements); i++ {
bw.append(elements[i])
bw.append(elements[i], randomExt(bitmapSize, 5))
}
// Re-construct the block writer with data
@ -223,7 +248,10 @@ func TestBlcokWriterDeleteWithData(t *testing.T) {
max: 20,
entries: 5,
}
bw, err := newBlockWriter(bw.finish(), desc, elements[len(elements)-1])
if bitmapSize > 0 {
desc.extBitmap = make([]byte, bitmapSize)
}
bw, err := newBlockWriter(bw.finish(), desc, elements[len(elements)-1], bitmapSize != 0)
if err != nil {
t.Fatalf("Failed to construct block writer %v", err)
}
@ -234,7 +262,7 @@ func TestBlcokWriterDeleteWithData(t *testing.T) {
newTail := elements[i-1]
// Ensure the element can still be queried with no issue
br, err := newBlockReader(bw.finish())
br, err := newBlockReader(bw.finish(), bitmapSize != 0)
if err != nil {
t.Fatalf("Failed to construct the block reader, %v", err)
}
@ -266,29 +294,60 @@ func TestBlcokWriterDeleteWithData(t *testing.T) {
}
func TestCorruptedIndexBlock(t *testing.T) {
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0), 0)
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0, 0), 0, false)
var maxElem uint64
for i := 0; i < 10; i++ {
bw.append(uint64(i + 1))
bw.append(uint64(i+1), nil)
maxElem = uint64(i + 1)
}
buf := bw.finish()
// Mutate the buffer manually
buf[len(buf)-1]++
_, err := newBlockWriter(buf, newIndexBlockDesc(0), maxElem)
_, err := newBlockWriter(buf, newIndexBlockDesc(0, 0), maxElem, false)
if err == nil {
t.Fatal("Corrupted index block data is not detected")
}
}
// BenchmarkParseIndexBlock benchmarks the performance of parseIndexBlock.
//
// goos: darwin
// goarch: arm64
// pkg: github.com/ethereum/go-ethereum/triedb/pathdb
// cpu: Apple M1 Pro
// BenchmarkParseIndexBlock
// BenchmarkParseIndexBlock-8 35829495 34.16 ns/op
func BenchmarkParseIndexBlock(b *testing.B) {
// Generate a realistic index block blob
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0), 0)
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0, 0), 0, false)
for i := 0; i < 4096; i++ {
bw.append(uint64(i * 2))
bw.append(uint64(i*2), nil)
}
blob := bw.finish()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, err := parseIndexBlock(blob)
if err != nil {
b.Fatalf("parseIndexBlock failed: %v", err)
}
}
}
// goos: darwin
// goarch: arm64
// pkg: github.com/ethereum/go-ethereum/triedb/pathdb
// cpu: Apple M1 Pro
// BenchmarkParseIndexBlockWithExt
// BenchmarkParseIndexBlockWithExt-8 35773242 33.72 ns/op
func BenchmarkParseIndexBlockWithExt(b *testing.B) {
// Generate a realistic index block blob
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0, 34), 0, true)
for i := 0; i < 4096; i++ {
id, ext := uint64(i*2), randomExt(34, 3)
bw.append(id, ext)
}
blob := bw.finish()
@ -302,21 +361,58 @@ func BenchmarkParseIndexBlock(b *testing.B) {
}
// BenchmarkBlockWriterAppend benchmarks the performance of indexblock.writer
//
// goos: darwin
// goarch: arm64
// pkg: github.com/ethereum/go-ethereum/triedb/pathdb
// cpu: Apple M1 Pro
// BenchmarkBlockWriterAppend
// BenchmarkBlockWriterAppend-8 293611083 4.113 ns/op 3 B/op 0 allocs/op
func BenchmarkBlockWriterAppend(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
var blockID uint32
desc := newIndexBlockDesc(blockID)
writer, _ := newBlockWriter(nil, desc, 0)
desc := newIndexBlockDesc(blockID, 0)
writer, _ := newBlockWriter(nil, desc, 0, false)
for i := 0; i < b.N; i++ {
if writer.full() {
if writer.estimateFull(nil) {
blockID += 1
desc = newIndexBlockDesc(blockID)
writer, _ = newBlockWriter(nil, desc, 0)
desc = newIndexBlockDesc(blockID, 0)
writer, _ = newBlockWriter(nil, desc, 0, false)
}
if err := writer.append(writer.desc.max + 1); err != nil {
if err := writer.append(writer.desc.max+1, nil); err != nil {
b.Error(err)
}
}
}
// goos: darwin
// goarch: arm64
// pkg: github.com/ethereum/go-ethereum/triedb/pathdb
// cpu: Apple M1 Pro
// BenchmarkBlockWriterAppendWithExt
// BenchmarkBlockWriterAppendWithExt-8 11123844 103.6 ns/op 42 B/op 2 allocs/op
func BenchmarkBlockWriterAppendWithExt(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
var (
bitmapSize = 34
blockID uint32
)
desc := newIndexBlockDesc(blockID, bitmapSize)
writer, _ := newBlockWriter(nil, desc, 0, true)
for i := 0; i < b.N; i++ {
ext := randomExt(bitmapSize, 3)
if writer.estimateFull(ext) {
blockID += 1
desc = newIndexBlockDesc(blockID, bitmapSize)
writer, _ = newBlockWriter(nil, desc, 0, true)
}
if err := writer.append(writer.desc.max+1, ext); err != nil {
b.Error(err)
}
}

View file

@ -40,31 +40,133 @@ type HistoryIndexIterator interface {
Error() error
}
// extFilter provides utilities for filtering index entries based on their
// extension field.
//
// It supports two primary operations:
//
// - determine whether a given target node ID or any of its descendants
// appears explicitly in the extension list.
//
// - determine whether a given target node ID or any of its descendants
// is marked in the extension bitmap.
//
// Together, these checks allow callers to efficiently filter out the irrelevant
// index entries during the lookup.
type extFilter uint16
// exists takes the entire extension field in the index block and determines
// whether the target ID or its descendants appears. Note, any of descendant
// can implicitly mean the presence of ancestor.
func (f extFilter) exists(ext []byte) (bool, error) {
fn := uint16(f)
list, err := decodeIDs(ext)
if err != nil {
return false, err
}
for _, elem := range list {
if elem == fn {
return true, nil
}
if isAncestor(fn, elem) {
return true, nil
}
}
return false, nil
}
const (
// bitmapBytesTwoLevels is the size of the bitmap for two levels of the
// 16-ary tree (16 nodes total, excluding the root).
bitmapBytesTwoLevels = 2
// bitmapBytesThreeLevels is the size of the bitmap for three levels of
// the 16-ary tree (272 nodes total, excluding the root).
bitmapBytesThreeLevels = 34
// bitmapElementThresholdTwoLevels is the total number of elements in the
// two levels of a 16-ary tree (16 nodes total, excluding the root).
bitmapElementThresholdTwoLevels = 16
// bitmapElementThresholdThreeLevels is the total number of elements in the
// two levels of a 16-ary tree (16 nodes total, excluding the root).
bitmapElementThresholdThreeLevels = bitmapElementThresholdTwoLevels + 16*16
)
// contains takes the bitmap from the block metadata and determines whether the
// target ID or its descendants is marked in the bitmap. Note, any of descendant
// can implicitly mean the presence of ancestor.
func (f extFilter) contains(bitmap []byte) (bool, error) {
id := int(f)
if id == 0 {
return true, nil
}
n := id - 1 // apply the position shift for excluding root node
switch len(bitmap) {
case 0:
// Bitmap is not available, return "false positive"
return true, nil
case bitmapBytesTwoLevels:
// Bitmap for 2-level trie with at most 16 elements inside
if n >= bitmapElementThresholdTwoLevels {
return false, fmt.Errorf("invalid extension filter %d for 2 bytes bitmap", id)
}
return isBitSet(bitmap, n), nil
case bitmapBytesThreeLevels:
// Bitmap for 3-level trie with at most 16+16*16 elements inside
if n >= bitmapElementThresholdThreeLevels {
return false, fmt.Errorf("invalid extension filter %d for 34 bytes bitmap", id)
} else if n >= bitmapElementThresholdTwoLevels {
return isBitSet(bitmap, n), nil
} else {
// Check the element itself first
if isBitSet(bitmap, n) {
return true, nil
}
// Check descendants: the presence of any descendant implicitly
// represents a mutation of its ancestor.
return bitmap[2+2*n] != 0 || bitmap[3+2*n] != 0, nil
}
default:
return false, fmt.Errorf("unsupported bitmap size %d", len(bitmap))
}
}
// blockIterator is the iterator to traverse the indices within a single block.
type blockIterator struct {
// immutable fields
data []byte // Reference to the data segment within the block reader
restarts []uint16 // Offsets pointing to the restart sections within the data
hasExt bool // Flag whether the extension is included in the data
// Optional extension filter
filter *extFilter // Filters index entries based on the extension field.
// mutable fields
id uint64 // ID of the element at the iterators current position
ext []byte // Extension field of the element at the iterators current position
dataPtr int // Current read position within the data slice
restartPtr int // Index of the restart section where the iterator is currently positioned
exhausted bool // Flag whether the iterator has been exhausted
err error // Accumulated error during the traversal
}
func newBlockIterator(data []byte, restarts []uint16) *blockIterator {
func (br *blockReader) newIterator(filter *extFilter) *blockIterator {
it := &blockIterator{
data: data, // hold the slice directly with no deep copy
restarts: restarts, // hold the slice directly with no deep copy
data: br.data, // hold the slice directly with no deep copy
restarts: br.restarts, // hold the slice directly with no deep copy
hasExt: br.hasExt, // flag whether the extension should be resolved
filter: filter, // optional extension filter
}
it.reset()
return it
}
func (it *blockIterator) set(dataPtr int, restartPtr int, id uint64) {
func (it *blockIterator) set(dataPtr int, restartPtr int, id uint64, ext []byte) {
it.id = id
it.ext = ext
it.dataPtr = dataPtr
it.restartPtr = restartPtr
it.exhausted = dataPtr == len(it.data)
@ -79,6 +181,8 @@ func (it *blockIterator) setErr(err error) {
func (it *blockIterator) reset() {
it.id = 0
it.ext = nil
it.dataPtr = -1
it.restartPtr = -1
it.exhausted = false
@ -90,12 +194,26 @@ func (it *blockIterator) reset() {
}
}
// SeekGT moves the iterator to the first element whose id is greater than the
func (it *blockIterator) resolveExt(pos int) ([]byte, int, error) {
if !it.hasExt {
return nil, 0, nil
}
length, n := binary.Uvarint(it.data[pos:])
if n <= 0 {
return nil, 0, fmt.Errorf("too short for extension, pos: %d, datalen: %d", pos, len(it.data))
}
if len(it.data[pos+n:]) < int(length) {
return nil, 0, fmt.Errorf("too short for extension, pos: %d, length: %d, datalen: %d", pos, length, len(it.data))
}
return it.data[pos+n : pos+n+int(length)], n + int(length), nil
}
// seekGT moves the iterator to the first element whose id is greater than the
// given number. It returns whether such element exists.
//
// Note, this operation will unset the exhausted status and subsequent traversal
// is allowed.
func (it *blockIterator) SeekGT(id uint64) bool {
func (it *blockIterator) seekGT(id uint64) bool {
if it.err != nil {
return false
}
@ -112,11 +230,20 @@ func (it *blockIterator) SeekGT(id uint64) bool {
return false
}
if index == 0 {
item, n := binary.Uvarint(it.data[it.restarts[0]:])
pos := int(it.restarts[0])
item, n := binary.Uvarint(it.data[pos:])
if n <= 0 {
it.setErr(fmt.Errorf("failed to decode item at pos %d", it.restarts[0]))
return false
}
pos = pos + n
// If the restart size is 1, then the restart pointer shouldn't be 0.
// It's not practical and should be denied in the first place.
it.set(int(it.restarts[0])+n, 0, item)
ext, shift, err := it.resolveExt(pos)
if err != nil {
it.setErr(err)
return false
}
it.set(pos+shift, 0, item, ext)
return true
}
var (
@ -154,11 +281,18 @@ func (it *blockIterator) SeekGT(id uint64) bool {
}
pos += n
ext, shift, err := it.resolveExt(pos)
if err != nil {
it.setErr(err)
return false
}
pos += shift
if result > id {
if pos == limit {
it.set(pos, restartIndex+1, result)
it.set(pos, restartIndex+1, result, ext)
} else {
it.set(pos, restartIndex, result)
it.set(pos, restartIndex, result, ext)
}
return true
}
@ -170,8 +304,45 @@ func (it *blockIterator) SeekGT(id uint64) bool {
}
// The element which is the first one greater than the specified id
// is exactly the one located at the restart point.
item, n := binary.Uvarint(it.data[it.restarts[index]:])
it.set(int(it.restarts[index])+n, index, item)
pos = int(it.restarts[index])
item, n := binary.Uvarint(it.data[pos:])
if n <= 0 {
it.setErr(fmt.Errorf("failed to decode item at pos %d", it.restarts[index]))
return false
}
pos = pos + n
ext, shift, err := it.resolveExt(pos)
if err != nil {
it.setErr(err)
return false
}
it.set(pos+shift, index, item, ext)
return true
}
// SeekGT implements HistoryIndexIterator, is the wrapper of the seekGT with
// optional extension filter logic applied.
func (it *blockIterator) SeekGT(id uint64) bool {
if !it.seekGT(id) {
return false
}
if it.filter == nil {
return true
}
for {
found, err := it.filter.exists(it.ext)
if err != nil {
it.setErr(err)
return false
}
if found {
break
}
if !it.next() {
return false
}
}
return true
}
@ -183,10 +354,9 @@ func (it *blockIterator) init() {
it.restartPtr = 0
}
// Next implements the HistoryIndexIterator, moving the iterator to the next
// element. If the iterator has been exhausted, and boolean with false should
// be returned.
func (it *blockIterator) Next() bool {
// next moves the iterator to the next element. If the iterator has been exhausted,
// and boolean with false should be returned.
func (it *blockIterator) next() bool {
if it.exhausted || it.err != nil {
return false
}
@ -198,7 +368,6 @@ func (it *blockIterator) Next() bool {
it.setErr(fmt.Errorf("failed to decode item at pos %d", it.dataPtr))
return false
}
var val uint64
if it.dataPtr == int(it.restarts[it.restartPtr]) {
val = v
@ -206,16 +375,48 @@ func (it *blockIterator) Next() bool {
val = it.id + v
}
// Decode the extension field
ext, shift, err := it.resolveExt(it.dataPtr + n)
if err != nil {
it.setErr(err)
return false
}
// Move to the next restart section if the data pointer crosses the boundary
nextRestartPtr := it.restartPtr
if it.restartPtr < len(it.restarts)-1 && it.dataPtr+n == int(it.restarts[it.restartPtr+1]) {
if it.restartPtr < len(it.restarts)-1 && it.dataPtr+n+shift == int(it.restarts[it.restartPtr+1]) {
nextRestartPtr = it.restartPtr + 1
}
it.set(it.dataPtr+n, nextRestartPtr, val)
it.set(it.dataPtr+n+shift, nextRestartPtr, val, ext)
return true
}
// Next implements the HistoryIndexIterator, moving the iterator to the next
// element. It's a wrapper of next with optional extension filter logic applied.
func (it *blockIterator) Next() bool {
if !it.next() {
return false
}
if it.filter == nil {
return true
}
for {
found, err := it.filter.exists(it.ext)
if err != nil {
it.setErr(err)
return false
}
if found {
break
}
if !it.next() {
return false
}
}
return true
}
// ID implements HistoryIndexIterator, returning the id of the element where the
// iterator is positioned at.
func (it *blockIterator) ID() uint64 {
@ -226,15 +427,15 @@ func (it *blockIterator) ID() uint64 {
// Exhausting all the elements is not considered to be an error.
func (it *blockIterator) Error() error { return it.err }
// blockLoader defines the method to retrieve the specific block for reading.
type blockLoader func(id uint32) (*blockReader, error)
// indexIterator is an iterator to traverse the history indices belonging to the
// specific state entry.
type indexIterator struct {
// immutable fields
descList []*indexBlockDesc
loader blockLoader
reader *indexReader
// Optional extension filter
filter *extFilter
// mutable fields
blockIt *blockIterator
@ -243,10 +444,26 @@ type indexIterator struct {
err error
}
func newIndexIterator(descList []*indexBlockDesc, loader blockLoader) *indexIterator {
// newBlockIter initializes the block iterator with the specified block ID.
func (r *indexReader) newBlockIter(id uint32, filter *extFilter) (*blockIterator, error) {
br, ok := r.readers[id]
if !ok {
var err error
br, err = newBlockReader(readStateIndexBlock(r.state, r.db, id), r.bitmapSize != 0)
if err != nil {
return nil, err
}
r.readers[id] = br
}
return br.newIterator(filter), nil
}
// newIterator initializes the index iterator with the specified extension filter.
func (r *indexReader) newIterator(filter *extFilter) *indexIterator {
it := &indexIterator{
descList: descList,
loader: loader,
descList: r.descList,
reader: r,
filter: filter,
}
it.reset()
return it
@ -271,16 +488,32 @@ func (it *indexIterator) reset() {
}
func (it *indexIterator) open(blockPtr int) error {
id := it.descList[blockPtr].id
br, err := it.loader(id)
blockIt, err := it.reader.newBlockIter(it.descList[blockPtr].id, it.filter)
if err != nil {
return err
}
it.blockIt = newBlockIterator(br.data, br.restarts)
it.blockIt = blockIt
it.blockPtr = blockPtr
return nil
}
func (it *indexIterator) applyFilter(index int) (int, error) {
if it.filter == nil {
return index, nil
}
for index < len(it.descList) {
found, err := it.filter.contains(it.descList[index].extBitmap)
if err != nil {
return 0, err
}
if found {
break
}
index++
}
return index, nil
}
// SeekGT moves the iterator to the first element whose id is greater than the
// given number. It returns whether such element exists.
//
@ -293,6 +526,11 @@ func (it *indexIterator) SeekGT(id uint64) bool {
index := sort.Search(len(it.descList), func(i int) bool {
return id < it.descList[i].max
})
index, err := it.applyFilter(index)
if err != nil {
it.setErr(err)
return false
}
if index == len(it.descList) {
return false
}
@ -304,7 +542,13 @@ func (it *indexIterator) SeekGT(id uint64) bool {
return false
}
}
return it.blockIt.SeekGT(id)
// Terminate if the element which is greater than the id can be found in the
// last block; otherwise move to the next block. It may happen that all the
// target elements in this block are all less than id.
if it.blockIt.SeekGT(id) {
return true
}
return it.Next()
}
func (it *indexIterator) init() error {
@ -325,15 +569,23 @@ func (it *indexIterator) Next() bool {
it.setErr(err)
return false
}
if it.blockIt.Next() {
return true
}
if it.blockPtr == len(it.descList)-1 {
it.blockPtr++
index, err := it.applyFilter(it.blockPtr)
if err != nil {
it.setErr(err)
return false
}
it.blockPtr = index
if it.blockPtr == len(it.descList) {
it.exhausted = true
return false
}
if err := it.open(it.blockPtr + 1); err != nil {
if err := it.open(it.blockPtr); err != nil {
it.setErr(err)
return false
}

View file

@ -19,7 +19,9 @@ package pathdb
import (
"errors"
"fmt"
"maps"
"math/rand"
"slices"
"sort"
"testing"
@ -28,12 +30,30 @@ import (
"github.com/ethereum/go-ethereum/ethdb"
)
func makeTestIndexBlock(count int) ([]byte, []uint64) {
func checkExt(f *extFilter, ext []uint16) bool {
if f == nil {
return true
}
fn := uint16(*f)
for _, n := range ext {
if n == fn {
return true
}
if isAncestor(fn, n) {
return true
}
}
return false
}
func makeTestIndexBlock(count int, bitmapSize int) ([]byte, []uint64, [][]uint16) {
var (
marks = make(map[uint64]bool)
elements []uint64
elements = make([]uint64, 0, count)
extList = make([][]uint16, 0, count)
)
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0), 0)
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0, bitmapSize), 0, bitmapSize != 0)
for i := 0; i < count; i++ {
n := uint64(rand.Uint32())
if marks[n] {
@ -45,17 +65,20 @@ func makeTestIndexBlock(count int) ([]byte, []uint64) {
sort.Slice(elements, func(i, j int) bool { return elements[i] < elements[j] })
for i := 0; i < len(elements); i++ {
bw.append(elements[i])
ext := randomExt(bitmapSize, 5)
extList = append(extList, ext)
bw.append(elements[i], ext)
}
data := bw.finish()
return data, elements
return data, elements, extList
}
func makeTestIndexBlocks(db ethdb.KeyValueStore, stateIdent stateIdent, count int) []uint64 {
func makeTestIndexBlocks(db ethdb.KeyValueStore, stateIdent stateIdent, count int, bitmapSize int) ([]uint64, [][]uint16) {
var (
marks = make(map[uint64]bool)
elements []uint64
extList [][]uint16
)
for i := 0; i < count; i++ {
n := uint64(rand.Uint32())
@ -67,15 +90,17 @@ func makeTestIndexBlocks(db ethdb.KeyValueStore, stateIdent stateIdent, count in
}
sort.Slice(elements, func(i, j int) bool { return elements[i] < elements[j] })
iw, _ := newIndexWriter(db, stateIdent, 0)
iw, _ := newIndexWriter(db, stateIdent, 0, bitmapSize)
for i := 0; i < len(elements); i++ {
iw.append(elements[i])
ext := randomExt(bitmapSize, 5)
extList = append(extList, ext)
iw.append(elements[i], ext)
}
batch := db.NewBatch()
iw.finish(batch)
batch.Write()
return elements
return elements, extList
}
func checkSeekGT(it HistoryIndexIterator, input uint64, exp bool, expVal uint64) error {
@ -113,43 +138,40 @@ func checkNext(it HistoryIndexIterator, values []uint64) error {
return it.Error()
}
func TestBlockIteratorSeekGT(t *testing.T) {
/* 0-size index block is not allowed
data, elements := makeTestIndexBlock(0)
testBlockIterator(t, data, elements)
*/
data, elements := makeTestIndexBlock(1)
testBlockIterator(t, data, elements)
data, elements = makeTestIndexBlock(indexBlockRestartLen)
testBlockIterator(t, data, elements)
data, elements = makeTestIndexBlock(3 * indexBlockRestartLen)
testBlockIterator(t, data, elements)
data, elements = makeTestIndexBlock(indexBlockEntriesCap)
testBlockIterator(t, data, elements)
}
func testBlockIterator(t *testing.T, data []byte, elements []uint64) {
br, err := newBlockReader(data)
if err != nil {
t.Fatalf("Failed to open the block for reading, %v", err)
func verifySeekGT(t *testing.T, elements []uint64, ext [][]uint16, newIter func(filter *extFilter) HistoryIndexIterator) {
set := make(map[extFilter]bool)
for _, extList := range ext {
for _, f := range extList {
set[extFilter(f)] = true
}
}
it := newBlockIterator(br.data, br.restarts)
filters := slices.Collect(maps.Keys(set))
for i := 0; i < 128; i++ {
var filter *extFilter
if rand.Intn(2) == 0 && len(filters) > 0 {
filter = &filters[rand.Intn(len(filters))]
} else {
filter = nil
}
var input uint64
if rand.Intn(2) == 0 {
input = elements[rand.Intn(len(elements))]
} else {
input = uint64(rand.Uint32())
}
index := sort.Search(len(elements), func(i int) bool {
return elements[i] > input
})
for index < len(elements) {
if checkExt(filter, ext[index]) {
break
}
index++
}
var (
exp bool
expVal uint64
@ -160,10 +182,17 @@ func testBlockIterator(t *testing.T, data []byte, elements []uint64) {
} else {
exp = true
expVal = elements[index]
if index < len(elements) {
remains = elements[index+1:]
index++
for index < len(elements) {
if checkExt(filter, ext[index]) {
remains = append(remains, elements[index])
}
index++
}
}
it := newIter(filter)
if err := checkSeekGT(it, input, exp, expVal); err != nil {
t.Fatal(err)
}
@ -175,62 +204,71 @@ func testBlockIterator(t *testing.T, data []byte, elements []uint64) {
}
}
func verifyTraversal(t *testing.T, elements []uint64, ext [][]uint16, newIter func(filter *extFilter) HistoryIndexIterator) {
set := make(map[extFilter]bool)
for _, extList := range ext {
for _, f := range extList {
set[extFilter(f)] = true
}
}
filters := slices.Collect(maps.Keys(set))
for i := 0; i < 16; i++ {
var filter *extFilter
if len(filters) > 0 {
filter = &filters[rand.Intn(len(filters))]
} else {
filter = nil
}
it := newIter(filter)
var (
pos int
exp []uint64
)
for pos < len(elements) {
if checkExt(filter, ext[pos]) {
exp = append(exp, elements[pos])
}
pos++
}
if err := checkNext(it, exp); err != nil {
t.Fatal(err)
}
}
}
func TestBlockIteratorSeekGT(t *testing.T) {
for _, size := range []int{0, 2, 34} {
for _, n := range []int{1, indexBlockRestartLen, 3 * indexBlockRestartLen} {
data, elements, ext := makeTestIndexBlock(n, size)
verifySeekGT(t, elements, ext, func(filter *extFilter) HistoryIndexIterator {
br, err := newBlockReader(data, size != 0)
if err != nil {
t.Fatalf("Failed to open the block for reading, %v", err)
}
return br.newIterator(filter)
})
}
}
}
func TestIndexIteratorSeekGT(t *testing.T) {
ident := newAccountIdent(common.Hash{0x1})
dbA := rawdb.NewMemoryDatabase()
testIndexIterator(t, ident, dbA, makeTestIndexBlocks(dbA, ident, 1))
for _, size := range []int{0, 2, 34} {
for _, n := range []int{1, 4096, 3 * 4096} {
db := rawdb.NewMemoryDatabase()
elements, ext := makeTestIndexBlocks(db, ident, n, size)
dbB := rawdb.NewMemoryDatabase()
testIndexIterator(t, ident, dbB, makeTestIndexBlocks(dbB, ident, 3*indexBlockEntriesCap))
dbC := rawdb.NewMemoryDatabase()
testIndexIterator(t, ident, dbC, makeTestIndexBlocks(dbC, ident, indexBlockEntriesCap-1))
dbD := rawdb.NewMemoryDatabase()
testIndexIterator(t, ident, dbD, makeTestIndexBlocks(dbD, ident, indexBlockEntriesCap+1))
}
func testIndexIterator(t *testing.T, stateIdent stateIdent, db ethdb.Database, elements []uint64) {
ir, err := newIndexReader(db, stateIdent)
if err != nil {
t.Fatalf("Failed to open the index reader, %v", err)
}
it := newIndexIterator(ir.descList, func(id uint32) (*blockReader, error) {
return newBlockReader(readStateIndexBlock(stateIdent, db, id))
})
for i := 0; i < 128; i++ {
var input uint64
if rand.Intn(2) == 0 {
input = elements[rand.Intn(len(elements))]
} else {
input = uint64(rand.Uint32())
}
index := sort.Search(len(elements), func(i int) bool {
return elements[i] > input
})
var (
exp bool
expVal uint64
remains []uint64
)
if index == len(elements) {
exp = false
} else {
exp = true
expVal = elements[index]
if index < len(elements) {
remains = elements[index+1:]
}
}
if err := checkSeekGT(it, input, exp, expVal); err != nil {
t.Fatal(err)
}
if exp {
if err := checkNext(it, remains); err != nil {
t.Fatal(err)
}
verifySeekGT(t, elements, ext, func(filter *extFilter) HistoryIndexIterator {
ir, err := newIndexReader(db, ident, size)
if err != nil {
t.Fatalf("Failed to open the index reader, %v", err)
}
return ir.newIterator(filter)
})
}
}
}
@ -242,56 +280,36 @@ func TestBlockIteratorTraversal(t *testing.T) {
testBlockIterator(t, data, elements)
*/
data, elements := makeTestIndexBlock(1)
testBlockIteratorTraversal(t, data, elements)
for _, size := range []int{0, 2, 34} {
for _, n := range []int{1, indexBlockRestartLen, 3 * indexBlockRestartLen} {
data, elements, ext := makeTestIndexBlock(n, size)
data, elements = makeTestIndexBlock(indexBlockRestartLen)
testBlockIteratorTraversal(t, data, elements)
data, elements = makeTestIndexBlock(3 * indexBlockRestartLen)
testBlockIteratorTraversal(t, data, elements)
data, elements = makeTestIndexBlock(indexBlockEntriesCap)
testBlockIteratorTraversal(t, data, elements)
}
func testBlockIteratorTraversal(t *testing.T, data []byte, elements []uint64) {
br, err := newBlockReader(data)
if err != nil {
t.Fatalf("Failed to open the block for reading, %v", err)
}
it := newBlockIterator(br.data, br.restarts)
if err := checkNext(it, elements); err != nil {
t.Fatal(err)
verifyTraversal(t, elements, ext, func(filter *extFilter) HistoryIndexIterator {
br, err := newBlockReader(data, size != 0)
if err != nil {
t.Fatalf("Failed to open the block for reading, %v", err)
}
return br.newIterator(filter)
})
}
}
}
func TestIndexIteratorTraversal(t *testing.T) {
ident := newAccountIdent(common.Hash{0x1})
dbA := rawdb.NewMemoryDatabase()
testIndexIteratorTraversal(t, ident, dbA, makeTestIndexBlocks(dbA, ident, 1))
for _, size := range []int{0, 2, 34} {
for _, n := range []int{1, 4096, 3 * 4096} {
db := rawdb.NewMemoryDatabase()
elements, ext := makeTestIndexBlocks(db, ident, n, size)
dbB := rawdb.NewMemoryDatabase()
testIndexIteratorTraversal(t, ident, dbB, makeTestIndexBlocks(dbB, ident, 3*indexBlockEntriesCap))
dbC := rawdb.NewMemoryDatabase()
testIndexIteratorTraversal(t, ident, dbC, makeTestIndexBlocks(dbC, ident, indexBlockEntriesCap-1))
dbD := rawdb.NewMemoryDatabase()
testIndexIteratorTraversal(t, ident, dbD, makeTestIndexBlocks(dbD, ident, indexBlockEntriesCap+1))
}
func testIndexIteratorTraversal(t *testing.T, stateIdent stateIdent, db ethdb.KeyValueReader, elements []uint64) {
ir, err := newIndexReader(db, stateIdent)
if err != nil {
t.Fatalf("Failed to open the index reader, %v", err)
}
it := newIndexIterator(ir.descList, func(id uint32) (*blockReader, error) {
return newBlockReader(readStateIndexBlock(stateIdent, db, id))
})
if err := checkNext(it, elements); err != nil {
t.Fatal(err)
verifyTraversal(t, elements, ext, func(filter *extFilter) HistoryIndexIterator {
ir, err := newIndexReader(db, ident, size)
if err != nil {
t.Fatalf("Failed to open the index reader, %v", err)
}
return ir.newIterator(filter)
})
}
}
}

View file

@ -29,19 +29,25 @@ import (
)
func TestIndexReaderBasic(t *testing.T) {
testIndexReaderBasic(t, 0)
testIndexReaderBasic(t, 2)
testIndexReaderBasic(t, 34)
}
func testIndexReaderBasic(t *testing.T, bitmapSize int) {
elements := []uint64{
1, 5, 10, 11, 20,
}
db := rawdb.NewMemoryDatabase()
bw, _ := newIndexWriter(db, newAccountIdent(common.Hash{0xa}), 0)
bw, _ := newIndexWriter(db, newAccountIdent(common.Hash{0xa}), 0, bitmapSize)
for i := 0; i < len(elements); i++ {
bw.append(elements[i])
bw.append(elements[i], randomExt(bitmapSize, 5))
}
batch := db.NewBatch()
bw.finish(batch)
batch.Write()
br, err := newIndexReader(db, newAccountIdent(common.Hash{0xa}))
br, err := newIndexReader(db, newAccountIdent(common.Hash{0xa}), bitmapSize)
if err != nil {
t.Fatalf("Failed to construct the index reader, %v", err)
}
@ -68,22 +74,28 @@ func TestIndexReaderBasic(t *testing.T) {
}
func TestIndexReaderLarge(t *testing.T) {
testIndexReaderLarge(t, 0)
testIndexReaderLarge(t, 2)
testIndexReaderLarge(t, 34)
}
func testIndexReaderLarge(t *testing.T, bitmapSize int) {
var elements []uint64
for i := 0; i < 10*indexBlockEntriesCap; i++ {
for i := 0; i < 10*4096; i++ {
elements = append(elements, rand.Uint64())
}
slices.Sort(elements)
db := rawdb.NewMemoryDatabase()
bw, _ := newIndexWriter(db, newAccountIdent(common.Hash{0xa}), 0)
bw, _ := newIndexWriter(db, newAccountIdent(common.Hash{0xa}), 0, bitmapSize)
for i := 0; i < len(elements); i++ {
bw.append(elements[i])
bw.append(elements[i], randomExt(bitmapSize, 5))
}
batch := db.NewBatch()
bw.finish(batch)
batch.Write()
br, err := newIndexReader(db, newAccountIdent(common.Hash{0xa}))
br, err := newIndexReader(db, newAccountIdent(common.Hash{0xa}), bitmapSize)
if err != nil {
t.Fatalf("Failed to construct the index reader, %v", err)
}
@ -107,7 +119,7 @@ func TestIndexReaderLarge(t *testing.T) {
}
func TestEmptyIndexReader(t *testing.T) {
br, err := newIndexReader(rawdb.NewMemoryDatabase(), newAccountIdent(common.Hash{0xa}))
br, err := newIndexReader(rawdb.NewMemoryDatabase(), newAccountIdent(common.Hash{0xa}), 0)
if err != nil {
t.Fatalf("Failed to construct the index reader, %v", err)
}
@ -121,27 +133,33 @@ func TestEmptyIndexReader(t *testing.T) {
}
func TestIndexWriterBasic(t *testing.T) {
testIndexWriterBasic(t, 0)
testIndexWriterBasic(t, 2)
testIndexWriterBasic(t, 34)
}
func testIndexWriterBasic(t *testing.T, bitmapSize int) {
db := rawdb.NewMemoryDatabase()
iw, _ := newIndexWriter(db, newAccountIdent(common.Hash{0xa}), 0)
iw.append(2)
if err := iw.append(1); err == nil {
iw, _ := newIndexWriter(db, newAccountIdent(common.Hash{0xa}), 0, bitmapSize)
iw.append(2, randomExt(bitmapSize, 5))
if err := iw.append(1, randomExt(bitmapSize, 5)); err == nil {
t.Fatal("out-of-order insertion is not expected")
}
var maxElem uint64
for i := 0; i < 10; i++ {
iw.append(uint64(i + 3))
iw.append(uint64(i+3), randomExt(bitmapSize, 5))
maxElem = uint64(i + 3)
}
batch := db.NewBatch()
iw.finish(batch)
batch.Write()
iw, err := newIndexWriter(db, newAccountIdent(common.Hash{0xa}), maxElem)
iw, err := newIndexWriter(db, newAccountIdent(common.Hash{0xa}), maxElem, bitmapSize)
if err != nil {
t.Fatalf("Failed to construct the block writer, %v", err)
}
for i := 0; i < 10; i++ {
if err := iw.append(uint64(i + 100)); err != nil {
if err := iw.append(uint64(i+100), randomExt(bitmapSize, 5)); err != nil {
t.Fatalf("Failed to append item, %v", err)
}
}
@ -149,61 +167,37 @@ func TestIndexWriterBasic(t *testing.T) {
}
func TestIndexWriterWithLimit(t *testing.T) {
db := rawdb.NewMemoryDatabase()
iw, _ := newIndexWriter(db, newAccountIdent(common.Hash{0xa}), 0)
testIndexWriterWithLimit(t, 0)
testIndexWriterWithLimit(t, 2)
testIndexWriterWithLimit(t, 34)
}
var maxElem uint64
for i := 0; i < indexBlockEntriesCap*2; i++ {
iw.append(uint64(i + 1))
maxElem = uint64(i + 1)
func testIndexWriterWithLimit(t *testing.T, bitmapSize int) {
db := rawdb.NewMemoryDatabase()
iw, _ := newIndexWriter(db, newAccountIdent(common.Hash{0xa}), 0, bitmapSize)
// 200 iterations (with around 50 bytes extension) is enough to cross
// the block boundary (4096 bytes)
for i := 0; i < 200; i++ {
iw.append(uint64(i+1), randomExt(bitmapSize, 50))
}
batch := db.NewBatch()
iw.finish(batch)
batch.Write()
suites := []struct {
limit uint64
expMax uint64
}{
// nothing to truncate
{
maxElem, maxElem,
},
// truncate the last element
{
maxElem - 1, maxElem - 1,
},
// truncation around the block boundary
{
uint64(indexBlockEntriesCap + 1),
uint64(indexBlockEntriesCap + 1),
},
// truncation around the block boundary
{
uint64(indexBlockEntriesCap),
uint64(indexBlockEntriesCap),
},
{
uint64(1), uint64(1),
},
// truncate the entire index, it's in theory invalid
{
uint64(0), uint64(0),
},
}
for i, suite := range suites {
iw, err := newIndexWriter(db, newAccountIdent(common.Hash{0xa}), suite.limit)
for i := 0; i < 200; i++ {
limit := uint64(i + 1)
iw, err := newIndexWriter(db, newAccountIdent(common.Hash{0xa}), limit, bitmapSize)
if err != nil {
t.Fatalf("Failed to construct the index writer, %v", err)
}
if iw.lastID != suite.expMax {
t.Fatalf("Test %d, unexpected max value, got %d, want %d", i, iw.lastID, suite.expMax)
if iw.lastID != limit {
t.Fatalf("Test %d, unexpected max value, got %d, want %d", i, iw.lastID, limit)
}
// Re-fill the elements
var maxElem uint64
for elem := suite.limit + 1; elem < indexBlockEntriesCap*4; elem++ {
if err := iw.append(elem); err != nil {
for elem := limit + 1; elem < 500; elem++ {
if err := iw.append(elem, randomExt(bitmapSize, 5)); err != nil {
t.Fatalf("Failed to append value %d: %v", elem, err)
}
maxElem = elem
@ -215,12 +209,20 @@ func TestIndexWriterWithLimit(t *testing.T) {
}
func TestIndexDeleterBasic(t *testing.T) {
db := rawdb.NewMemoryDatabase()
iw, _ := newIndexWriter(db, newAccountIdent(common.Hash{0xa}), 0)
testIndexDeleterBasic(t, 0)
testIndexDeleterBasic(t, 2)
testIndexDeleterBasic(t, 34)
}
func testIndexDeleterBasic(t *testing.T, bitmapSize int) {
db := rawdb.NewMemoryDatabase()
iw, _ := newIndexWriter(db, newAccountIdent(common.Hash{0xa}), 0, bitmapSize)
// 200 iterations (with around 50 bytes extension) is enough to cross
// the block boundary (4096 bytes)
var maxElem uint64
for i := 0; i < indexBlockEntriesCap*4; i++ {
iw.append(uint64(i + 1))
for i := 0; i < 200; i++ {
iw.append(uint64(i+1), randomExt(bitmapSize, 50))
maxElem = uint64(i + 1)
}
batch := db.NewBatch()
@ -228,11 +230,11 @@ func TestIndexDeleterBasic(t *testing.T) {
batch.Write()
// Delete unknown id, the request should be rejected
id, _ := newIndexDeleter(db, newAccountIdent(common.Hash{0xa}), maxElem)
if err := id.pop(indexBlockEntriesCap * 5); err == nil {
id, _ := newIndexDeleter(db, newAccountIdent(common.Hash{0xa}), maxElem, bitmapSize)
if err := id.pop(500); err == nil {
t.Fatal("Expect error to occur for unknown id")
}
for i := indexBlockEntriesCap * 4; i >= 1; i-- {
for i := 200; i >= 1; i-- {
if err := id.pop(uint64(i)); err != nil {
t.Fatalf("Unexpected error for element popping, %v", err)
}
@ -243,57 +245,33 @@ func TestIndexDeleterBasic(t *testing.T) {
}
func TestIndexDeleterWithLimit(t *testing.T) {
db := rawdb.NewMemoryDatabase()
iw, _ := newIndexWriter(db, newAccountIdent(common.Hash{0xa}), 0)
testIndexDeleterWithLimit(t, 0)
testIndexDeleterWithLimit(t, 2)
testIndexDeleterWithLimit(t, 34)
}
var maxElem uint64
for i := 0; i < indexBlockEntriesCap*2; i++ {
iw.append(uint64(i + 1))
maxElem = uint64(i + 1)
func testIndexDeleterWithLimit(t *testing.T, bitmapSize int) {
db := rawdb.NewMemoryDatabase()
iw, _ := newIndexWriter(db, newAccountIdent(common.Hash{0xa}), 0, bitmapSize)
// 200 iterations (with around 50 bytes extension) is enough to cross
// the block boundary (4096 bytes)
for i := 0; i < 200; i++ {
iw.append(uint64(i+1), randomExt(bitmapSize, 50))
}
batch := db.NewBatch()
iw.finish(batch)
batch.Write()
suites := []struct {
limit uint64
expMax uint64
}{
// nothing to truncate
{
maxElem, maxElem,
},
// truncate the last element
{
maxElem - 1, maxElem - 1,
},
// truncation around the block boundary
{
uint64(indexBlockEntriesCap + 1),
uint64(indexBlockEntriesCap + 1),
},
// truncation around the block boundary
{
uint64(indexBlockEntriesCap),
uint64(indexBlockEntriesCap),
},
{
uint64(1), uint64(1),
},
// truncate the entire index, it's in theory invalid
{
uint64(0), uint64(0),
},
}
for i, suite := range suites {
id, err := newIndexDeleter(db, newAccountIdent(common.Hash{0xa}), suite.limit)
for i := 0; i < 200; i++ {
limit := uint64(i + 1)
id, err := newIndexDeleter(db, newAccountIdent(common.Hash{0xa}), limit, bitmapSize)
if err != nil {
t.Fatalf("Failed to construct the index writer, %v", err)
}
if id.lastID != suite.expMax {
t.Fatalf("Test %d, unexpected max value, got %d, want %d", i, id.lastID, suite.expMax)
if id.lastID != limit {
t.Fatalf("Test %d, unexpected max value, got %d, want %d", i, iw.lastID, limit)
}
// Keep removing elements
for elem := id.lastID; elem > 0; elem-- {
if err := id.pop(elem); err != nil {
@ -339,7 +317,7 @@ func TestBatchIndexerWrite(t *testing.T) {
}
}
for addrHash, indexes := range accounts {
ir, _ := newIndexReader(db, newAccountIdent(addrHash))
ir, _ := newIndexReader(db, newAccountIdent(addrHash), 0)
for i := 0; i < len(indexes)-1; i++ {
n, err := ir.readGreaterThan(indexes[i])
if err != nil {
@ -359,7 +337,7 @@ func TestBatchIndexerWrite(t *testing.T) {
}
for addrHash, slots := range storages {
for slotHash, indexes := range slots {
ir, _ := newIndexReader(db, newStorageIdent(addrHash, slotHash))
ir, _ := newIndexReader(db, newStorageIdent(addrHash, slotHash), 0)
for i := 0; i < len(indexes)-1; i++ {
n, err := ir.readGreaterThan(indexes[i])
if err != nil {

View file

@ -34,7 +34,8 @@ import (
const (
// The batch size for reading state histories
historyReadBatch = 1000
historyReadBatch = 1000
historyIndexBatch = 8 * 1024 * 1024 // The number of state history indexes for constructing or deleting as batch
stateHistoryIndexV0 = uint8(0) // initial version of state index structure
stateHistoryIndexVersion = stateHistoryIndexV0 // the current state index version
@ -191,12 +192,12 @@ func (b *batchIndexer) finish(force bool) error {
for ident, list := range b.index {
eg.Go(func() error {
if !b.delete {
iw, err := newIndexWriter(b.db, ident, indexed)
iw, err := newIndexWriter(b.db, ident, indexed, 0)
if err != nil {
return err
}
for _, n := range list {
if err := iw.append(n); err != nil {
if err := iw.append(n, nil); err != nil {
return err
}
}
@ -204,7 +205,7 @@ func (b *batchIndexer) finish(force bool) error {
iw.finish(batch)
})
} else {
id, err := newIndexDeleter(b.db, ident, indexed)
id, err := newIndexDeleter(b.db, ident, indexed, 0)
if err != nil {
return err
}

View file

@ -40,8 +40,8 @@ type indexReaderWithLimitTag struct {
}
// newIndexReaderWithLimitTag constructs a index reader with indexing position.
func newIndexReaderWithLimitTag(db ethdb.KeyValueReader, state stateIdent, limit uint64) (*indexReaderWithLimitTag, error) {
r, err := newIndexReader(db, state)
func newIndexReaderWithLimitTag(db ethdb.KeyValueReader, state stateIdent, limit uint64, bitmapSize int) (*indexReaderWithLimitTag, error) {
r, err := newIndexReader(db, state, bitmapSize)
if err != nil {
return nil, err
}
@ -252,7 +252,7 @@ func (r *historyReader) read(state stateIdentQuery, stateID uint64, lastID uint6
// state retrieval
ir, ok := r.readers[state.String()]
if !ok {
ir, err = newIndexReaderWithLimitTag(r.disk, state.stateIdent, metadata.Last)
ir, err = newIndexReaderWithLimitTag(r.disk, state.stateIdent, metadata.Last, 0)
if err != nil {
return nil, err
}

View file

@ -159,17 +159,6 @@ func newTrienodeHistory(root common.Hash, parent common.Hash, block uint64, node
}
}
// sharedLen returns the length of the common prefix shared by a and b.
func sharedLen(a, b []byte) int {
n := min(len(a), len(b))
for i := range n {
if a[i] != b[i] {
return i
}
}
return n
}
// typ implements the history interface, returning the historical data type held.
func (h *trienodeHistory) typ() historyType {
return typeTrienodeHistory
@ -219,7 +208,7 @@ func (h *trienodeHistory) encode() ([]byte, []byte, []byte, error) {
restarts = append(restarts, internalValOffset)
prefixLen = 0
} else {
prefixLen = sharedLen(prevKey, key)
prefixLen = commonPrefixLen(prevKey, key)
}
value := h.nodes[owner][path]

View file

@ -580,8 +580,8 @@ func TestTrienodeHistoryReaderIterator(t *testing.T) {
}
}
// TestSharedLen tests the sharedLen helper function
func TestSharedLen(t *testing.T) {
// TestCommonPrefixLen tests the commonPrefixLen helper function
func TestCommonPrefixLen(t *testing.T) {
tests := []struct {
a, b []byte
expected int
@ -610,13 +610,13 @@ func TestSharedLen(t *testing.T) {
}
for i, test := range tests {
result := sharedLen(test.a, test.b)
result := commonPrefixLen(test.a, test.b)
if result != test.expected {
t.Errorf("Test %d: sharedLen(%q, %q) = %d, expected %d",
i, test.a, test.b, result, test.expected)
}
// Test commutativity
resultReverse := sharedLen(test.b, test.a)
resultReverse := commonPrefixLen(test.b, test.a)
if result != resultReverse {
t.Errorf("Test %d: sharedLen is not commutative: sharedLen(a,b)=%d, sharedLen(b,a)=%d",
i, result, resultReverse)

View file

@ -0,0 +1,83 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package pathdb
import (
"encoding/binary"
"fmt"
"slices"
)
// commonPrefixLen returns the length of the common prefix shared by a and b.
func commonPrefixLen(a, b []byte) int {
n := min(len(a), len(b))
for i := range n {
if a[i] != b[i] {
return i
}
}
return n
}
// encodeIDs sorts the given list of uint16 IDs and encodes them into a
// compact byte slice using variable-length unsigned integer encoding.
func encodeIDs(ids []uint16) []byte {
slices.Sort(ids)
buf := make([]byte, 0, len(ids))
for _, id := range ids {
buf = binary.AppendUvarint(buf, uint64(id))
}
return buf
}
// decodeIDs decodes a sequence of variable-length encoded uint16 IDs from the
// given byte slice and returns them as a set.
//
// Returns an error if the input buffer does not contain a complete Uvarint value.
func decodeIDs(buf []byte) ([]uint16, error) {
var res []uint16
for len(buf) > 0 {
id, n := binary.Uvarint(buf)
if n <= 0 {
return nil, fmt.Errorf("too short for decoding node id, %v", buf)
}
buf = buf[n:]
res = append(res, uint16(id))
}
return res, nil
}
// isAncestor reports whether node x is the ancestor of node y.
func isAncestor(x, y uint16) bool {
for y > x {
y = (y - 1) / 16 // parentID(y) = (y - 1) / 16
if y == x {
return true
}
}
return false
}
// isBitSet reports whether the bit at `index` in the byte slice `b` is set.
func isBitSet(b []byte, index int) bool {
return b[index/8]&(1<<(7-index%8)) != 0
}
// setBit sets the bit at `index` in the byte slice `b` to 1.
func setBit(b []byte, index int) {
b[index/8] |= 1 << (7 - index%8)
}

View file

@ -0,0 +1,81 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package pathdb
import (
"bytes"
"testing"
)
func TestIsAncestor(t *testing.T) {
suites := []struct {
x, y uint16
want bool
}{
{0, 1, true},
{0, 16, true},
{0, 17, true},
{0, 272, true},
{1, 0, false},
{1, 2, false},
{1, 17, true},
{1, 18, true},
{17, 273, true},
{1, 1, false},
}
for _, tc := range suites {
result := isAncestor(tc.x, tc.y)
if result != tc.want {
t.Fatalf("isAncestor(%d, %d) = %v, want %v", tc.x, tc.y, result, tc.want)
}
}
}
func TestBitmapSet(t *testing.T) {
suites := []struct {
index int
expect []byte
}{
{
0, []byte{0b10000000, 0x0},
},
{
1, []byte{0b01000000, 0x0},
},
{
7, []byte{0b00000001, 0x0},
},
{
8, []byte{0b00000000, 0b10000000},
},
{
15, []byte{0b00000000, 0b00000001},
},
}
for _, tc := range suites {
var buf [2]byte
setBit(buf[:], tc.index)
if !bytes.Equal(buf[:], tc.expect) {
t.Fatalf("bitmap = %v, want %v", buf, tc.expect)
}
if !isBitSet(buf[:], tc.index) {
t.Fatal("bit is not set")
}
}
}