mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
273 lines
8.0 KiB
Go
273 lines
8.0 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package merkledb
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
|
|
"github.com/luxfi/database"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/container/maybe"
|
|
)
|
|
|
|
type ViewChanges struct {
|
|
BatchOps []database.BatchOp
|
|
MapOps map[string]maybe.Maybe[[]byte]
|
|
// ConsumeBytes when set to true will skip copying of bytes and assume
|
|
// ownership of the provided bytes.
|
|
ConsumeBytes bool
|
|
}
|
|
|
|
type MerkleRootGetter interface {
|
|
// GetMerkleRoot returns the merkle root of the trie.
|
|
// Returns ids.Empty if the trie is empty.
|
|
GetMerkleRoot(ctx context.Context) (ids.ID, error)
|
|
}
|
|
|
|
type ProofGetter interface {
|
|
// GetProof generates a proof of the value associated with a particular key,
|
|
// or a proof of its absence from the trie
|
|
// Returns ErrEmptyProof if the trie is empty.
|
|
GetProof(ctx context.Context, keyBytes []byte) (*Proof, error)
|
|
}
|
|
|
|
type trieInternals interface {
|
|
// get the value associated with the key in path form
|
|
// database.ErrNotFound if the key is not present
|
|
getValue(key Key) ([]byte, error)
|
|
|
|
// get an editable copy of the node with the given key path
|
|
// hasValue indicates which db to look in (value or intermediate)
|
|
getEditableNode(key Key, hasValue bool) (*node, error)
|
|
|
|
// get the node associated with the key without locking
|
|
getNode(key Key, hasValue bool) (*node, error)
|
|
|
|
// If this trie is non-empty, returns the root node.
|
|
// Must be copied before modification.
|
|
// Otherwise returns Nothing.
|
|
getRoot() maybe.Maybe[*node]
|
|
|
|
getTokenSize() int
|
|
}
|
|
|
|
type Trie interface {
|
|
trieInternals
|
|
MerkleRootGetter
|
|
ProofGetter
|
|
database.Iteratee
|
|
|
|
// GetValue gets the value associated with the specified key
|
|
// database.ErrNotFound if the key is not present
|
|
GetValue(ctx context.Context, key []byte) ([]byte, error)
|
|
|
|
// GetValues gets the values associated with the specified keys
|
|
// database.ErrNotFound if the key is not present
|
|
GetValues(ctx context.Context, keys [][]byte) ([][]byte, []error)
|
|
|
|
// GetRangeProof returns a proof of up to [maxLength] key-value pairs with
|
|
// keys in range [start, end].
|
|
// 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 ErrEmptyProof if the trie is empty.
|
|
GetRangeProof(ctx context.Context, start maybe.Maybe[[]byte], end maybe.Maybe[[]byte], maxLength int) (*RangeProof, error)
|
|
|
|
// NewView returns a new view on top of this Trie where the passed changes
|
|
// have been applied.
|
|
NewView(
|
|
ctx context.Context,
|
|
changes ViewChanges,
|
|
) (View, error)
|
|
}
|
|
|
|
type View interface {
|
|
Trie
|
|
|
|
// CommitToDB writes the changes in this view to the database.
|
|
// Takes the DB commit lock.
|
|
CommitToDB(ctx context.Context) error
|
|
}
|
|
|
|
// Calls [visitNode] on the nodes along the path to [key].
|
|
// The first node is the root, and the last node is either the node with the
|
|
// given [key], if it's in the trie, or the node with the largest prefix of
|
|
// the [key] if it isn't in the trie.
|
|
// Assumes [t] doesn't change while this function is running.
|
|
func visitPathToKey(t Trie, key Key, visitNode func(*node) error) error {
|
|
maybeRoot := t.getRoot()
|
|
if maybeRoot.IsNothing() {
|
|
return nil
|
|
}
|
|
root := maybeRoot.Value()
|
|
if !key.HasPrefix(root.key) {
|
|
return nil
|
|
}
|
|
var (
|
|
// all node paths start at the root
|
|
currentNode = root
|
|
tokenSize = t.getTokenSize()
|
|
err error
|
|
)
|
|
if err := visitNode(currentNode); err != nil {
|
|
return err
|
|
}
|
|
// while the entire path hasn't been matched
|
|
for currentNode.key.length < key.length {
|
|
// confirm that a child exists and grab its ID before attempting to load it
|
|
nextChildEntry, hasChild := currentNode.children[key.Token(currentNode.key.length, tokenSize)]
|
|
|
|
if !hasChild || !key.iteratedHasPrefix(nextChildEntry.compressedKey, currentNode.key.length+tokenSize, tokenSize) {
|
|
// there was no child along the path or the child that was there doesn't match the remaining path
|
|
return nil
|
|
}
|
|
// grab the next node along the path
|
|
currentNode, err = t.getNode(key.Take(currentNode.key.length+tokenSize+nextChildEntry.compressedKey.length), nextChildEntry.hasValue)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := visitNode(currentNode); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Returns a proof that [key] is in or not in trie [t].
|
|
// Assumes [t] doesn't change while this function is running.
|
|
func getProof(t Trie, key []byte) (*Proof, error) {
|
|
root := t.getRoot()
|
|
if root.IsNothing() {
|
|
return nil, ErrEmptyProof
|
|
}
|
|
|
|
proof := &Proof{
|
|
Key: ToKey(key),
|
|
}
|
|
|
|
var closestNode *node
|
|
if err := visitPathToKey(t, proof.Key, func(n *node) error {
|
|
closestNode = n
|
|
// From root --> node from left --> right.
|
|
proof.Path = append(proof.Path, n.asProofNode())
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(proof.Path) == 0 {
|
|
// No key in [t] is a prefix of [key].
|
|
// The root alone proves that [key] isn't in [t].
|
|
proof.Path = append(proof.Path, root.Value().asProofNode())
|
|
return proof, nil
|
|
}
|
|
|
|
if closestNode.key == proof.Key {
|
|
// There is a node with the given [key].
|
|
proof.Value = maybe.Bind(closestNode.value, slices.Clone[[]byte])
|
|
return proof, nil
|
|
}
|
|
|
|
// There is no node with the given [key].
|
|
// If there is a child at the index where the node would be
|
|
// if it existed, include that child in the proof.
|
|
nextIndex := proof.Key.Token(closestNode.key.length, t.getTokenSize())
|
|
child, ok := closestNode.children[nextIndex]
|
|
if !ok {
|
|
return proof, nil
|
|
}
|
|
|
|
childNode, err := t.getNode(
|
|
closestNode.key.Extend(ToToken(nextIndex, t.getTokenSize()), child.compressedKey),
|
|
child.hasValue,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
proof.Path = append(proof.Path, childNode.asProofNode())
|
|
return proof, nil
|
|
}
|
|
|
|
// getRangeProof returns a range proof for (at least part of) the key range [start, end].
|
|
// The returned proof's [KeyValues] has at most [maxLength] values.
|
|
// [maxLength] must be > 0.
|
|
// Assumes [t] doesn't change while this function is running.
|
|
func getRangeProof(
|
|
t Trie,
|
|
start maybe.Maybe[[]byte],
|
|
end maybe.Maybe[[]byte],
|
|
maxLength int,
|
|
) (*RangeProof, error) {
|
|
switch {
|
|
case start.HasValue() && end.HasValue() && bytes.Compare(start.Value(), end.Value()) == 1:
|
|
return nil, ErrStartAfterEnd
|
|
case maxLength <= 0:
|
|
return nil, fmt.Errorf("%w but was %d", ErrInvalidMaxLength, maxLength)
|
|
case t.getRoot().IsNothing():
|
|
return nil, ErrEmptyProof
|
|
}
|
|
|
|
result := RangeProof{
|
|
KeyChanges: make([]KeyChange, 0, initKeyValuesSize),
|
|
}
|
|
it := t.NewIteratorWithStart(start.Value())
|
|
for it.Next() && len(result.KeyChanges) < maxLength && (end.IsNothing() || bytes.Compare(it.Key(), end.Value()) <= 0) {
|
|
// clone the value to prevent editing of the values stored within the trie
|
|
result.KeyChanges = append(result.KeyChanges, KeyChange{
|
|
Key: it.Key(),
|
|
Value: maybe.Some(slices.Clone(it.Value())),
|
|
})
|
|
}
|
|
it.Release()
|
|
if err := it.Error(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// This proof may not contain all key-value pairs in [start, end] due to size limitations.
|
|
// The end proof we provide should be for the last key-value pair in the proof, not for
|
|
// the last key-value pair requested, which may not be in this proof.
|
|
var (
|
|
endProof *Proof
|
|
err error
|
|
)
|
|
if len(result.KeyChanges) > 0 {
|
|
// [endProof] => inclusion proof for the largest key
|
|
greatestKey := result.KeyChanges[len(result.KeyChanges)-1].Key
|
|
endProof, err = getProof(t, greatestKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if end.HasValue() {
|
|
// [endProof] => exclusion proof for the [end] key
|
|
endProof, err = getProof(t, end.Value())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if endProof != nil {
|
|
result.EndProof = endProof.Path
|
|
}
|
|
|
|
if start.HasValue() {
|
|
// [startProof] => inclusion/exclusion proof for [start] key
|
|
startProof, err := getProof(t, start.Value())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.StartProof = startProof.Path
|
|
|
|
// strip out any common nodes to reduce proof size
|
|
i := 0
|
|
for ; i < len(result.StartProof) &&
|
|
i < len(result.EndProof) &&
|
|
result.StartProof[i].Key == result.EndProof[i].Key; i++ {
|
|
}
|
|
result.StartProof = result.StartProof[i:]
|
|
}
|
|
|
|
return &result, nil
|
|
}
|