mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
443 lines
14 KiB
Go
443 lines
14 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package merkledb
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
|
|
"github.com/luxfi/container/buffer"
|
|
"github.com/luxfi/container/heap"
|
|
"github.com/luxfi/container/maybe"
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
var (
|
|
ErrInsufficientHistory = errors.New("insufficient history to generate proof")
|
|
ErrNoEndRoot = fmt.Errorf("%w: end root not found", ErrInsufficientHistory)
|
|
)
|
|
|
|
// stores previous trie states
|
|
type trieHistory struct {
|
|
// Root ID --> The most recent insert number resulting in [rootID].
|
|
lastChangesInsertNumber map[ids.ID]uint64
|
|
|
|
// Maximum number of previous roots/changes to store in [history].
|
|
maxHistoryLen int
|
|
|
|
// Contains the history.
|
|
// Sorted by increasing order of insertion.
|
|
// Contains at most [maxHistoryLen] values.
|
|
history buffer.Deque[*changeSummaryAndInsertNumber]
|
|
}
|
|
|
|
// Tracks the beginning and ending state of a value.
|
|
type change[T any] struct {
|
|
before T
|
|
after T
|
|
}
|
|
|
|
// Wrapper around a changeSummary that allows comparison
|
|
// of when the change was made.
|
|
type changeSummaryAndInsertNumber struct {
|
|
*changeSummary
|
|
// Another changeSummaryAndInsertNumber with a greater
|
|
// [insertNumber] means that change was after this one.
|
|
insertNumber uint64
|
|
}
|
|
|
|
// Tracks all the node and value changes that resulted in the rootID.
|
|
type changeSummary struct {
|
|
// The ID of the trie after these changes.
|
|
rootID ids.ID
|
|
// The root before/after this change.
|
|
// Set in [applyValueChanges].
|
|
rootChange change[maybe.Maybe[*node]]
|
|
nodes map[Key]*change[*node]
|
|
|
|
keyChanges map[Key]*change[maybe.Maybe[[]byte]]
|
|
sortedKeys []Key
|
|
}
|
|
|
|
func newChangeSummary(estimatedSize int) *changeSummary {
|
|
return &changeSummary{
|
|
nodes: make(map[Key]*change[*node], estimatedSize),
|
|
keyChanges: make(map[Key]*change[maybe.Maybe[[]byte]], estimatedSize),
|
|
sortedKeys: make([]Key, 0, estimatedSize),
|
|
rootChange: change[maybe.Maybe[*node]]{},
|
|
}
|
|
}
|
|
|
|
func newTrieHistory(maxHistoryLookback int) *trieHistory {
|
|
return &trieHistory{
|
|
maxHistoryLen: maxHistoryLookback,
|
|
history: buffer.NewUnboundedDeque[*changeSummaryAndInsertNumber](maxHistoryLookback),
|
|
lastChangesInsertNumber: make(map[ids.ID]uint64),
|
|
}
|
|
}
|
|
|
|
func (th *trieHistory) getNextInsertNumber() uint64 {
|
|
if oldestEntry, ok := th.history.PeekRight(); ok {
|
|
return oldestEntry.insertNumber + 1
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
func (th *trieHistory) getRootChanges(root ids.ID) (*changeSummaryAndInsertNumber, bool) {
|
|
insertNumber, ok := th.lastChangesInsertNumber[root]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
|
|
mostRecentChangeInsertNumber := th.getNextInsertNumber() - 1
|
|
|
|
// The difference between the last index in [th.history] and the index of [rootChanges].
|
|
rootToMostRecentOffset := int(mostRecentChangeInsertNumber - insertNumber)
|
|
|
|
mostRecentChangeIndex := th.history.Len() - 1
|
|
|
|
// The index in [th.history] of the latest change resulting in [root].
|
|
index := mostRecentChangeIndex - rootToMostRecentOffset
|
|
|
|
rootChanges, ok := th.history.Index(index)
|
|
if !ok {
|
|
panic("root changes not found in history")
|
|
}
|
|
|
|
return rootChanges, true
|
|
}
|
|
|
|
type valueChange struct {
|
|
key Key
|
|
change *change[maybe.Maybe[[]byte]]
|
|
}
|
|
|
|
// Returns up to [maxLength] sorted changes with keys in
|
|
// [start, end] that occurred between [startRoot] and [endRoot].
|
|
// If [start] is Nothing, there's no lower bound on the range.
|
|
// If [end] is Nothing, there's no upper bound on the range.
|
|
// Returns [ErrInsufficientHistory] if the history is insufficient
|
|
// to generate the proof.
|
|
// Returns [ErrNoEndRoot], which wraps [ErrInsufficientHistory], if
|
|
// the [endRoot] isn't in the history.
|
|
func (th *trieHistory) getValueChanges(
|
|
startRoot ids.ID,
|
|
endRoot ids.ID,
|
|
start maybe.Maybe[[]byte],
|
|
end maybe.Maybe[[]byte],
|
|
maxLength int,
|
|
) ([]valueChange, error) {
|
|
if maxLength <= 0 {
|
|
return nil, fmt.Errorf("%w but was %d", ErrInvalidMaxLength, maxLength)
|
|
}
|
|
|
|
if startRoot == endRoot {
|
|
return []valueChange{}, nil
|
|
}
|
|
|
|
// [endRootChanges] is the last change in the history resulting in [endRoot].
|
|
endRootChanges, ok := th.getRootChanges(endRoot)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: %s", ErrNoEndRoot, endRoot)
|
|
}
|
|
|
|
// Confirm there's a change resulting in [startRoot] before
|
|
// a change resulting in [endRoot] in the history.
|
|
// [startRootChanges] is the last appearance of [startRoot].
|
|
startRootChanges, ok := th.getRootChanges(startRoot)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: start root %s not found", ErrInsufficientHistory, startRoot)
|
|
}
|
|
|
|
var (
|
|
// The insert number of the last element in [th.history].
|
|
mostRecentChangeInsertNumber = th.getNextInsertNumber() - 1
|
|
|
|
// The index within [th.history] of its last element.
|
|
mostRecentChangeIndex = th.history.Len() - 1
|
|
|
|
// The difference between the last index in [th.history] and the index of [endRootChanges].
|
|
endToMostRecentOffset = int(mostRecentChangeInsertNumber - endRootChanges.insertNumber)
|
|
|
|
// The index in [th.history] of the latest change resulting in [endRoot].
|
|
endRootIndex = mostRecentChangeIndex - endToMostRecentOffset
|
|
)
|
|
|
|
if startRootChanges.insertNumber > endRootChanges.insertNumber {
|
|
// [startRootChanges] happened after [endRootChanges].
|
|
// However, that is just the *latest* change resulting in [startRoot].
|
|
// Attempt to find a change resulting in [startRoot] before [endRootChanges].
|
|
//
|
|
// Translate the insert number to the index in [th.history] so we can iterate
|
|
// backward from [endRootChanges].
|
|
for i := endRootIndex - 1; i >= 0; i-- {
|
|
changes, _ := th.history.Index(i)
|
|
|
|
if changes.rootID == startRoot {
|
|
// [startRootChanges] is now the last change resulting in
|
|
// [startRoot] before [endRootChanges].
|
|
startRootChanges = changes
|
|
break
|
|
}
|
|
|
|
if i == 0 {
|
|
return nil, fmt.Errorf(
|
|
"%w: start root %s not found before end root %s",
|
|
ErrInsufficientHistory, startRoot, endRoot,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// historyChangesIndex is used for tracking keyChanges index from each historical root.
|
|
type historyChangesIndex struct {
|
|
changes *changeSummaryAndInsertNumber
|
|
kvChangeIndex int
|
|
}
|
|
|
|
// historyChangesIndexHeap is used to traverse the changes sorted by ASC [key] and ASC [insertNumber].
|
|
historyChangesIndexHeap := heap.NewQueue[historyChangesIndex](func(a, b historyChangesIndex) bool {
|
|
keyComparison := a.changes.sortedKeys[a.kvChangeIndex].Compare(b.changes.sortedKeys[b.kvChangeIndex])
|
|
if keyComparison != 0 {
|
|
return keyComparison < 0
|
|
}
|
|
|
|
return a.changes.insertNumber < b.changes.insertNumber
|
|
})
|
|
|
|
var (
|
|
startKey = maybe.Bind(start, ToKey)
|
|
endKey = maybe.Bind(end, ToKey)
|
|
|
|
// For each element in the history in the range between [startRoot]'s
|
|
// last appearance (exclusive) and [endRoot]'s last appearance (inclusive),
|
|
// add the changes to keys in [start, end] to [combinedChanges].
|
|
// Only the key-value pairs with the greatest [maxLength] keys will be kept.
|
|
// The difference between the index of [startRootChanges] and [endRootChanges] in [th.history].
|
|
startToEndOffset = int(endRootChanges.insertNumber - startRootChanges.insertNumber)
|
|
|
|
// The index of the last change resulting in [startRoot]
|
|
// which occurs before [endRootChanges].
|
|
startRootIndex = endRootIndex - startToEndOffset
|
|
)
|
|
|
|
// Push in the heap first key in [startKey, endKey] for each historical root.
|
|
for i := startRootIndex + 1; i <= endRootIndex; i++ {
|
|
historyChanges, ok := th.history.Index(i)
|
|
if !ok {
|
|
panic(fmt.Sprintf("missing history changes at index %d", i))
|
|
}
|
|
|
|
startKeyIndex := 0
|
|
if startKey.HasValue() {
|
|
// Binary search for [startKey] index, or the index where [startKey] would appear.
|
|
startKeyIndex, _ = slices.BinarySearchFunc(historyChanges.sortedKeys, startKey, func(k Key, m maybe.Maybe[Key]) int {
|
|
return k.Compare(m.Value())
|
|
})
|
|
|
|
if startKeyIndex >= len(historyChanges.keyChanges) {
|
|
// [startKey] is after last key of [sortedKeyChanges].
|
|
continue
|
|
}
|
|
}
|
|
|
|
keyChange := historyChanges.sortedKeys[startKeyIndex]
|
|
if end.HasValue() && keyChange.Greater(endKey.Value()) {
|
|
// [keyChange] is after [endKey].
|
|
continue
|
|
}
|
|
|
|
// [startKeyIndex] is the index of the first key in [startKey, endKey] from [sortedKeyChanges].
|
|
historyChangesIndexHeap.Push(historyChangesIndex{
|
|
changes: historyChanges,
|
|
kvChangeIndex: startKeyIndex,
|
|
})
|
|
}
|
|
|
|
var (
|
|
combinedKeyChanges = make([]valueChange, 0, maxLength)
|
|
|
|
// Used for combining the changes of all the historical changes, for the current smallest key.
|
|
currentKeyChange *valueChange
|
|
)
|
|
|
|
for historyChangesIndexHeap.Len() > 0 {
|
|
historyRootChanges, _ := historyChangesIndexHeap.Pop()
|
|
kvChangeKey := historyRootChanges.changes.sortedKeys[historyRootChanges.kvChangeIndex]
|
|
kvChange := historyRootChanges.changes.keyChanges[kvChangeKey]
|
|
|
|
if end.HasValue() && kvChangeKey.Greater(endKey.Value()) {
|
|
// Skip processing the current [historyRootChanges] if we are after [endKey].
|
|
continue
|
|
}
|
|
|
|
if len(historyRootChanges.changes.keyChanges) > 1+historyRootChanges.kvChangeIndex {
|
|
// If there are remaining changes in the current [historyRootChanges], push to minheap.
|
|
historyRootChanges.kvChangeIndex++
|
|
historyChangesIndexHeap.Push(historyRootChanges)
|
|
}
|
|
|
|
if currentKeyChange != nil {
|
|
if currentKeyChange.key.value == kvChangeKey.value {
|
|
// Same key, update [after] value.
|
|
currentKeyChange.change.after = kvChange.after
|
|
|
|
continue
|
|
}
|
|
|
|
// New key
|
|
|
|
// Add the last [currentKeyChange] to [combinedKeyChanges] if there is an actual change.
|
|
if !maybe.Equal(currentKeyChange.change.before, currentKeyChange.change.after, bytes.Equal) {
|
|
combinedKeyChanges = append(combinedKeyChanges, *currentKeyChange)
|
|
|
|
if len(combinedKeyChanges) >= maxLength {
|
|
// If we have [maxLength] changes, we can return the current [combinedKeyChanges].
|
|
return combinedKeyChanges, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
currentKeyChange = &valueChange{
|
|
change: &change[maybe.Maybe[[]byte]]{
|
|
before: kvChange.before,
|
|
after: kvChange.after,
|
|
},
|
|
key: kvChangeKey,
|
|
}
|
|
}
|
|
|
|
if currentKeyChange != nil {
|
|
// Add the last [currentKeyChange] to [combinedKeyChanges] if there is an actual change.
|
|
if !maybe.Equal(currentKeyChange.change.before, currentKeyChange.change.after, bytes.Equal) {
|
|
combinedKeyChanges = append(combinedKeyChanges, *currentKeyChange)
|
|
}
|
|
}
|
|
|
|
return combinedKeyChanges, nil
|
|
}
|
|
|
|
// Returns the changes to go from the current trie state back to the requested [rootID]
|
|
// for the keys in [start, end].
|
|
// If [start] is Nothing, all keys are considered > [start].
|
|
// If [end] is Nothing, all keys are considered < [end].
|
|
func (th *trieHistory) getChangesToGetToRoot(rootID ids.ID, start maybe.Maybe[[]byte], end maybe.Maybe[[]byte]) (*changeSummary, error) {
|
|
// [lastRootChange] is the last change in the history resulting in [rootID].
|
|
lastRootChange, ok := th.getRootChanges(rootID)
|
|
if !ok {
|
|
return nil, ErrInsufficientHistory
|
|
}
|
|
|
|
var (
|
|
startKey = maybe.Bind(start, ToKey)
|
|
endKey = maybe.Bind(end, ToKey)
|
|
combinedChanges = newChangeSummary(defaultPreallocationSize)
|
|
mostRecentChangeInsertNumber = th.getNextInsertNumber() - 1
|
|
mostRecentChangeIndex = th.history.Len() - 1
|
|
offset = int(mostRecentChangeInsertNumber - lastRootChange.insertNumber)
|
|
lastRootChangeIndex = mostRecentChangeIndex - offset
|
|
keyChanges = map[Key]*change[maybe.Maybe[[]byte]]{}
|
|
)
|
|
|
|
// Go backward from the most recent change in the history up to but
|
|
// not including the last change resulting in [rootID].
|
|
// Record each change in [combinedChanges].
|
|
for i := mostRecentChangeIndex; i > lastRootChangeIndex; i-- {
|
|
changes, _ := th.history.Index(i)
|
|
|
|
if i == mostRecentChangeIndex {
|
|
combinedChanges.rootChange.before = changes.rootChange.after
|
|
}
|
|
if i == lastRootChangeIndex+1 {
|
|
combinedChanges.rootChange.after = changes.rootChange.before
|
|
}
|
|
|
|
for key, changedNode := range changes.nodes {
|
|
combinedChanges.nodes[key] = &change[*node]{
|
|
after: changedNode.before,
|
|
}
|
|
}
|
|
|
|
startKeyIndex := 0
|
|
if startKey.HasValue() {
|
|
// Binary search for [startKey] index.
|
|
startKeyIndex, _ = slices.BinarySearchFunc(changes.sortedKeys, startKey, func(k Key, m maybe.Maybe[Key]) int {
|
|
return k.Compare(m.Value())
|
|
})
|
|
}
|
|
|
|
for _, key := range changes.sortedKeys[startKeyIndex:] {
|
|
if end.HasValue() && key.Greater(endKey.Value()) {
|
|
break
|
|
}
|
|
|
|
if existing, ok := keyChanges[key]; ok {
|
|
// Update existing [after] with current [before]
|
|
existing.after = changes.keyChanges[key].before
|
|
|
|
continue
|
|
}
|
|
|
|
keyChanges[key] = &change[maybe.Maybe[[]byte]]{
|
|
before: changes.keyChanges[key].after,
|
|
after: changes.keyChanges[key].before,
|
|
}
|
|
}
|
|
}
|
|
|
|
sortedKeys := slices.Collect(maps.Keys(keyChanges))
|
|
slices.SortFunc(sortedKeys, func(a, b Key) int {
|
|
return a.Compare(b)
|
|
})
|
|
|
|
for _, key := range sortedKeys {
|
|
if maybe.Equal(keyChanges[key].before, keyChanges[key].after, bytes.Equal) {
|
|
// Remove changed key, if there is a no-op.
|
|
delete(keyChanges, key)
|
|
|
|
continue
|
|
}
|
|
|
|
combinedChanges.keyChanges[key] = keyChanges[key]
|
|
combinedChanges.sortedKeys = append(combinedChanges.sortedKeys, key)
|
|
}
|
|
|
|
return combinedChanges, nil
|
|
}
|
|
|
|
// record the provided set of changes in the history
|
|
func (th *trieHistory) record(changes *changeSummary) {
|
|
// we aren't recording history so noop
|
|
if th.maxHistoryLen == 0 {
|
|
return
|
|
}
|
|
|
|
if th.history.Len() == th.maxHistoryLen {
|
|
// This change causes us to go over our lookback limit.
|
|
// Remove the oldest set of changes.
|
|
oldestEntry, _ := th.history.PopLeft()
|
|
|
|
if th.lastChangesInsertNumber[oldestEntry.rootID] == oldestEntry.insertNumber {
|
|
// The removed change was the most recent resulting in this root ID.
|
|
// (Note: this if is for situations when the same root could appear twice in history)
|
|
delete(th.lastChangesInsertNumber, oldestEntry.rootID)
|
|
}
|
|
}
|
|
|
|
changesAndIndex := &changeSummaryAndInsertNumber{
|
|
changeSummary: changes,
|
|
insertNumber: th.getNextInsertNumber(),
|
|
}
|
|
|
|
// Add [changes] to the sorted change list.
|
|
_ = th.history.PushRight(changesAndIndex)
|
|
|
|
// Mark that this is the most recent change resulting in [changes.rootID].
|
|
th.lastChangesInsertNumber[changes.rootID] = changesAndIndex.insertNumber
|
|
}
|