mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-18 19:00:46 +00:00
nomt/merkle: add Phase 2 merkle engine (PageWalker, PageSet, ElidedChildren)
Implement the in-memory batch update engine for the NOMT binary merkle trie: - elided.go: ElidedChildren 64-bit bitfield for tracking elided child pages - pageset.go: PageSet interface + MemoryPageSet in-memory implementation - pagewalker.go: PageWalker left-to-right walker with partial compaction - AdvanceAndReplace: replace terminal nodes with sub-tries - AdvanceAndPlaceNode: place pre-computed child page roots - Conclude: finalize walk and return new root + updated pages - compactUp/compactStep: hash upward with leaf/terminator compaction - core/triepos.go: add SharedDepth method needed by PageWalker Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7aebfb3c71
commit
88fd10529f
7 changed files with 1208 additions and 0 deletions
|
|
@ -184,6 +184,20 @@ func (p *TriePosition) SiblingIndex() int {
|
||||||
return siblingIndex(p.nodeIndex)
|
return siblingIndex(p.nodeIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SharedDepth returns the number of leading path bits shared between two
|
||||||
|
// TriePositions, considering only bits up to the shorter depth.
|
||||||
|
func (p *TriePosition) SharedDepth(other *TriePosition) int {
|
||||||
|
maxBits := min(int(p.depth), int(other.depth))
|
||||||
|
for i := range maxBits {
|
||||||
|
pBit := (p.path[i/8] >> (7 - i%8)) & 1
|
||||||
|
oBit := (other.path[i/8] >> (7 - i%8)) & 1
|
||||||
|
if pBit != oBit {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxBits
|
||||||
|
}
|
||||||
|
|
||||||
// --- internal helpers ---
|
// --- internal helpers ---
|
||||||
|
|
||||||
// computeNodeIndex converts a page-local bit path to a level-order node index.
|
// computeNodeIndex converts a page-local bit path to a level-order node index.
|
||||||
|
|
|
||||||
54
nomt/merkle/elided.go
Normal file
54
nomt/merkle/elided.go
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
// Package merkle implements the in-memory batch update engine for the NOMT
|
||||||
|
// binary merkle trie. It processes sorted key-value changes and produces
|
||||||
|
// updated pages plus a new root hash.
|
||||||
|
package merkle
|
||||||
|
|
||||||
|
import "encoding/binary"
|
||||||
|
|
||||||
|
// ElidedChildren is a 64-bit bitfield tracking which of a page's 64 child
|
||||||
|
// pages are elided (not stored on disk and reconstructed on-the-fly).
|
||||||
|
type ElidedChildren struct {
|
||||||
|
elided uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewElidedChildren returns an empty ElidedChildren with no children elided.
|
||||||
|
func NewElidedChildren() ElidedChildren {
|
||||||
|
return ElidedChildren{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ElidedChildrenFromBytes decodes an ElidedChildren from its 8-byte
|
||||||
|
// little-endian representation.
|
||||||
|
func ElidedChildrenFromBytes(raw [8]byte) ElidedChildren {
|
||||||
|
return ElidedChildren{elided: binary.LittleEndian.Uint64(raw[:])}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ElidedChildrenFromUint64 wraps a raw uint64 bitfield.
|
||||||
|
func ElidedChildrenFromUint64(v uint64) ElidedChildren {
|
||||||
|
return ElidedChildren{elided: v}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBytes encodes the ElidedChildren as 8 bytes (little-endian).
|
||||||
|
func (e *ElidedChildren) ToBytes() [8]byte {
|
||||||
|
var buf [8]byte
|
||||||
|
binary.LittleEndian.PutUint64(buf[:], e.elided)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetElide marks or clears the elided flag for the given child index (0-63).
|
||||||
|
func (e *ElidedChildren) SetElide(childIndex uint8, elide bool) {
|
||||||
|
if elide {
|
||||||
|
e.elided |= 1 << childIndex
|
||||||
|
} else {
|
||||||
|
e.elided &^= 1 << childIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsElided reports whether the child at the given index is elided.
|
||||||
|
func (e *ElidedChildren) IsElided(childIndex uint8) bool {
|
||||||
|
return (e.elided>>childIndex)&1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw returns the underlying uint64 bitfield.
|
||||||
|
func (e *ElidedChildren) Raw() uint64 {
|
||||||
|
return e.elided
|
||||||
|
}
|
||||||
54
nomt/merkle/elided_test.go
Normal file
54
nomt/merkle/elided_test.go
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
package merkle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestElidedChildrenNew(t *testing.T) {
|
||||||
|
ec := NewElidedChildren()
|
||||||
|
for i := range 64 {
|
||||||
|
assert.False(t, ec.IsElided(uint8(i)), "child %d", i)
|
||||||
|
}
|
||||||
|
assert.Equal(t, uint64(0), ec.Raw())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestElidedChildrenSetAndCheck(t *testing.T) {
|
||||||
|
ec := NewElidedChildren()
|
||||||
|
|
||||||
|
ec.SetElide(0, true)
|
||||||
|
assert.True(t, ec.IsElided(0))
|
||||||
|
assert.False(t, ec.IsElided(1))
|
||||||
|
|
||||||
|
ec.SetElide(63, true)
|
||||||
|
assert.True(t, ec.IsElided(63))
|
||||||
|
|
||||||
|
ec.SetElide(0, false)
|
||||||
|
assert.False(t, ec.IsElided(0))
|
||||||
|
assert.True(t, ec.IsElided(63))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestElidedChildrenRoundTrip(t *testing.T) {
|
||||||
|
ec := NewElidedChildren()
|
||||||
|
ec.SetElide(5, true)
|
||||||
|
ec.SetElide(33, true)
|
||||||
|
ec.SetElide(62, true)
|
||||||
|
|
||||||
|
encoded := ec.ToBytes()
|
||||||
|
decoded := ElidedChildrenFromBytes(encoded)
|
||||||
|
|
||||||
|
assert.True(t, decoded.IsElided(5))
|
||||||
|
assert.True(t, decoded.IsElided(33))
|
||||||
|
assert.True(t, decoded.IsElided(62))
|
||||||
|
assert.False(t, decoded.IsElided(0))
|
||||||
|
assert.Equal(t, ec.Raw(), decoded.Raw())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestElidedChildrenFromUint64(t *testing.T) {
|
||||||
|
ec := ElidedChildrenFromUint64(0xFF)
|
||||||
|
for i := range 8 {
|
||||||
|
assert.True(t, ec.IsElided(uint8(i)), "child %d", i)
|
||||||
|
}
|
||||||
|
assert.False(t, ec.IsElided(8))
|
||||||
|
}
|
||||||
113
nomt/merkle/pageset.go
Normal file
113
nomt/merkle/pageset.go
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
package merkle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ethereum/go-ethereum/nomt/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PageOriginKind discriminates the origin of a page in the PageSet.
|
||||||
|
type PageOriginKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PageOriginPersisted indicates the page was loaded from on-disk storage.
|
||||||
|
PageOriginPersisted PageOriginKind = iota
|
||||||
|
// PageOriginFresh indicates the page was freshly created (zeroed).
|
||||||
|
PageOriginFresh
|
||||||
|
)
|
||||||
|
|
||||||
|
// PageOrigin tracks where a page came from, used by the PageWalker to decide
|
||||||
|
// how to handle page elision and diff tracking.
|
||||||
|
type PageOrigin struct {
|
||||||
|
Kind PageOriginKind
|
||||||
|
}
|
||||||
|
|
||||||
|
// PageSet is the interface through which the PageWalker reads and creates
|
||||||
|
// pages during trie updates.
|
||||||
|
type PageSet interface {
|
||||||
|
// Get retrieves a page by its ID. Returns the page, its origin, and
|
||||||
|
// whether it was found.
|
||||||
|
Get(pageID core.PageID) (*core.RawPage, PageOrigin, bool)
|
||||||
|
|
||||||
|
// Contains reports whether a page exists in the set.
|
||||||
|
Contains(pageID core.PageID) bool
|
||||||
|
|
||||||
|
// Fresh creates a new zeroed page for the given ID.
|
||||||
|
Fresh(pageID core.PageID) *core.RawPage
|
||||||
|
|
||||||
|
// Insert adds or replaces a page in the set.
|
||||||
|
Insert(pageID core.PageID, page *core.RawPage, origin PageOrigin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemoryPageSet is an in-memory PageSet implementation backed by a map.
|
||||||
|
type MemoryPageSet struct {
|
||||||
|
pages map[string]memoryPageEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
type memoryPageEntry struct {
|
||||||
|
page *core.RawPage
|
||||||
|
origin PageOrigin
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMemoryPageSet creates a MemoryPageSet, optionally pre-populated with a
|
||||||
|
// root page.
|
||||||
|
func NewMemoryPageSet(withRoot bool) *MemoryPageSet {
|
||||||
|
ps := &MemoryPageSet{
|
||||||
|
pages: make(map[string]memoryPageEntry, 16),
|
||||||
|
}
|
||||||
|
if withRoot {
|
||||||
|
root := core.RootPageID()
|
||||||
|
page := new(core.RawPage)
|
||||||
|
ps.pages[pageIDKey(root)] = memoryPageEntry{
|
||||||
|
page: page,
|
||||||
|
origin: PageOrigin{Kind: PageOriginFresh},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ps
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a page from the in-memory set.
|
||||||
|
func (m *MemoryPageSet) Get(pageID core.PageID) (*core.RawPage, PageOrigin, bool) {
|
||||||
|
entry, ok := m.pages[pageIDKey(pageID)]
|
||||||
|
if !ok {
|
||||||
|
return nil, PageOrigin{}, false
|
||||||
|
}
|
||||||
|
// Return a copy so the walker can mutate freely.
|
||||||
|
pageCopy := new(core.RawPage)
|
||||||
|
*pageCopy = *entry.page
|
||||||
|
return pageCopy, entry.origin, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains reports whether the page exists.
|
||||||
|
func (m *MemoryPageSet) Contains(pageID core.PageID) bool {
|
||||||
|
_, ok := m.pages[pageIDKey(pageID)]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fresh creates a new zeroed page.
|
||||||
|
func (m *MemoryPageSet) Fresh(pageID core.PageID) *core.RawPage {
|
||||||
|
return new(core.RawPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert stores a page in the set.
|
||||||
|
func (m *MemoryPageSet) Insert(
|
||||||
|
pageID core.PageID, page *core.RawPage, origin PageOrigin,
|
||||||
|
) {
|
||||||
|
m.pages[pageIDKey(pageID)] = memoryPageEntry{page: page, origin: origin}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply applies a list of UpdatedPages into the page set, making them
|
||||||
|
// available for subsequent reads.
|
||||||
|
func (m *MemoryPageSet) Apply(updates []UpdatedPage) {
|
||||||
|
for _, up := range updates {
|
||||||
|
pageCopy := new(core.RawPage)
|
||||||
|
*pageCopy = *up.Page
|
||||||
|
m.pages[pageIDKey(up.PageID)] = memoryPageEntry{
|
||||||
|
page: pageCopy,
|
||||||
|
origin: PageOrigin{Kind: PageOriginPersisted},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageIDKey(id core.PageID) string {
|
||||||
|
encoded := id.Encode()
|
||||||
|
return string(encoded[:])
|
||||||
|
}
|
||||||
72
nomt/merkle/pageset_test.go
Normal file
72
nomt/merkle/pageset_test.go
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
package merkle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/nomt/core"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMemoryPageSetRootInit(t *testing.T) {
|
||||||
|
ps := NewMemoryPageSet(true)
|
||||||
|
root := core.RootPageID()
|
||||||
|
assert.True(t, ps.Contains(root))
|
||||||
|
|
||||||
|
page, origin, ok := ps.Get(root)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, PageOriginFresh, origin.Kind)
|
||||||
|
assert.NotNil(t, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryPageSetInsertGet(t *testing.T) {
|
||||||
|
ps := NewMemoryPageSet(false)
|
||||||
|
root := core.RootPageID()
|
||||||
|
assert.False(t, ps.Contains(root))
|
||||||
|
|
||||||
|
page := new(core.RawPage)
|
||||||
|
page.SetNodeAt(0, core.Node{0x42})
|
||||||
|
ps.Insert(root, page, PageOrigin{Kind: PageOriginPersisted})
|
||||||
|
|
||||||
|
got, origin, ok := ps.Get(root)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, PageOriginPersisted, origin.Kind)
|
||||||
|
assert.Equal(t, core.Node{0x42}, got.NodeAt(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryPageSetGetReturnsCopy(t *testing.T) {
|
||||||
|
ps := NewMemoryPageSet(false)
|
||||||
|
root := core.RootPageID()
|
||||||
|
page := new(core.RawPage)
|
||||||
|
page.SetNodeAt(0, core.Node{0x01})
|
||||||
|
ps.Insert(root, page, PageOrigin{Kind: PageOriginFresh})
|
||||||
|
|
||||||
|
got, _, _ := ps.Get(root)
|
||||||
|
got.SetNodeAt(0, core.Node{0xFF}) // mutate the copy
|
||||||
|
|
||||||
|
original, _, _ := ps.Get(root)
|
||||||
|
assert.Equal(t, core.Node{0x01}, original.NodeAt(0),
|
||||||
|
"mutation should not affect the stored page")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryPageSetFresh(t *testing.T) {
|
||||||
|
ps := NewMemoryPageSet(false)
|
||||||
|
root := core.RootPageID()
|
||||||
|
page := ps.Fresh(root)
|
||||||
|
assert.NotNil(t, page)
|
||||||
|
assert.Equal(t, core.Terminator, page.NodeAt(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryPageSetChildPage(t *testing.T) {
|
||||||
|
ps := NewMemoryPageSet(false)
|
||||||
|
root := core.RootPageID()
|
||||||
|
child, err := root.ChildPageID(5)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
page := new(core.RawPage)
|
||||||
|
page.SetNodeAt(0, core.Node{0xAB})
|
||||||
|
ps.Insert(child, page, PageOrigin{Kind: PageOriginPersisted})
|
||||||
|
|
||||||
|
assert.True(t, ps.Contains(child))
|
||||||
|
assert.False(t, ps.Contains(root))
|
||||||
|
}
|
||||||
541
nomt/merkle/pagewalker.go
Normal file
541
nomt/merkle/pagewalker.go
Normal file
|
|
@ -0,0 +1,541 @@
|
||||||
|
package merkle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ethereum/go-ethereum/nomt/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdatedPage is a page that was modified during a trie update.
|
||||||
|
type UpdatedPage struct {
|
||||||
|
PageID core.PageID
|
||||||
|
Page *core.RawPage
|
||||||
|
Diff core.PageDiff
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output is the result of concluding a PageWalker.
|
||||||
|
type Output struct {
|
||||||
|
// Root is the new root node hash (set when no parent page was supplied).
|
||||||
|
Root core.Node
|
||||||
|
// Pages is the list of all pages modified during the update.
|
||||||
|
Pages []UpdatedPage
|
||||||
|
// ChildPageRoots holds (position, node) pairs for nodes that should be
|
||||||
|
// placed in the parent page's bottom layer (set when a parent page was
|
||||||
|
// supplied).
|
||||||
|
ChildPageRoots []childPageRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
type childPageRoot struct {
|
||||||
|
Position core.TriePosition
|
||||||
|
Node core.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
// stackPage is a page currently held in the walker's ascending page stack.
|
||||||
|
type stackPage struct {
|
||||||
|
pageID core.PageID
|
||||||
|
page *core.RawPage
|
||||||
|
diff core.PageDiff
|
||||||
|
elided ElidedChildren
|
||||||
|
origin PageOrigin
|
||||||
|
}
|
||||||
|
|
||||||
|
// PageWalker performs left-to-right walking and updating of the page tree.
|
||||||
|
//
|
||||||
|
// Usage: create a PageWalker, make repeated calls to AdvanceAndReplace (and
|
||||||
|
// optionally Advance / AdvanceAndPlaceNode), then call Conclude to get the
|
||||||
|
// new root and all updated pages.
|
||||||
|
type PageWalker struct {
|
||||||
|
lastPosition *core.TriePosition // nil before first advance
|
||||||
|
position core.TriePosition
|
||||||
|
parentPage *core.PageID
|
||||||
|
root core.Node
|
||||||
|
|
||||||
|
stack []stackPage
|
||||||
|
siblingStack []siblingEntry
|
||||||
|
prevNode *core.Node
|
||||||
|
|
||||||
|
outputPages []UpdatedPage
|
||||||
|
childPageRoots []childPageRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
type siblingEntry struct {
|
||||||
|
node core.Node
|
||||||
|
depth int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPageWalker creates a new PageWalker starting from the given root.
|
||||||
|
// If parentPage is non-nil, the walker is constrained to pages below the
|
||||||
|
// parent, and Conclude returns ChildPageRoots instead of Root.
|
||||||
|
func NewPageWalker(root core.Node, parentPage *core.PageID) *PageWalker {
|
||||||
|
return &PageWalker{
|
||||||
|
position: core.NewTriePosition(),
|
||||||
|
root: root,
|
||||||
|
parentPage: parentPage,
|
||||||
|
stack: make([]stackPage, 0, 8),
|
||||||
|
siblingStack: make([]siblingEntry, 0, 16),
|
||||||
|
outputPages: make([]UpdatedPage, 0, 16),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdvanceAndReplace advances to the given position and replaces the terminal
|
||||||
|
// node there with a sub-trie built from the provided key-value pairs.
|
||||||
|
//
|
||||||
|
// The pairs must be sorted and must all share the prefix corresponding to
|
||||||
|
// the position. An empty slice deletes the existing terminal node.
|
||||||
|
//
|
||||||
|
// Panics if the position is not greater than the previous position.
|
||||||
|
func (w *PageWalker) AdvanceAndReplace(
|
||||||
|
pageSet PageSet,
|
||||||
|
newPos core.TriePosition,
|
||||||
|
ops []core.KeyValue,
|
||||||
|
) {
|
||||||
|
if w.lastPosition != nil {
|
||||||
|
w.assertForward(&newPos)
|
||||||
|
w.compactUp(&newPos)
|
||||||
|
}
|
||||||
|
pos := newPos
|
||||||
|
w.lastPosition = &pos
|
||||||
|
w.buildStack(pageSet, newPos)
|
||||||
|
w.replaceTerminal(pageSet, ops)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdvanceAndPlaceNode advances to the given position and sets the given node.
|
||||||
|
//
|
||||||
|
// This is used to place child-page root nodes computed by parallel workers.
|
||||||
|
func (w *PageWalker) AdvanceAndPlaceNode(
|
||||||
|
pageSet PageSet,
|
||||||
|
newPos core.TriePosition,
|
||||||
|
node core.Node,
|
||||||
|
) {
|
||||||
|
if w.lastPosition != nil {
|
||||||
|
w.assertForward(&newPos)
|
||||||
|
w.compactUp(&newPos)
|
||||||
|
}
|
||||||
|
pos := newPos
|
||||||
|
w.lastPosition = &pos
|
||||||
|
w.buildStack(pageSet, newPos)
|
||||||
|
w.placeNode(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance moves the walker to a new position without modifying the trie.
|
||||||
|
func (w *PageWalker) Advance(newPos core.TriePosition) {
|
||||||
|
if w.lastPosition != nil {
|
||||||
|
w.assertForward(&newPos)
|
||||||
|
w.compactUp(&newPos)
|
||||||
|
}
|
||||||
|
pos := newPos
|
||||||
|
w.lastPosition = &pos
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conclude finalizes the walk and returns the output: a new root node and
|
||||||
|
// all updated pages.
|
||||||
|
func (w *PageWalker) Conclude() Output {
|
||||||
|
w.compactUpToRoot()
|
||||||
|
|
||||||
|
out := Output{
|
||||||
|
Root: w.root,
|
||||||
|
Pages: w.outputPages,
|
||||||
|
ChildPageRoots: w.childPageRoots,
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- core operations ---
|
||||||
|
|
||||||
|
func (w *PageWalker) placeNode(node core.Node) {
|
||||||
|
if w.position.IsRoot() {
|
||||||
|
prev := w.root
|
||||||
|
w.prevNode = &prev
|
||||||
|
w.root = node
|
||||||
|
} else {
|
||||||
|
prev := w.node()
|
||||||
|
w.prevNode = &prev
|
||||||
|
w.setNode(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *PageWalker) replaceTerminal(pageSet PageSet, ops []core.KeyValue) {
|
||||||
|
var existingNode core.Node
|
||||||
|
if w.position.IsRoot() {
|
||||||
|
existingNode = w.root
|
||||||
|
} else {
|
||||||
|
existingNode = w.node()
|
||||||
|
}
|
||||||
|
w.prevNode = &existingNode
|
||||||
|
|
||||||
|
startDepth := w.position.Depth()
|
||||||
|
|
||||||
|
core.BuildTrie(int(w.position.Depth()), ops, func(wn core.WriteNode) {
|
||||||
|
node := wn.Node
|
||||||
|
|
||||||
|
// For internal nodes, clear garbage in the sibling slot if the
|
||||||
|
// sibling is a terminator.
|
||||||
|
if wn.Kind == core.WriteNodeInternal && wn.InternalData != nil {
|
||||||
|
lastBit := w.position.PeekLastBit()
|
||||||
|
var zeroSibling bool
|
||||||
|
if lastBit {
|
||||||
|
zeroSibling = core.IsTerminator(&wn.InternalData.Left)
|
||||||
|
} else {
|
||||||
|
zeroSibling = core.IsTerminator(&wn.InternalData.Right)
|
||||||
|
}
|
||||||
|
if zeroSibling {
|
||||||
|
w.setSibling(core.Terminator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate: up then down.
|
||||||
|
if wn.GoUp && len(wn.DownBits) > 0 {
|
||||||
|
// Optimization: if the first down bit goes to the sibling,
|
||||||
|
// use Sibling() instead of going up + down.
|
||||||
|
if wn.DownBits[0] != w.position.PeekLastBit() {
|
||||||
|
w.position.Sibling()
|
||||||
|
w.downBits(pageSet, wn.DownBits[1:], true)
|
||||||
|
} else {
|
||||||
|
w.up()
|
||||||
|
w.downBits(pageSet, wn.DownBits, true)
|
||||||
|
}
|
||||||
|
} else if wn.GoUp {
|
||||||
|
w.up()
|
||||||
|
} else if len(wn.DownBits) > 0 {
|
||||||
|
// First bit is only fresh if we are at the start position and
|
||||||
|
// start is at end of page (or root). After that, definitely fresh.
|
||||||
|
fresh := w.position.DepthInPage() == core.PageDepth ||
|
||||||
|
w.position.IsRoot()
|
||||||
|
if w.position.Depth() <= startDepth {
|
||||||
|
w.downBits(pageSet, wn.DownBits[:1], fresh)
|
||||||
|
w.downBits(pageSet, wn.DownBits[1:], true)
|
||||||
|
} else {
|
||||||
|
w.downBits(pageSet, wn.DownBits, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.position.IsRoot() {
|
||||||
|
w.root = node
|
||||||
|
} else {
|
||||||
|
w.setNode(node)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- stack navigation ---
|
||||||
|
|
||||||
|
// up moves one level toward the root. If crossing a page boundary, pops the
|
||||||
|
// stack page and emits it as output.
|
||||||
|
func (w *PageWalker) up() {
|
||||||
|
if w.position.DepthInPage() == 1 {
|
||||||
|
w.popStackPage()
|
||||||
|
}
|
||||||
|
w.position.Up(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// downBits descends along the given bit path, pushing new pages onto the
|
||||||
|
// stack as needed.
|
||||||
|
func (w *PageWalker) downBits(pageSet PageSet, bits []bool, fresh bool) {
|
||||||
|
for _, bit := range bits {
|
||||||
|
if w.position.IsRoot() {
|
||||||
|
rootID := core.RootPageID()
|
||||||
|
var page *core.RawPage
|
||||||
|
var origin PageOrigin
|
||||||
|
if fresh {
|
||||||
|
page = pageSet.Fresh(rootID)
|
||||||
|
origin = PageOrigin{Kind: PageOriginFresh}
|
||||||
|
} else {
|
||||||
|
var ok bool
|
||||||
|
page, origin, ok = pageSet.Get(rootID)
|
||||||
|
if !ok {
|
||||||
|
panic("pagewalker: root page not in page set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.pushPage(rootID, page, origin)
|
||||||
|
} else if w.position.DepthInPage() == core.PageDepth {
|
||||||
|
// Crossing into a child page.
|
||||||
|
parentSP := w.stack[len(w.stack)-1]
|
||||||
|
childIdx := w.position.ChildPageIndex()
|
||||||
|
childID, err := parentSP.pageID.ChildPageID(childIdx)
|
||||||
|
if err != nil {
|
||||||
|
panic("pagewalker: child page ID overflow")
|
||||||
|
}
|
||||||
|
var page *core.RawPage
|
||||||
|
var origin PageOrigin
|
||||||
|
if fresh {
|
||||||
|
page = pageSet.Fresh(childID)
|
||||||
|
origin = PageOrigin{Kind: PageOriginFresh}
|
||||||
|
} else {
|
||||||
|
var ok bool
|
||||||
|
page, origin, ok = pageSet.Get(childID)
|
||||||
|
if !ok {
|
||||||
|
panic("pagewalker: child page not in page set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.pushPage(childID, page, origin)
|
||||||
|
}
|
||||||
|
w.position.Down(bit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *PageWalker) pushPage(
|
||||||
|
pageID core.PageID, page *core.RawPage, origin PageOrigin,
|
||||||
|
) {
|
||||||
|
w.stack = append(w.stack, stackPage{
|
||||||
|
pageID: pageID,
|
||||||
|
page: page,
|
||||||
|
diff: core.PageDiff{},
|
||||||
|
elided: ElidedChildrenFromUint64(page.ElidedChildren()),
|
||||||
|
origin: origin,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *PageWalker) popStackPage() {
|
||||||
|
if len(w.stack) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sp := w.stack[len(w.stack)-1]
|
||||||
|
w.stack = w.stack[:len(w.stack)-1]
|
||||||
|
|
||||||
|
// Store elided children back into the page before emitting.
|
||||||
|
if !sp.pageID.IsRoot() {
|
||||||
|
sp.page.SetElidedChildren(sp.elided.Raw())
|
||||||
|
}
|
||||||
|
|
||||||
|
w.outputPages = append(w.outputPages, UpdatedPage{
|
||||||
|
PageID: sp.pageID,
|
||||||
|
Page: sp.page,
|
||||||
|
Diff: sp.diff,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildStack pushes pages onto the stack from the current position down to
|
||||||
|
// the target position's page.
|
||||||
|
func (w *PageWalker) buildStack(pageSet PageSet, position core.TriePosition) {
|
||||||
|
w.position = position
|
||||||
|
newPageID := position.PageID()
|
||||||
|
if newPageID == nil {
|
||||||
|
// Target is at the root — pop all remaining stack pages.
|
||||||
|
for len(w.stack) > 0 {
|
||||||
|
w.popStackPage()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which ancestor to push down from.
|
||||||
|
var target *core.PageID
|
||||||
|
if len(w.stack) > 0 {
|
||||||
|
t := w.stack[len(w.stack)-1].pageID
|
||||||
|
target = &t
|
||||||
|
} else if w.parentPage != nil {
|
||||||
|
target = w.parentPage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect pages from newPageID up to target.
|
||||||
|
var toPush []core.PageID
|
||||||
|
cur := *newPageID
|
||||||
|
for {
|
||||||
|
if target != nil && cur.Equal(*target) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
toPush = append(toPush, cur)
|
||||||
|
if cur.IsRoot() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cur = cur.ParentPageID()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push in ascending order (root-ward first).
|
||||||
|
for i := len(toPush) - 1; i >= 0; i-- {
|
||||||
|
pid := toPush[i]
|
||||||
|
page, origin, ok := pageSet.Get(pid)
|
||||||
|
if !ok {
|
||||||
|
panic("pagewalker: page not in page set during build_stack")
|
||||||
|
}
|
||||||
|
w.pushPage(pid, page, origin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- compaction ---
|
||||||
|
|
||||||
|
// compactUp hashes upward from the current position toward the shared
|
||||||
|
// ancestor with the target position. This is the core "partial compaction"
|
||||||
|
// that avoids redundant hashing.
|
||||||
|
func (w *PageWalker) compactUp(targetPos *core.TriePosition) {
|
||||||
|
if len(w.stack) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDepth := int(w.position.Depth())
|
||||||
|
sharedDepth := w.position.SharedDepth(targetPos)
|
||||||
|
|
||||||
|
// Prune sibling stack entries beyond shared depth.
|
||||||
|
keepLen := 0
|
||||||
|
for _, s := range w.siblingStack {
|
||||||
|
if s.depth <= sharedDepth {
|
||||||
|
keepLen++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.siblingStack = w.siblingStack[:keepLen]
|
||||||
|
|
||||||
|
compactLayers := currentDepth - (sharedDepth + 1)
|
||||||
|
if compactLayers == 0 {
|
||||||
|
if w.prevNode != nil {
|
||||||
|
w.siblingStack = append(w.siblingStack, siblingEntry{
|
||||||
|
node: *w.prevNode,
|
||||||
|
depth: currentDepth,
|
||||||
|
})
|
||||||
|
w.prevNode = nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w.prevNode = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range compactLayers {
|
||||||
|
nextNode := w.compactStep()
|
||||||
|
w.up()
|
||||||
|
|
||||||
|
if len(w.stack) == 0 {
|
||||||
|
if w.parentPage == nil {
|
||||||
|
w.root = nextNode
|
||||||
|
} else {
|
||||||
|
w.childPageRoots = append(w.childPageRoots, childPageRoot{
|
||||||
|
Position: w.position,
|
||||||
|
Node: nextNode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
// Save the final relevant sibling.
|
||||||
|
if i == compactLayers-1 {
|
||||||
|
w.siblingStack = append(w.siblingStack, siblingEntry{
|
||||||
|
node: w.node(),
|
||||||
|
depth: int(w.position.Depth()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
w.setNode(nextNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// compactUpToRoot is called by Conclude to hash all remaining layers to root.
|
||||||
|
func (w *PageWalker) compactUpToRoot() {
|
||||||
|
if len(w.stack) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.siblingStack = w.siblingStack[:0]
|
||||||
|
compactLayers := int(w.position.Depth())
|
||||||
|
|
||||||
|
for range compactLayers {
|
||||||
|
nextNode := w.compactStep()
|
||||||
|
w.up()
|
||||||
|
|
||||||
|
if len(w.stack) == 0 {
|
||||||
|
if w.parentPage == nil {
|
||||||
|
w.root = nextNode
|
||||||
|
} else {
|
||||||
|
w.childPageRoots = append(w.childPageRoots, childPageRoot{
|
||||||
|
Position: w.position,
|
||||||
|
Node: nextNode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
w.setNode(nextNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// compactStep performs one layer of compaction: reads the current node and
|
||||||
|
// its sibling, then either compacts terminators/leaves upward or hashes
|
||||||
|
// an internal node.
|
||||||
|
func (w *PageWalker) compactStep() core.Node {
|
||||||
|
node := w.node()
|
||||||
|
sibling := w.siblingNode()
|
||||||
|
bit := w.position.PeekLastBit()
|
||||||
|
|
||||||
|
nodeKind := core.NodeKindOf(&node)
|
||||||
|
sibKind := core.NodeKindOf(&sibling)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case nodeKind == core.NodeTerminator && sibKind == core.NodeTerminator:
|
||||||
|
return core.Terminator
|
||||||
|
|
||||||
|
case nodeKind == core.NodeLeaf && sibKind == core.NodeTerminator:
|
||||||
|
// Compact: clear this node, move leaf up.
|
||||||
|
w.setNode(core.Terminator)
|
||||||
|
return node
|
||||||
|
|
||||||
|
case nodeKind == core.NodeTerminator && sibKind == core.NodeLeaf:
|
||||||
|
// Compact: clear sibling, move leaf up.
|
||||||
|
w.position.Sibling()
|
||||||
|
w.setNode(core.Terminator)
|
||||||
|
return sibling
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Internal: hash the two children together.
|
||||||
|
var id core.InternalData
|
||||||
|
if bit {
|
||||||
|
id = core.InternalData{Left: sibling, Right: node}
|
||||||
|
} else {
|
||||||
|
id = core.InternalData{Left: node, Right: sibling}
|
||||||
|
}
|
||||||
|
return core.HashInternal(&id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- page node access ---
|
||||||
|
|
||||||
|
// node reads the node at the current position from the top stack page.
|
||||||
|
func (w *PageWalker) node() core.Node {
|
||||||
|
sp := &w.stack[len(w.stack)-1]
|
||||||
|
return sp.page.NodeAt(w.position.NodeIndex())
|
||||||
|
}
|
||||||
|
|
||||||
|
// siblingNode reads the sibling of the current position.
|
||||||
|
func (w *PageWalker) siblingNode() core.Node {
|
||||||
|
sp := &w.stack[len(w.stack)-1]
|
||||||
|
return sp.page.NodeAt(w.position.SiblingIndex())
|
||||||
|
}
|
||||||
|
|
||||||
|
// setNode writes a node at the current position and records it in the diff.
|
||||||
|
func (w *PageWalker) setNode(node core.Node) {
|
||||||
|
idx := w.position.NodeIndex()
|
||||||
|
sibNode := w.siblingNode()
|
||||||
|
|
||||||
|
sp := &w.stack[len(w.stack)-1]
|
||||||
|
sp.page.SetNodeAt(idx, node)
|
||||||
|
|
||||||
|
// If both the node and its sibling are terminators at the first layer,
|
||||||
|
// mark the page as cleared instead of changed.
|
||||||
|
if w.position.IsFirstLayerInPage() &&
|
||||||
|
core.IsTerminator(&node) &&
|
||||||
|
core.IsTerminator(&sibNode) {
|
||||||
|
sp.diff.SetCleared()
|
||||||
|
} else {
|
||||||
|
sp.diff.SetChanged(idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setSibling writes a node at the sibling position and records the change.
|
||||||
|
func (w *PageWalker) setSibling(node core.Node) {
|
||||||
|
sibIdx := w.position.SiblingIndex()
|
||||||
|
sp := &w.stack[len(w.stack)-1]
|
||||||
|
sp.page.SetNodeAt(sibIdx, node)
|
||||||
|
sp.diff.SetChanged(sibIdx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- assertions ---
|
||||||
|
|
||||||
|
func (w *PageWalker) assertForward(newPos *core.TriePosition) {
|
||||||
|
if w.lastPosition == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newPath := newPos.Path()
|
||||||
|
lastPath := w.lastPosition.Path()
|
||||||
|
for i := range newPath {
|
||||||
|
if newPath[i] > lastPath[i] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if newPath[i] < lastPath[i] {
|
||||||
|
panic("pagewalker: positions must advance left-to-right")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic("pagewalker: positions must advance left-to-right (equal)")
|
||||||
|
}
|
||||||
360
nomt/merkle/pagewalker_test.go
Normal file
360
nomt/merkle/pagewalker_test.go
Normal file
|
|
@ -0,0 +1,360 @@
|
||||||
|
package merkle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/nomt/core"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper: build a TriePosition by descending along the given bits.
|
||||||
|
func triePos(bits ...bool) core.TriePosition {
|
||||||
|
p := core.NewTriePosition()
|
||||||
|
for _, b := range bits {
|
||||||
|
p.Down(b)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: create a KeyPath with the given bits set at the MSB positions.
|
||||||
|
func keyPath(bits ...bool) core.KeyPath {
|
||||||
|
var kp core.KeyPath
|
||||||
|
for i, b := range bits {
|
||||||
|
if b {
|
||||||
|
kp[i/8] |= 1 << (7 - i%8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return kp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: create a ValueHash filled with a single byte.
|
||||||
|
func val(v byte) core.ValueHash {
|
||||||
|
var vh core.ValueHash
|
||||||
|
for i := range vh {
|
||||||
|
vh[i] = v
|
||||||
|
}
|
||||||
|
return vh
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: compute the expected root from a set of key-value pairs using
|
||||||
|
// BuildTrie directly (the "oracle").
|
||||||
|
func expectedRoot(kvs []core.KeyValue) core.Node {
|
||||||
|
return core.BuildTrie(0, kvs, func(_ core.WriteNode) {})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPageWalkerEmptyTrie(t *testing.T) {
|
||||||
|
ps := NewMemoryPageSet(true)
|
||||||
|
walker := NewPageWalker(core.Terminator, nil)
|
||||||
|
out := walker.Conclude()
|
||||||
|
assert.Equal(t, core.Terminator, out.Root)
|
||||||
|
assert.Empty(t, out.Pages)
|
||||||
|
_ = ps
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPageWalkerSingleInsert(t *testing.T) {
|
||||||
|
ps := NewMemoryPageSet(true)
|
||||||
|
walker := NewPageWalker(core.Terminator, nil)
|
||||||
|
|
||||||
|
kp := keyPath(false, false)
|
||||||
|
v := val(1)
|
||||||
|
pos := triePos(false, false)
|
||||||
|
|
||||||
|
walker.AdvanceAndReplace(ps, pos, []core.KeyValue{{Key: kp, Value: v}})
|
||||||
|
out := walker.Conclude()
|
||||||
|
|
||||||
|
expected := expectedRoot([]core.KeyValue{{Key: kp, Value: v}})
|
||||||
|
assert.Equal(t, expected, out.Root)
|
||||||
|
assert.True(t, core.IsLeaf(&out.Root))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPageWalkerTwoInsertsSameAdvance(t *testing.T) {
|
||||||
|
// Two keys that share a common prefix at position [0,0], then diverge.
|
||||||
|
ps := NewMemoryPageSet(true)
|
||||||
|
walker := NewPageWalker(core.Terminator, nil)
|
||||||
|
|
||||||
|
kp1 := keyPath(false, false, true, false) // 0010...
|
||||||
|
kp2 := keyPath(false, false, true, true) // 0011...
|
||||||
|
v1, v2 := val(1), val(2)
|
||||||
|
|
||||||
|
pos := triePos(false, false)
|
||||||
|
walker.AdvanceAndReplace(ps, pos, []core.KeyValue{
|
||||||
|
{Key: kp1, Value: v1},
|
||||||
|
{Key: kp2, Value: v2},
|
||||||
|
})
|
||||||
|
out := walker.Conclude()
|
||||||
|
|
||||||
|
expected := expectedRoot([]core.KeyValue{
|
||||||
|
{Key: kp1, Value: v1},
|
||||||
|
{Key: kp2, Value: v2},
|
||||||
|
})
|
||||||
|
assert.Equal(t, expected, out.Root)
|
||||||
|
assert.True(t, core.IsInternal(&out.Root))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPageWalkerTwoAdvances(t *testing.T) {
|
||||||
|
// First advance inserts at [0], second at [1].
|
||||||
|
ps := NewMemoryPageSet(true)
|
||||||
|
walker := NewPageWalker(core.Terminator, nil)
|
||||||
|
|
||||||
|
kp0 := keyPath(false, false) // 00...
|
||||||
|
kp1 := keyPath(true, false) // 10...
|
||||||
|
v0, v1 := val(1), val(2)
|
||||||
|
|
||||||
|
walker.AdvanceAndReplace(ps, triePos(false), []core.KeyValue{
|
||||||
|
{Key: kp0, Value: v0},
|
||||||
|
})
|
||||||
|
walker.AdvanceAndReplace(ps, triePos(true), []core.KeyValue{
|
||||||
|
{Key: kp1, Value: v1},
|
||||||
|
})
|
||||||
|
out := walker.Conclude()
|
||||||
|
|
||||||
|
expected := expectedRoot([]core.KeyValue{
|
||||||
|
{Key: kp0, Value: v0},
|
||||||
|
{Key: kp1, Value: v1},
|
||||||
|
})
|
||||||
|
assert.Equal(t, expected, out.Root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPageWalkerMultipleAdvances(t *testing.T) {
|
||||||
|
// Match the Rust multi_value test pattern:
|
||||||
|
// 0b00010000 = 0x10, 0b00100000 = 0x20, 0b01000000 = 0x40,
|
||||||
|
// 0b10100000 = 0xA0, 0b10110000 = 0xB0
|
||||||
|
ps := NewMemoryPageSet(true)
|
||||||
|
walker := NewPageWalker(core.Terminator, nil)
|
||||||
|
|
||||||
|
kvA := core.KeyValue{Key: makeKVKey(0x10), Value: makeKVVal(0x10)}
|
||||||
|
kvB := core.KeyValue{Key: makeKVKey(0x20), Value: makeKVVal(0x20)}
|
||||||
|
kvC := core.KeyValue{Key: makeKVKey(0x40), Value: makeKVVal(0x40)}
|
||||||
|
kvD := core.KeyValue{Key: makeKVKey(0xA0), Value: makeKVVal(0xA0)}
|
||||||
|
kvE := core.KeyValue{Key: makeKVKey(0xB0), Value: makeKVVal(0xB0)}
|
||||||
|
|
||||||
|
allOps := []core.KeyValue{kvA, kvB, kvC, kvD, kvE}
|
||||||
|
expected := expectedRoot(allOps)
|
||||||
|
|
||||||
|
// Group by terminal: A and B share prefix 00, C is at 01, D and E
|
||||||
|
// share prefix 101.
|
||||||
|
// Terminal at [0,0]: A, B
|
||||||
|
walker.AdvanceAndReplace(ps, triePos(false, false),
|
||||||
|
[]core.KeyValue{kvA, kvB})
|
||||||
|
// Terminal at [0,1]: C
|
||||||
|
walker.AdvanceAndReplace(ps, triePos(false, true),
|
||||||
|
[]core.KeyValue{kvC})
|
||||||
|
// Terminal at [1]: D, E
|
||||||
|
walker.AdvanceAndReplace(ps, triePos(true),
|
||||||
|
[]core.KeyValue{kvD, kvE})
|
||||||
|
|
||||||
|
out := walker.Conclude()
|
||||||
|
assert.Equal(t, expected, out.Root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPageWalkerDeleteToTerminator(t *testing.T) {
|
||||||
|
// Insert a leaf, then delete it in a second walker pass.
|
||||||
|
ps := NewMemoryPageSet(true)
|
||||||
|
|
||||||
|
kp := keyPath(false)
|
||||||
|
v := val(1)
|
||||||
|
|
||||||
|
// First: insert a leaf.
|
||||||
|
walker1 := NewPageWalker(core.Terminator, nil)
|
||||||
|
walker1.AdvanceAndReplace(ps, triePos(false), []core.KeyValue{
|
||||||
|
{Key: kp, Value: v},
|
||||||
|
})
|
||||||
|
out1 := walker1.Conclude()
|
||||||
|
require.True(t, core.IsLeaf(&out1.Root))
|
||||||
|
ps.Apply(out1.Pages)
|
||||||
|
|
||||||
|
// Second: delete it (empty ops = terminator replacement).
|
||||||
|
walker2 := NewPageWalker(out1.Root, nil)
|
||||||
|
walker2.AdvanceAndReplace(ps, triePos(false), nil)
|
||||||
|
out2 := walker2.Conclude()
|
||||||
|
assert.Equal(t, core.Terminator, out2.Root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPageWalkerCompactionLeafUp(t *testing.T) {
|
||||||
|
// When one sibling becomes a terminator and the other is a leaf,
|
||||||
|
// the leaf should be compacted upward.
|
||||||
|
ps := NewMemoryPageSet(true)
|
||||||
|
|
||||||
|
kp0 := keyPath(false)
|
||||||
|
kp1 := keyPath(true)
|
||||||
|
v := val(1)
|
||||||
|
|
||||||
|
// Insert two leaves.
|
||||||
|
walker1 := NewPageWalker(core.Terminator, nil)
|
||||||
|
walker1.AdvanceAndReplace(ps, triePos(false), []core.KeyValue{
|
||||||
|
{Key: kp0, Value: v},
|
||||||
|
})
|
||||||
|
walker1.AdvanceAndReplace(ps, triePos(true), []core.KeyValue{
|
||||||
|
{Key: kp1, Value: v},
|
||||||
|
})
|
||||||
|
out1 := walker1.Conclude()
|
||||||
|
ps.Apply(out1.Pages)
|
||||||
|
|
||||||
|
// Delete the right leaf — left leaf should compact up to root.
|
||||||
|
walker2 := NewPageWalker(out1.Root, nil)
|
||||||
|
walker2.AdvanceAndReplace(ps, triePos(true), nil)
|
||||||
|
out2 := walker2.Conclude()
|
||||||
|
|
||||||
|
expectedLeaf := core.HashLeaf(&core.LeafData{KeyPath: kp0, ValueHash: v})
|
||||||
|
assert.Equal(t, expectedLeaf, out2.Root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPageWalkerOutputPages(t *testing.T) {
|
||||||
|
ps := NewMemoryPageSet(true)
|
||||||
|
walker := NewPageWalker(core.Terminator, nil)
|
||||||
|
|
||||||
|
kp := keyPath(false)
|
||||||
|
v := val(1)
|
||||||
|
|
||||||
|
walker.AdvanceAndReplace(ps, triePos(false), []core.KeyValue{
|
||||||
|
{Key: kp, Value: v},
|
||||||
|
})
|
||||||
|
out := walker.Conclude()
|
||||||
|
|
||||||
|
// Should output at least the root page.
|
||||||
|
require.NotEmpty(t, out.Pages)
|
||||||
|
assert.True(t, out.Pages[0].PageID.IsRoot())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPageWalkerAdvanceBackwardsPanics(t *testing.T) {
|
||||||
|
ps := NewMemoryPageSet(true)
|
||||||
|
walker := NewPageWalker(core.Terminator, nil)
|
||||||
|
|
||||||
|
walker.AdvanceAndReplace(ps, triePos(true), []core.KeyValue{
|
||||||
|
{Key: keyPath(true), Value: val(1)},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
walker.AdvanceAndReplace(ps, triePos(false), []core.KeyValue{
|
||||||
|
{Key: keyPath(false), Value: val(2)},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPageWalkerDeterministic(t *testing.T) {
|
||||||
|
// Same inputs should produce the same root.
|
||||||
|
kvs := []core.KeyValue{
|
||||||
|
{Key: makeKVKey(0x10), Value: makeKVVal(0x10)},
|
||||||
|
{Key: makeKVKey(0x50), Value: makeKVVal(0x50)},
|
||||||
|
{Key: makeKVKey(0xA0), Value: makeKVVal(0xA0)},
|
||||||
|
}
|
||||||
|
|
||||||
|
run := func() core.Node {
|
||||||
|
ps := NewMemoryPageSet(true)
|
||||||
|
w := NewPageWalker(core.Terminator, nil)
|
||||||
|
// All at root terminal since trie is empty.
|
||||||
|
w.AdvanceAndReplace(ps, triePos(false), kvs[:2])
|
||||||
|
w.AdvanceAndReplace(ps, triePos(true), kvs[2:])
|
||||||
|
return w.Conclude().Root
|
||||||
|
}
|
||||||
|
|
||||||
|
r1 := run()
|
||||||
|
r2 := run()
|
||||||
|
assert.Equal(t, r1, r2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPageWalkerIncrementalUpdates(t *testing.T) {
|
||||||
|
// Build a trie, apply updates, verify the resulting root matches
|
||||||
|
// building from scratch.
|
||||||
|
ps := NewMemoryPageSet(true)
|
||||||
|
|
||||||
|
kp0 := makeKVKey(0x10)
|
||||||
|
kp1 := makeKVKey(0x80)
|
||||||
|
v1, v2 := makeKVVal(0x01), makeKVVal(0x02)
|
||||||
|
|
||||||
|
// Pass 1: insert two keys.
|
||||||
|
w1 := NewPageWalker(core.Terminator, nil)
|
||||||
|
w1.AdvanceAndReplace(ps, triePos(false), []core.KeyValue{
|
||||||
|
{Key: kp0, Value: v1},
|
||||||
|
})
|
||||||
|
w1.AdvanceAndReplace(ps, triePos(true), []core.KeyValue{
|
||||||
|
{Key: kp1, Value: v1},
|
||||||
|
})
|
||||||
|
out1 := w1.Conclude()
|
||||||
|
ps.Apply(out1.Pages)
|
||||||
|
|
||||||
|
// Pass 2: update the second key's value.
|
||||||
|
w2 := NewPageWalker(out1.Root, nil)
|
||||||
|
w2.AdvanceAndReplace(ps, triePos(true), []core.KeyValue{
|
||||||
|
{Key: kp1, Value: v2},
|
||||||
|
})
|
||||||
|
out2 := w2.Conclude()
|
||||||
|
|
||||||
|
// The expected root should match building the whole trie from scratch
|
||||||
|
// with the updated value.
|
||||||
|
expected := expectedRoot([]core.KeyValue{
|
||||||
|
{Key: kp0, Value: v1},
|
||||||
|
{Key: kp1, Value: v2},
|
||||||
|
})
|
||||||
|
assert.Equal(t, expected, out2.Root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPageWalkerAdvanceWithoutModify(t *testing.T) {
|
||||||
|
// Test the Advance (read-only) method.
|
||||||
|
ps := NewMemoryPageSet(true)
|
||||||
|
walker := NewPageWalker(core.Terminator, nil)
|
||||||
|
|
||||||
|
kp0 := keyPath(false, false, true, false) // 0010...
|
||||||
|
kp1 := keyPath(false, false, true, true) // 0011...
|
||||||
|
kp2 := keyPath(true) // 1...
|
||||||
|
v := val(1)
|
||||||
|
|
||||||
|
walker.AdvanceAndReplace(ps, triePos(false, false), []core.KeyValue{
|
||||||
|
{Key: kp0, Value: v},
|
||||||
|
{Key: kp1, Value: v},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Advance past [0,1] without modifying — the walker should still
|
||||||
|
// compact correctly.
|
||||||
|
walker.Advance(triePos(false, true))
|
||||||
|
|
||||||
|
walker.AdvanceAndReplace(ps, triePos(true), []core.KeyValue{
|
||||||
|
{Key: kp2, Value: v},
|
||||||
|
})
|
||||||
|
|
||||||
|
out := walker.Conclude()
|
||||||
|
|
||||||
|
expected := expectedRoot([]core.KeyValue{
|
||||||
|
{Key: kp0, Value: v},
|
||||||
|
{Key: kp1, Value: v},
|
||||||
|
{Key: kp2, Value: v},
|
||||||
|
})
|
||||||
|
assert.Equal(t, expected, out.Root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPageWalkerPageDiffs(t *testing.T) {
|
||||||
|
// Verify that output pages have non-empty diffs.
|
||||||
|
ps := NewMemoryPageSet(true)
|
||||||
|
walker := NewPageWalker(core.Terminator, nil)
|
||||||
|
|
||||||
|
kp := keyPath(false, false)
|
||||||
|
v := val(1)
|
||||||
|
walker.AdvanceAndReplace(ps, triePos(false, false), []core.KeyValue{
|
||||||
|
{Key: kp, Value: v},
|
||||||
|
})
|
||||||
|
out := walker.Conclude()
|
||||||
|
|
||||||
|
require.NotEmpty(t, out.Pages)
|
||||||
|
// The root page should have at least one changed node.
|
||||||
|
assert.True(t, out.Pages[0].Diff.Count() > 0,
|
||||||
|
"page diff should track changed nodes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- helpers ---
|
||||||
|
|
||||||
|
func makeKVKey(b byte) core.KeyPath {
|
||||||
|
var kp core.KeyPath
|
||||||
|
for i := range kp {
|
||||||
|
kp[i] = b
|
||||||
|
}
|
||||||
|
return kp
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeKVVal(b byte) core.ValueHash {
|
||||||
|
var vh core.ValueHash
|
||||||
|
for i := range vh {
|
||||||
|
vh[i] = b
|
||||||
|
}
|
||||||
|
return vh
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue