diff --git a/triedb/pathdb/history_inspect.go b/triedb/pathdb/history_inspect.go index 9b4eea27b4..78a1ff0598 100644 --- a/triedb/pathdb/history_inspect.go +++ b/triedb/pathdb/history_inspect.go @@ -21,7 +21,6 @@ import ( "time" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" ) @@ -98,35 +97,90 @@ func inspectHistory(freezer ethdb.AncientReader, start, end uint64, onHistory fu // accountHistory inspects the account history within the range. func accountHistory(freezer ethdb.AncientReader, address common.Address, start, end uint64) (*HistoryStats, error) { - return inspectHistory(freezer, start, end, func(h *stateHistory, stats *HistoryStats) { - blob, exists := h.accounts[address] - if !exists { - return + return inspectAccountHistory(freezer, address, start, end) +} + +func inspectAccountHistory(freezer ethdb.AncientReader, targetAddr common.Address, start, end uint64) (*HistoryStats, error) { + var ( + stats = &HistoryStats{} + init = time.Now() + logged = time.Now() + ) + start, end, err := sanitizeRange(start, end, freezer) + if err != nil { + return nil, err + } + for id := start; id <= end; id += 1 { + h, err := readAccountHistory(freezer, id, targetAddr) + if err != nil { + return nil, err } - stats.Blocks = append(stats.Blocks, h.meta.block) - stats.Origins = append(stats.Origins, blob) - }) + if id == start { + stats.Start = h.meta.block + } + if id == end { + stats.End = h.meta.block + } + + if blob, exists := h.accounts[targetAddr]; exists { + stats.Blocks = append(stats.Blocks, h.meta.block) + stats.Origins = append(stats.Origins, blob) + } + + if time.Since(logged) > time.Second*8 { + logged = time.Now() + eta := float64(time.Since(init)) / float64(id-start+1) * float64(end-id) + log.Info("Inspecting account history", "checked", id-start+1, "left", end-id, "elapsed", common.PrettyDuration(time.Since(init)), "eta", common.PrettyDuration(eta)) + } + } + log.Info("Inspected account history", "total", end-start+1, "elapsed", common.PrettyDuration(time.Since(init))) + return stats, nil } // storageHistory inspects the storage history within the range. func storageHistory(freezer ethdb.AncientReader, address common.Address, slot common.Hash, start uint64, end uint64) (*HistoryStats, error) { - slotHash := crypto.Keccak256Hash(slot.Bytes()) - return inspectHistory(freezer, start, end, func(h *stateHistory, stats *HistoryStats) { - slots, exists := h.storages[address] - if !exists { - return + return inspectStorageHistory(freezer, address, slot, start, end) +} + +// inspectStorageHistory efficiently inspects storage history by only decoding the target account's storage. +func inspectStorageHistory(freezer ethdb.AncientReader, targetAddr common.Address, targetSlot common.Hash, start, end uint64) (*HistoryStats, error) { + var ( + stats = &HistoryStats{} + init = time.Now() + logged = time.Now() + ) + start, end, err := sanitizeRange(start, end, freezer) + if err != nil { + return nil, err + } + for id := start; id <= end; id += 1 { + h, err := readSlotHistory(freezer, id, targetAddr, targetSlot) + if err != nil { + return nil, err } - key := slotHash - if h.meta.version != stateHistoryV0 { - key = slot + if id == start { + stats.Start = h.meta.block } - blob, exists := slots[key] - if !exists { - return + if id == end { + stats.End = h.meta.block } - stats.Blocks = append(stats.Blocks, h.meta.block) - stats.Origins = append(stats.Origins, blob) - }) + + if slots, exists := h.storages[targetAddr]; exists { + for _, blob := range slots { + stats.Blocks = append(stats.Blocks, h.meta.block) + stats.Origins = append(stats.Origins, blob) + break + } + } + + if time.Since(logged) > time.Second*8 { + logged = time.Now() + eta := float64(time.Since(init)) / float64(id-start+1) * float64(end-id) + log.Info("Inspecting storage history", "checked", id-start+1, "left", end-id, "elapsed", common.PrettyDuration(time.Since(init)), "eta", common.PrettyDuration(eta)) + } + } + log.Info("Inspected storage history", "total", end-start+1, "elapsed", common.PrettyDuration(time.Since(init))) + return stats, nil } // historyRange returns the block number range of local state histories. diff --git a/triedb/pathdb/history_state.go b/triedb/pathdb/history_state.go index 9d1e4dfb09..733c9aba2a 100644 --- a/triedb/pathdb/history_state.go +++ b/triedb/pathdb/history_state.go @@ -535,6 +535,178 @@ func (h *stateHistory) decode(accountData, storageData, accountIndexes, storageI return nil } +// decodeAccount selectively decodes only the specified account from the byte streams. +func (h *stateHistory) decodeAccount(accountData, accountIndexes []byte, targetAddr common.Address) error { + h.initializeEmptyState() + + count := len(accountIndexes) / accountIndexSize + if count == 0 { + return nil + } + + if err := (&decoder{accountData: accountData, accountIndexes: accountIndexes}).verify(); err != nil { + return err + } + + targetIndex := h.binarySearchAccount(accountIndexes, count, targetAddr) + if targetIndex < 0 { + return nil + } + + return h.extractAccountData(accountData, accountIndexes, targetIndex, targetAddr) +} + +// initializeEmptyState initializes all state maps to empty +func (h *stateHistory) initializeEmptyState() { + h.accounts = make(map[common.Address][]byte) + h.accountList = []common.Address{} + h.storages = make(map[common.Address]map[common.Hash][]byte) + h.storageList = make(map[common.Address][]common.Hash) +} + +// binarySearchAccount performs binary search to find the target account index +func (h *stateHistory) binarySearchAccount(accountIndexes []byte, count int, targetAddr common.Address) int { + left, right := 0, count-1 + + for left <= right { + mid := (left + right) / 2 + var index accountIndex + index.decode(accountIndexes[mid*accountIndexSize : (mid+1)*accountIndexSize]) + + switch bytes.Compare(index.address.Bytes(), targetAddr.Bytes()) { + case 0: + return mid + case -1: + left = mid + 1 + case 1: + right = mid - 1 + } + } + return -1 +} + +// extractAccountData extracts and sets account data for the target account +func (h *stateHistory) extractAccountData(accountData, accountIndexes []byte, targetIndex int, targetAddr common.Address) error { + var index accountIndex + index.decode(accountIndexes[targetIndex*accountIndexSize : (targetIndex+1)*accountIndexSize]) + + end := index.offset + uint32(index.length) + if uint32(len(accountData)) < end { + return errors.New("account data buffer is corrupted") + } + + h.accounts[targetAddr] = accountData[index.offset:end] + h.accountList = []common.Address{targetAddr} + return nil +} + +// decodeStorage decodes the specified account's storage slot. +func (h *stateHistory) decodeStorage(accountIndexes, storageIndexes, storageData []byte, targetAddr common.Address, targetSlot common.Hash) error { + h.initializeEmptyState() + + count := len(accountIndexes) / accountIndexSize + if count == 0 { + return nil + } + + targetAccountIdx := h.binarySearchAccount(accountIndexes, count, targetAddr) + if targetAccountIdx < 0 { + return nil + } + + var accountIdx accountIndex + accountIdx.decode(accountIndexes[targetAccountIdx*accountIndexSize : (targetAccountIdx+1)*accountIndexSize]) + if accountIdx.storageSlots == 0 { + return nil + } + + return h.extractStorageSlot(storageIndexes, storageData, &accountIdx, targetAddr, targetSlot) +} + +// extractStorageSlot finds and extracts the target storage slot data using binary search +func (h *stateHistory) extractStorageSlot(storageIndexes, storageData []byte, accountIdx *accountIndex, targetAddr common.Address, targetSlot common.Hash) error { + slotPos := h.binarySearchStorageSlot(storageIndexes, accountIdx, targetSlot) + if slotPos < 0 { + return nil + } + + slotIdx := h.getSlotIndex(storageIndexes, accountIdx.storageOffset+uint32(slotPos)) + if slotIdx == nil { + return nil + } + + return h.setStorageData(storageData, slotIdx, targetAddr) +} + +// binarySearchStorageSlot performs binary search to find the target storage slot index within an account's slots +func (h *stateHistory) binarySearchStorageSlot(storageIndexes []byte, accountIdx *accountIndex, targetSlot common.Hash) int { + left, right := 0, int(accountIdx.storageSlots)-1 + + for left <= right { + mid := (left + right) / 2 + slotIdx := h.getSlotIndex(storageIndexes, accountIdx.storageOffset+uint32(mid)) + if slotIdx == nil { + return -1 + } + + var cmp int + if h.meta.version == stateHistoryV0 { + targetSlotHash := crypto.Keccak256Hash(targetSlot.Bytes()) + cmp = bytes.Compare(slotIdx.id.Bytes(), targetSlotHash.Bytes()) + } else { + cmp = bytes.Compare(slotIdx.id.Bytes(), targetSlot.Bytes()) + } + + switch cmp { + case 0: + return mid + case -1: + left = mid + 1 + case 1: + right = mid - 1 + } + } + return -1 +} + +// getSlotIndex safely extracts a slot index at the given position +func (h *stateHistory) getSlotIndex(storageIndexes []byte, position uint32) *slotIndex { + start := position * uint32(slotIndexSize) + end := start + uint32(slotIndexSize) + + if uint32(len(storageIndexes)) < end { + return nil + } + + var index slotIndex + index.decode(storageIndexes[start:end]) + return &index +} + +// isTargetSlot checks if the slot index matches the target slot (handles v0/v1 compatibility) +func (h *stateHistory) isTargetSlot(slotIdx *slotIndex, targetSlot common.Hash) bool { + if h.meta.version == stateHistoryV0 { + return slotIdx.id == crypto.Keccak256Hash(targetSlot.Bytes()) + } + return slotIdx.id == targetSlot +} + +// setStorageData safely extracts and sets the storage data +func (h *stateHistory) setStorageData(storageData []byte, slotIdx *slotIndex, targetAddr common.Address) error { + end := slotIdx.offset + uint32(slotIdx.length) + if uint32(len(storageData)) < end { + return errors.New("storage data buffer is corrupted") + } + + h.storages = map[common.Address]map[common.Hash][]byte{ + targetAddr: {slotIdx.id: storageData[slotIdx.offset:end]}, + } + h.storageList = map[common.Address][]common.Hash{ + targetAddr: {slotIdx.id}, + } + return nil +} + // readStateHistoryMeta reads the metadata of state history with the specified id. func readStateHistoryMeta(reader ethdb.AncientReader, id uint64) (*meta, error) { data := rawdb.ReadStateHistoryMeta(reader, id) @@ -566,6 +738,40 @@ func readStateHistory(reader ethdb.AncientReader, id uint64) (*stateHistory, err return &h, nil } +// readAccountHistory reads account state history +func readAccountHistory(reader ethdb.AncientReader, id uint64, targetAddr common.Address) (*stateHistory, error) { + mData, accountIndexes, _, accountData, _, err := rawdb.ReadStateHistory(reader, id) + if err != nil { + return nil, err + } + var m meta + if err := m.decode(mData); err != nil { + return nil, err + } + h := stateHistory{meta: &m} + if err := h.decodeAccount(accountData, accountIndexes, targetAddr); err != nil { + return nil, err + } + return &h, nil +} + +// readSlotHistory reads slot state history +func readSlotHistory(reader ethdb.AncientReader, id uint64, targetAddr common.Address, targetSlot common.Hash) (*stateHistory, error) { + mData, accountIndexes, storageIndexes, _, storageData, err := rawdb.ReadStateHistory(reader, id) + if err != nil { + return nil, err + } + var m meta + if err := m.decode(mData); err != nil { + return nil, err + } + h := stateHistory{meta: &m} + if err := h.decodeStorage(accountIndexes, storageIndexes, storageData, targetAddr, targetSlot); err != nil { + return nil, err + } + return &h, nil +} + // readStateHistories reads a list of state history records within the specified range. func readStateHistories(freezer ethdb.AncientReader, start uint64, count uint64) ([]history, error) { var histories []history