diff --git a/core/rawdb/accessors_history.go b/core/rawdb/accessors_history.go new file mode 100644 index 0000000000..0127dd8cc5 --- /dev/null +++ b/core/rawdb/accessors_history.go @@ -0,0 +1,98 @@ +// Copyright 2024 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 . + +package rawdb + +import ( + "encoding/binary" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" +) + +// ReadStateHistoryIndexHead retrieves the number of latest indexed state history. +func ReadStateHistoryIndexHead(db ethdb.KeyValueReader) *uint64 { + data, _ := db.Get(stateHistoryIndexHeadKey) + if len(data) != 8 { + return nil + } + number := binary.BigEndian.Uint64(data) + return &number +} + +// WriteStateHistoryIndexHead stores the number of latest indexed state history +// into database. +func WriteStateHistoryIndexHead(db ethdb.KeyValueWriter, number uint64) { + if err := db.Put(stateHistoryIndexHeadKey, encodeBlockNumber(number)); err != nil { + log.Crit("Failed to store the state index tail", "err", err) + } +} + +// DeleteStateHistoryIndexHead removes the number of latest indexed state history. +func DeleteStateHistoryIndexHead(db ethdb.KeyValueWriter) { + if err := db.Delete(stateHistoryIndexHeadKey); err != nil { + log.Crit("Failed to delete the state index tail", "err", err) + } +} + +// ReadStateIndex retrieves the state index with the provided account address +// and state hash. +func ReadStateIndex(db ethdb.KeyValueReader, address common.Address, state common.Hash) []byte { + data, err := db.Get(stateIndexKey(address, state)) + if err != nil || len(data) == 0 { + return nil + } + return data +} + +// WriteStateIndex writes the provided state index into database. +func WriteStateIndex(db ethdb.KeyValueWriter, address common.Address, state common.Hash, data []byte) { + if err := db.Put(stateIndexKey(address, state), data); err != nil { + log.Crit("Failed to store state index", "err", err) + } +} + +// DeleteStateIndex deletes the specified state index from the database. +func DeleteStateIndex(db ethdb.KeyValueWriter, address common.Address, state common.Hash) { + if err := db.Delete(stateIndexKey(address, state)); err != nil { + log.Crit("Failed to delete state index", "err", err) + } +} + +// ReadStateIndexBlock retrieves the state index block with the provided state +// identifier along with the block id. +func ReadStateIndexBlock(db ethdb.KeyValueReader, address common.Address, state common.Hash, id uint32) []byte { + data, err := db.Get(stateIndexBlockKey(address, state, id)) + if err != nil || len(data) == 0 { + return nil + } + return data +} + +// WriteStateIndexBlock writes the provided state index block into database. +func WriteStateIndexBlock(db ethdb.KeyValueWriter, address common.Address, state common.Hash, id uint32, data []byte) { + if err := db.Put(stateIndexBlockKey(address, state, id), data); err != nil { + log.Crit("Failed to store state index", "err", err) + } +} + +// DeleteStateIndexBlock deletes the specified state index block from the database. +func DeleteStateIndexBlock(db ethdb.KeyValueWriter, address common.Address, state common.Hash, id uint32) { + if err := db.Delete(stateIndexBlockKey(address, state, id)); err != nil { + log.Crit("Failed to delete state index", "err", err) + } +} diff --git a/core/rawdb/accessors_state.go b/core/rawdb/accessors_state.go index 9ce58e7d27..944da43997 100644 --- a/core/rawdb/accessors_state.go +++ b/core/rawdb/accessors_state.go @@ -251,6 +251,33 @@ func ReadStateHistory(db ethdb.AncientReaderOp, id uint64) ([]byte, []byte, []by return meta, accountIndex, storageIndex, accountData, storageData, nil } +// ReadStateHistoryList retrieves a list of state histories from database with +// specific range. Compute the position of state history in freezer by minus one +// since the id of first state history starts from one(zero for initial state). +func ReadStateHistoryList(db ethdb.AncientReaderOp, start uint64, count uint64) ([][]byte, [][]byte, [][]byte, [][]byte, [][]byte, error) { + metaList, err := db.AncientRange(stateHistoryMeta, start-1, count, 0) + if err != nil { + return nil, nil, nil, nil, nil, err + } + aIndexList, err := db.AncientRange(stateHistoryAccountIndex, start-1, count, 0) + if err != nil { + return nil, nil, nil, nil, nil, err + } + sIndexList, err := db.AncientRange(stateHistoryStorageIndex, start-1, count, 0) + if err != nil { + return nil, nil, nil, nil, nil, err + } + aDataList, err := db.AncientRange(stateHistoryAccountData, start-1, count, 0) + if err != nil { + return nil, nil, nil, nil, nil, err + } + sDataList, err := db.AncientRange(stateHistoryStorageData, start-1, count, 0) + if err != nil { + return nil, nil, nil, nil, nil, err + } + return metaList, aIndexList, sIndexList, aDataList, sDataList, nil +} + // WriteStateHistory writes the provided state history to database. Compute the // position of state history in freezer by minus one since the id of first state // history starts from one(zero for initial state). diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 04b5d0d6d2..fb2d8cea0f 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -76,6 +76,9 @@ var ( // trieJournalKey tracks the in-memory trie node layers across restarts. trieJournalKey = []byte("TrieJournal") + // stateHistoryIndexHeadKey tracks the ID of the latest state that has been indexed. + stateHistoryIndexHeadKey = []byte("StateHistoryIndexHead") + // txIndexTailKey tracks the oldest block whose transactions have been indexed. txIndexTailKey = []byte("TransactionIndexTail") @@ -117,6 +120,9 @@ var ( TrieNodeStoragePrefix = []byte("O") // TrieNodeStoragePrefix + accountHash + hexPath -> trie node stateIDPrefix = []byte("L") // stateIDPrefix + state root -> state id + // state history indexing within path-based storage scheme + stateIndexPrefix = []byte("M") // stateIndexPrefix + account address or (account address + slotHash) -> index + // VerklePrefix is the database prefix for Verkle trie data, which includes: // (a) Trie nodes // (b) In-memory trie node journal @@ -346,3 +352,21 @@ func IsStorageTrieNode(key []byte) bool { ok, _, _ := ResolveStorageTrieNode(key) return ok } + +// stateIndexKey = stateIndexPrefix + address + state +func stateIndexKey(address common.Address, state common.Hash) []byte { + if state == (common.Hash{}) { + return append(stateIndexPrefix, address.Bytes()...) + } + return append(append(stateIndexPrefix, address.Bytes()...), state.Bytes()...) +} + +// stateIndexBlockKey = stateIndexPrefix + address + state + id +func stateIndexBlockKey(address common.Address, state common.Hash, id uint32) []byte { + var buf [4]byte + binary.BigEndian.PutUint32(buf[:], id) + if state == (common.Hash{}) { + return append(append(stateIndexPrefix, address.Bytes()...), buf[:]...) + } + return append(append(append(stateIndexPrefix, address.Bytes()...), state.Bytes()...), buf[:]...) +} diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go index a2707d6150..2c374d97e9 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -161,6 +161,7 @@ type Database struct { diskdb ethdb.Database // Persistent storage for matured trie nodes tree *layerTree // The group for all known layers freezer ethdb.ResettableAncientStore // Freezer for storing trie histories, nil possible in tests + indexer *historyIndexer // History indexer lock sync.RWMutex // Lock to prevent mutations from happening at the same time } @@ -257,6 +258,8 @@ func (db *Database) repairHistory() error { if pruned != 0 { log.Warn("Truncated extra state histories", "number", pruned) } + // TODO read-only mode? + db.indexer = newHistoryIndexer(db.diskdb, db.freezer, db.tree.bottom().stateID()) return nil } diff --git a/triedb/pathdb/disklayer.go b/triedb/pathdb/disklayer.go index 8925086963..f8d9809980 100644 --- a/triedb/pathdb/disklayer.go +++ b/triedb/pathdb/disklayer.go @@ -240,6 +240,9 @@ func (dl *diskLayer) commit(bottom *diffLayer, force bool) (*diskLayer, error) { overflow = true oldest = bottom.stateID() - limit + 1 // track the id of history **after truncation** } + if dl.db.indexer != nil { + dl.db.indexer.notify(bottom.stateID()) + } } // Mark the diskLayer as stale before applying any mutations on top. dl.markStale() diff --git a/triedb/pathdb/history.go b/triedb/pathdb/history.go index e1cd981153..bd0c6f2840 100644 --- a/triedb/pathdb/history.go +++ b/triedb/pathdb/history.go @@ -490,6 +490,32 @@ func readHistory(reader ethdb.AncientReader, id uint64) (*history, error) { return &dec, nil } +// readHistories reads and decodes a list of state histories with specific +// history range. +func readHistories(freezer ethdb.AncientReader, start uint64, count uint64) ([]*history, error) { + metaList, aIndexList, sIndexList, aDataList, sDataList, err := rawdb.ReadStateHistoryList(freezer, start, count) + if err != nil { + return nil, err + } + number := len(metaList) + if number != len(aIndexList) || number != len(sIndexList) || number != len(aDataList) || number != len(sDataList) { + return nil, errors.New("corrupted state history") + } + var result []*history + for i := 0; i < number; i++ { + var m meta + if err := m.decode(metaList[i]); err != nil { + return nil, err + } + dec := history{meta: &m} + if err := dec.decode(aDataList[i], sDataList[i], aIndexList[i], sIndexList[i]); err != nil { + return nil, err + } + result = append(result, &dec) + } + return result, nil +} + // writeHistory persists the state history with the provided state set. func writeHistory(writer ethdb.AncientWriter, dl *diffLayer) error { // Short circuit if state set is not available. diff --git a/triedb/pathdb/history_index_reader.go b/triedb/pathdb/history_index_reader.go new file mode 100644 index 0000000000..df6dae8842 --- /dev/null +++ b/triedb/pathdb/history_index_reader.go @@ -0,0 +1,339 @@ +// Copyright 2023 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 0; i-- { + restart := binary.BigEndian.Uint32(blob[len(blob)-int(i+1)*4:]) + restarts = append(restarts, restart) + } + prev := restarts[0] + for i := 1; i < len(restarts); i++ { + if restarts[i] <= prev { + return nil, nil, fmt.Errorf("restart out of order, prev: %d, next: %d", prev, restarts[i]) + } + if int(restarts[i]) >= len(blob)-tailLen { + return nil, nil, fmt.Errorf("invalid restart position, restart: %d, size: %d", restarts[i], len(blob)-tailLen) + } + prev = restarts[i] + } + return restarts, blob[:len(blob)-tailLen], nil +} + +func newBlockReader(disk ethdb.KeyValueReader, addr common.Address, state common.Hash, id uint32) (*blockReader, error) { + blob := rawdb.ReadStateIndexBlock(disk, addr, state, id) + if len(blob) == 0 { + return nil, errors.New("index block is not present") + } + restarts, data, err := parseIndexBlock(blob) + if err != nil { + return nil, err + } + return &blockReader{ + restarts: restarts, + buf: data, // safe to own the slice + }, nil +} + +func (br *blockReader) readGreaterThan(id uint64) (uint64, error) { + var err error + index := sort.Search(len(br.restarts), func(i int) bool { + item, n := binary.Uvarint(br.buf[br.restarts[i]:]) + if n <= 0 { + err = errors.New("failed to decode item at restart point") + } + return item > id + }) + if err != nil { + return 0, err + } + if index == 0 { + item, _ := binary.Uvarint(br.buf[br.restarts[0]:]) + return item, nil + } + var ( + start int + limit int + result uint64 + ) + if index == len(br.restarts) { + start = int(br.restarts[len(br.restarts)-1]) + limit = len(br.buf) + } else { + start = int(br.restarts[index-1]) + limit = int(br.restarts[index]) + } + pos := start + for pos < limit { + x, n := binary.Uvarint(br.buf[pos:]) + if pos == start { + result = x + } else { + result += x + } + if result > id { + return result, nil + } + pos += n + } + return 0, errors.New("not found") +} + +type indexReader struct { + disk ethdb.KeyValueReader + descList []*indexBlockDesc + readers map[uint32]*blockReader + owner common.Address + state common.Hash +} + +func parseIndex(blob []byte) ([]*indexBlockDesc, error) { + if len(blob) == 0 { + return nil, errors.New("state index not found") + } + if len(blob)%indexBlockDescSize != 0 { + return nil, fmt.Errorf("corrupted state index, len: %d", len(blob)) + } + var descList []*indexBlockDesc + for i := 0; i < len(blob)/indexBlockDescSize; i++ { + var desc indexBlockDesc + desc.decode(blob[i*indexBlockDescSize : (i+1)*indexBlockDescSize]) + if desc.empty() { + return nil, errors.New("empty state index block") + } + descList = append(descList, &desc) + } + return descList, nil +} + +func newIndexReader(disk ethdb.KeyValueReader, owner common.Address, state common.Hash) (*indexReader, error) { + descList, err := parseIndex(rawdb.ReadStateIndex(disk, owner, state)) + if err != nil { + return nil, err + } + return &indexReader{ + descList: descList, + readers: make(map[uint32]*blockReader), + disk: disk, + owner: owner, + state: state, + }, nil +} + +func (r *indexReader) readGreaterThan(id uint64) (uint64, error) { + index := sort.Search(len(r.descList), func(i int) bool { + return id < r.descList[i].max + }) + if index == len(r.descList) { + return math.MaxUint64, nil + } + desc := r.descList[index] + + br, ok := r.readers[desc.id] + if !ok { + var err error + br, err = newBlockReader(r.disk, r.owner, r.state, desc.id) + if err != nil { + return 0, err + } + r.readers[desc.id] = br + } + return br.readGreaterThan(id) +} + +type historyReader struct { + disk ethdb.KeyValueReader + freezer ethdb.AncientReader + readers map[string]*indexReader +} + +func newHistoryReader(disk ethdb.KeyValueReader, freezer ethdb.AncientReader) *historyReader { + return &historyReader{ + disk: disk, + freezer: freezer, + readers: make(map[string]*indexReader), + } +} + +func (r *historyReader) findAccount(account common.Address, id uint64, resolve func([]byte)) error { + blob := rawdb.ReadStateAccountIndex(r.freezer, id) + if len(blob)%accountIndexSize != 0 { + return errors.New("corrupted account index") + } + n := len(blob) / accountIndexSize + + index := sort.Search(n, func(i int) bool { + h := blob[accountIndexSize*i : accountIndexSize*i+common.HashLength] + return bytes.Compare(h, account.Bytes()) >= 0 + }) + if index == n { + return errors.New("account is not found") + } + if account != common.BytesToAddress(blob[accountIndexSize*index:accountIndexSize*index+common.AddressLength]) { + return errors.New("account is not found") + } + resolve(blob[accountIndexSize*index : accountIndexSize*(index+1)]) + return nil +} + +func (r *historyReader) findStorage(storageHash common.Hash, id uint64, slotOffset, slotLength int, resolve func([]byte)) error { + blob := rawdb.ReadStateStorageIndex(r.freezer, id) + if len(blob)%slotIndexSize != 0 { + return errors.New("storage indices are not corrupted") + } + if slotIndexSize*(slotOffset+slotLength) > len(blob) { + return errors.New("out of slice") + } + subSlice := blob[slotIndexSize*slotOffset : slotIndexSize*(slotOffset+slotLength)] + + index := sort.Search(slotLength, func(i int) bool { + slotHash := subSlice[slotIndexSize*i : slotIndexSize*i+common.HashLength] + return bytes.Compare(slotHash, storageHash.Bytes()) >= 0 + }) + if index == slotLength { + return errors.New("storage is not found") + } + if storageHash != common.BytesToHash(subSlice[slotIndexSize*index:slotIndexSize*index+common.HashLength]) { + return errors.New("storage is not found") + } + resolve(subSlice[slotIndexSize*index : slotIndexSize*(index+1)]) + return nil +} + +func (r *historyReader) resolveAccount(accountHash common.Address, id uint64) ([]byte, error) { + var ( + offset int + length int + ) + err := r.findAccount(accountHash, id, func(blob []byte) { + length = int(blob[common.AddressLength]) + offset = int(binary.BigEndian.Uint32(blob[common.AddressLength+1 : common.AddressLength+5])) + }) + if err != nil { + return nil, err + } + // TODO(rj493456442) optimize it with partial read + data := rawdb.ReadStateAccountHistory(r.freezer, id) + if len(data) < offset+length { + return nil, errors.New("corrupted account data") + } + return data[offset : offset+length], nil +} + +func (r *historyReader) resolveStorage(account common.Address, storageHash common.Hash, id uint64) ([]byte, error) { + var ( + slotOffset int + slotLength int + offset int + length int + ) + err := r.findAccount(account, id, func(blob []byte) { + slotOffset = int(binary.BigEndian.Uint32(blob[common.AddressLength+5 : common.AddressLength+9])) + slotLength = int(binary.BigEndian.Uint32(blob[common.AddressLength+9 : common.AddressLength+13])) + }) + if err != nil { + return nil, err + } + err = r.findStorage(storageHash, id, slotOffset, slotLength, func(blob []byte) { + length = int(blob[common.HashLength]) + offset = int(binary.BigEndian.Uint32(blob[common.HashLength+1 : common.HashLength+5])) + }) + if err != nil { + return nil, err + } + // TODO(rj493456442) optimize it with partial read + data := rawdb.ReadStateStorageHistory(r.freezer, id) + if len(data) < offset+length { + return nil, errors.New("corrupted storage data") + } + return data[offset : offset+length], nil +} + +func (r *historyReader) resolve(owner common.Address, state common.Hash, id uint64) ([]byte, error) { + if state == (common.Hash{}) { + return r.resolveAccount(owner, id) + } + return r.resolveStorage(owner, state, id) +} + +func (r *historyReader) read(owner common.Address, state common.Hash, id uint64, latest uint64) ([]byte, error) { + tail, err := r.freezer.Tail() + if err != nil { + return nil, err + } + // id == tail is allowed, as the first history object preserved is tail+1 + if id < tail { + return nil, errors.New("historic state is pruned") + } + head := rawdb.ReadStateHistoryIndexHead(r.disk) + + /* + the available range of histories is [tail+1, head] + */ + if head == nil || *head <= id { + return nil, errors.New("state history is not fully indexed") + } + ir, ok := r.readers[owner.Hex()+state.Hex()] + if !ok { + ir, err = newIndexReader(r.disk, owner, state) + if err != nil { + return nil, err + } + r.readers[owner.Hex()+state.Hex()] = ir + } + id, err = ir.readGreaterThan(id) + if err != nil { + return nil, err + } + if id == math.MaxUint64 { + if *head < latest { + return nil, errors.New("state history is not fully indexed") + } + return nil, errors.New("not found") + } + return r.resolve(owner, state, id) +} diff --git a/triedb/pathdb/history_index_writer.go b/triedb/pathdb/history_index_writer.go new file mode 100644 index 0000000000..03bcafe8a2 --- /dev/null +++ b/triedb/pathdb/history_index_writer.go @@ -0,0 +1,330 @@ +// Copyright 2024 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 = indexBlockEntriesCap +} + +// encode packs index block descriptor into byte stream. +func (d *indexBlockDesc) encode() []byte { + var buf [indexBlockDescSize]byte + binary.BigEndian.PutUint64(buf[:8], d.min) + binary.BigEndian.PutUint64(buf[8:16], d.max) + binary.BigEndian.PutUint32(buf[16:20], d.entries) + binary.BigEndian.PutUint32(buf[20:24], d.id) + return buf[:] +} + +// decode unpacks index block descriptor from byte stream. +func (d *indexBlockDesc) decode(blob []byte) { + d.min = binary.BigEndian.Uint64(blob[:8]) + d.max = binary.BigEndian.Uint64(blob[8:16]) + d.entries = binary.BigEndian.Uint32(blob[16:20]) + d.id = binary.BigEndian.Uint32(blob[20:24]) +} + +type blockWriter struct { + desc *indexBlockDesc + restarts []uint32 + scratch []byte + buf []byte +} + +func newBlockWriter(blob []byte, desc *indexBlockDesc) (*blockWriter, error) { + scratch := make([]byte, binary.MaxVarintLen64) + if len(blob) == 0 { + return &blockWriter{ + desc: desc, + scratch: scratch, + buf: make([]byte, 0, 1024), + }, nil + } + restarts, data, err := parseIndexBlock(blob) + if err != nil { + return nil, err + } + return &blockWriter{ + desc: desc, + restarts: restarts, + scratch: scratch, + buf: data, // safe to own the slice + }, nil +} + +func (b *blockWriter) append(id uint64) error { + if id <= b.desc.max { + return fmt.Errorf("element out of order, last: %d, this: %d", b.desc.max, id) + } + if b.desc.entries%indexBlockRestartLen == 0 { + b.restarts = append(b.restarts, uint32(len(b.buf))) + + // The restart point item can be either encoded in variable + // size or fixed size. Although variable-size encoding is + // slightly slower (2ns per operation), it is still relatively + // fast, therefore, it's picked for better space efficiency. + n := binary.PutUvarint(b.scratch[0:], id) + b.buf = append(b.buf, b.scratch[:n]...) + } else { + n := binary.PutUvarint(b.scratch[0:], id-b.desc.max) + b.buf = append(b.buf, b.scratch[:n]...) + } + b.desc.entries++ + if b.desc.min == 0 { + b.desc.min = id + } + b.desc.max = id + return nil +} + +func (b *blockWriter) empty() bool { + return b.desc.empty() +} + +func (b *blockWriter) full() bool { + return b.desc.full() +} + +func (b *blockWriter) finish() error { + b.restarts = append(b.restarts, uint32(len(b.restarts))) + for _, number := range b.restarts { + binary.BigEndian.PutUint32(b.scratch[:4], number) + b.buf = append(b.buf, b.scratch[:4]...) + } + return nil +} + +type indexWriter struct { + descList []*indexBlockDesc + last uint64 + bw *blockWriter + frozen []*blockWriter + + db ethdb.KeyValueStore + addr common.Address + state common.Hash +} + +func newIndexWriter(db ethdb.KeyValueStore, addr common.Address, state common.Hash) (*indexWriter, error) { + blob := rawdb.ReadStateIndex(db, addr, state) + if len(blob) == 0 { + desc := &indexBlockDesc{} + bw, _ := newBlockWriter(nil, desc) + return &indexWriter{ + descList: []*indexBlockDesc{desc}, + bw: bw, + db: db, + addr: addr, + state: state, + }, nil + } + descList, err := parseIndex(blob) + if err != nil { + return nil, err + } + // Open the last block writer, or create a new one in case + // it's already full. + var ( + lastDesc = descList[len(descList)-1] + lastElem = lastDesc.max + ) + if lastDesc.full() { + descList = append(descList, &indexBlockDesc{id: lastDesc.id + 1}) + lastDesc = descList[len(descList)-1] + } + + indexBlock := rawdb.ReadStateIndexBlock(db, addr, state, lastDesc.id) + bw, err := newBlockWriter(indexBlock, lastDesc) + if err != nil { + return nil, err + } + return &indexWriter{ + descList: descList, + last: lastElem, + bw: bw, + db: db, + addr: addr, + state: state, + }, nil +} + +func (w *indexWriter) append(id uint64) error { + if id <= w.last { + return fmt.Errorf("element out of order, last: %d, this: %d", w.last, id) + } + if err := w.bw.append(id); err != nil { + return err + } + w.last = id + + if w.bw.full() { + w.rotate() + } + return nil +} + +func (w *indexWriter) rotate() { + w.frozen = append(w.frozen, w.bw) + desc := newIndexBlockDesc(w.bw.desc.id + 1) + w.bw, _ = newBlockWriter(nil, desc) + w.descList = append(w.descList, desc) +} + +func (w *indexWriter) finish(batch ethdb.Batch) error { + var ( + writers = append(w.frozen, w.bw) + descList = w.descList + ) + // Chop the last block if it's empty + if w.bw.empty() { + writers = writers[:len(writers)-1] + descList = descList[:len(descList)-1] + } + if len(writers) == 0 { + return nil + } + for _, bw := range writers { + if err := bw.finish(); err != nil { + return err + } + rawdb.WriteStateIndexBlock(batch, w.addr, w.state, bw.desc.id, bw.buf) + } + buf := make([]byte, 0, indexBlockDescSize*len(descList)) + for _, desc := range descList { + buf = append(buf, desc.encode()...) + } + rawdb.WriteStateIndex(batch, w.addr, w.state, buf) + return nil +} + +type historyWriter struct { + accounts map[common.Address][]uint64 + storages map[common.Address]map[common.Hash][]uint64 + total int +} + +func newHistoryWriter() *historyWriter { + return &historyWriter{ + accounts: make(map[common.Address][]uint64), + storages: make(map[common.Address]map[common.Hash][]uint64), + } +} + +func (w *historyWriter) reset() { + w.total = 0 + w.accounts = make(map[common.Address][]uint64) + w.storages = make(map[common.Address]map[common.Hash][]uint64) +} + +func (w *historyWriter) addAccount(addr common.Address, number uint64) { + w.total += 1 + w.accounts[addr] = append(w.accounts[addr], number) +} + +func (w *historyWriter) addSlot(addr common.Address, hash common.Hash, number uint64) { + w.total += 1 + if _, ok := w.storages[addr]; !ok { + w.storages[addr] = make(map[common.Hash][]uint64) + } + w.storages[addr][hash] = append(w.storages[addr][hash], number) +} + +func (w *historyWriter) finish(db ethdb.KeyValueStore, force bool, head uint64) error { + if !force && w.total < stateWriteBatch { + return nil + } + var ( + s = time.Now() + batch = db.NewBatch() + ) + for account, idList := range w.accounts { + iw, err := newIndexWriter(db, account, common.Hash{}) + if err != nil { + return err + } + for _, id := range idList { + if err := iw.append(id); err != nil { + return err + } + } + if err := iw.finish(batch); err != nil { + return err + } + } + for account, slots := range w.storages { + for slot, idList := range slots { + iw, err := newIndexWriter(db, account, slot) + if err != nil { + return err + } + for _, id := range idList { + if err := iw.append(id); err != nil { + return err + } + } + if err := iw.finish(batch); err != nil { + return err + } + } + } + rawdb.WriteStateHistoryIndexHead(batch, head) + if err := batch.Write(); err != nil { + return err + } + w.reset() + log.Info("Written state history indexes", "head", head, "size", common.StorageSize(batch.ValueSize()), "elapsed", common.PrettyDuration(time.Since(s))) + return nil +} diff --git a/triedb/pathdb/history_indexer.go b/triedb/pathdb/history_indexer.go new file mode 100644 index 0000000000..3a1bdde49e --- /dev/null +++ b/triedb/pathdb/history_indexer.go @@ -0,0 +1,210 @@ +// Copyright 2024 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 = tailID { + return *head + 1, nil + } + // History has been shortened without indexing. Discard the gapped segment + // in the history and shift to the first available element. + // + // The missing indexes corresponding to the gapped histories won't be visible. + // It's fine to leave them unindexed. + log.Info("History gap detected, discard old segment", "oldHead", *head, "newHead", tailID) + return tailID, nil +} + +func (i *historyIndexer) run(done chan struct{}, head uint64, interrupt *atomic.Int32) { + defer close(done) + + begin, err := i.next() + if err != nil { + log.Error("Failed to find next state history for indexing", "err", err) + return + } + // TODO what if head is lower than the index head. It can + // happen if the entire state history freezer is reset. + //if begin > head { + // + //} + log.Info("Start history indexing", "begin", begin, "head", head) + + var ( + current = begin + writer = newHistoryWriter() + start = time.Now() + logged = time.Now() + ) + for current <= head { + count := head - current + 1 + if count > historyReadBatch { + count = historyReadBatch + } + s := time.Now() + result, err := readHistories(i.freezer, current, count) + if err != nil { + log.Error("Failed to read history", "err", err) + return + } + log.Debug("Loaded histories", "number", len(result), "elapsed", common.PrettyDuration(time.Since(s))) + + for _, h := range result { + if err := i.process(writer, h, current); err != nil { + log.Error("Failed to index history", "err", err) + return + } + current += 1 + + if time.Since(logged) > time.Second*8 { + logged = time.Now() + + var ( + left = head - current + done = current - begin + speed = done/uint64(time.Since(start)/time.Millisecond+1) + 1 // +1s to avoid division by zero + ) + // Override the ETA if larger than the largest until now + eta := time.Duration(left/speed) * time.Millisecond + log.Info("Indexing state history", "processed", current-begin+1, "remain", head-current, "eta", common.PrettyDuration(eta)) + } + } + // Check interruption signal and abort process if it's fired + if interrupt != nil { + if signal := interrupt.Load(); signal != 0 { + if err := writer.finish(i.disk, true, current-1); err != nil { + log.Error("Failed to flush index", "err", err) + } + log.Info("State indexing interrupted") + return + } + } + } + if err := writer.finish(i.disk, true, head); err != nil { + log.Error("Failed to flush index", "err", err) + } + log.Info("Indexed state history", "from", begin, "to", head, "elapsed", common.PrettyDuration(time.Since(start))) +} + +func (i *historyIndexer) loop(head uint64) { + defer i.wg.Done() + + // Launch background indexing thread + done, interrupt := make(chan struct{}), new(atomic.Int32) + go i.run(done, head, interrupt) + + for { + select { + case newHead := <-i.headCh: + if newHead <= head { + // TODO, reorg?? + continue + } + head = newHead + + if done == nil { + done, interrupt = make(chan struct{}), new(atomic.Int32) + go i.run(done, head, interrupt) + } + case <-done: + done, interrupt = nil, nil + + case <-i.closeCh: + if done != nil { + interrupt.Store(1) + log.Info("Waiting background history indexer to exit") + <-done + } + return + } + } +}