go-ethereum/nomt/core/triepos.go
weiihann 88fd10529f 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>
2026-02-12 17:10:58 +08:00

267 lines
6.6 KiB
Go

package core
// TriePosition tracks a position within the paged binary trie, combining a
// key path prefix with a node index within the current page.
type TriePosition struct {
path [32]byte
depth uint16
nodeIndex int
}
// NewTriePosition creates a TriePosition at the root.
func NewTriePosition() TriePosition {
return TriePosition{}
}
// TriePositionFromPathAndDepth creates a TriePosition at the given depth
// within the path. Panics if depth is 0.
func TriePositionFromPathAndDepth(path KeyPath, depth uint16) TriePosition {
if depth == 0 {
panic("triepos: depth must be non-zero")
}
if depth > 256 {
panic("triepos: depth out of range")
}
pagePath := lastPagePath(path[:], depth)
return TriePosition{
path: path,
depth: depth,
nodeIndex: computeNodeIndex(pagePath),
}
}
// IsRoot reports whether the position is at the root.
func (p *TriePosition) IsRoot() bool {
return p.depth == 0
}
// Depth returns the current depth in the trie.
func (p *TriePosition) Depth() uint16 {
return p.depth
}
// Path returns the raw 32-byte path.
func (p *TriePosition) Path() [32]byte {
return p.path
}
// Bit returns the bit at position n in the path (0 = MSB of byte 0).
func (p *TriePosition) Bit(n int) bool {
return (p.path[n/8]>>(7-n%8))&1 == 1
}
// NodeIndex returns the index of the current node within its page.
func (p *TriePosition) NodeIndex() int {
return p.nodeIndex
}
// Down moves the position down by 1 bit (left if bit=false, right if bit=true).
func (p *TriePosition) Down(bit bool) {
if p.depth == 256 {
panic("triepos: can't descend past 256 bits")
}
if int(p.depth)%PageDepth == 0 {
// Entering a new page: node index resets.
if bit {
p.nodeIndex = 1
} else {
p.nodeIndex = 0
}
} else {
left := p.nodeIndex*2 + 2
if bit {
p.nodeIndex = left + 1
} else {
p.nodeIndex = left
}
}
setBit(&p.path, int(p.depth), bit)
p.depth++
}
// Up moves the position up by d bits.
func (p *TriePosition) Up(d uint16) {
if d > p.depth {
panic("triepos: can't move up past root")
}
newDepth := p.depth - d
if newDepth == 0 {
*p = NewTriePosition()
return
}
prevPageDepth := (int(p.depth) + PageDepth - 1) / PageDepth
newPageDepth := (int(newDepth) + PageDepth - 1) / PageDepth
p.depth = newDepth
if prevPageDepth == newPageDepth {
// Same page — walk up parent indices.
for range d {
p.nodeIndex = (p.nodeIndex - 2) / 2
}
} else {
// Crossed a page boundary — recompute.
pagePath := lastPagePath(p.path[:], p.depth)
p.nodeIndex = computeNodeIndex(pagePath)
}
}
// Sibling moves to the sibling node. Panics at root.
func (p *TriePosition) Sibling() {
if p.depth == 0 {
panic("triepos: can't sibling at root")
}
i := int(p.depth) - 1
flipBit(&p.path, i)
p.nodeIndex = siblingIndex(p.nodeIndex)
}
// PeekLastBit returns the last bit of the path. Panics at root.
func (p *TriePosition) PeekLastBit() bool {
if p.depth == 0 {
panic("triepos: can't peek at root")
}
return p.Bit(int(p.depth) - 1)
}
// PageID returns the PageID for the page this position lands in.
// Returns nil if at the root.
func (p *TriePosition) PageID() *PageID {
if p.IsRoot() {
return nil
}
pageID := RootPageID()
d := int(p.depth)
// Number of complete 6-bit chunks before the current partial chunk.
fullChunks := (d - 1) / PageDepth
for i := range fullChunks {
childIndex := extractChildIndex(p.path, i*PageDepth)
pageID, _ = pageID.ChildPageID(childIndex)
}
return &pageID
}
// DepthInPage returns the number of bits traversed in the current page (1-6),
// or 0 if at the root.
func (p *TriePosition) DepthInPage() int {
if p.depth == 0 {
return 0
}
d := int(p.depth)
return d - ((d-1)/PageDepth)*PageDepth
}
// IsFirstLayerInPage reports whether this position is at the top of its page
// (node index 0 or 1).
func (p *TriePosition) IsFirstLayerInPage() bool {
return p.nodeIndex&^1 == 0
}
// ChildNodeIndices returns the left and right child node indices within the
// page. Panics if not at depth 1-5 within the page.
func (p *TriePosition) ChildNodeIndices() (left, right int) {
dip := p.DepthInPage()
if dip == 0 || dip > PageDepth-1 {
panic("triepos: child indices out of bounds")
}
left = p.nodeIndex*2 + 2
right = left + 1
return
}
// ChildPageIndex returns the ChildPageIndex for the current node.
// Panics if not at the last layer of the page (indices 62-125).
func (p *TriePosition) ChildPageIndex() uint8 {
if p.nodeIndex < 62 {
panic("triepos: not at last layer")
}
return uint8(p.nodeIndex - 62)
}
// SiblingIndex returns the index of the sibling node.
func (p *TriePosition) SiblingIndex() int {
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 ---
// computeNodeIndex converts a page-local bit path to a level-order node index.
// Formula: (2^depth - 2) + bits_as_uint, where depth is 1-6.
func computeNodeIndex(pagePath pageBitPath) int {
depth := pagePath.len
if depth == 0 {
return 0
}
if depth > PageDepth {
depth = PageDepth
}
return (1 << depth) - 2 + pagePath.asUint(depth)
}
// pageBitPath represents a sub-slice of bits within a path for node indexing.
type pageBitPath struct {
path []byte // the full 32-byte key path
offset int // bit offset where this page's path starts
len int // number of bits (1-6)
}
// asUint interprets the first `n` bits as an unsigned integer (MSB first).
func (p pageBitPath) asUint(n int) int {
var val int
for i := range n {
byteIdx := (p.offset + i) / 8
bitIdx := 7 - (p.offset+i)%8
bit := int((p.path[byteIdx] >> bitIdx) & 1)
val = (val << 1) | bit
}
return val
}
// lastPagePath extracts the relevant bit path for the current page.
func lastPagePath(path []byte, depth uint16) pageBitPath {
d := int(depth)
prevPageEnd := ((d - 1) / PageDepth) * PageDepth
return pageBitPath{
path: path,
offset: prevPageEnd,
len: d - prevPageEnd,
}
}
func setBit(path *[32]byte, idx int, val bool) {
byteIdx := idx / 8
bitIdx := uint(7 - idx%8)
if val {
path[byteIdx] |= 1 << bitIdx
} else {
path[byteIdx] &^= 1 << bitIdx
}
}
func flipBit(path *[32]byte, idx int) {
byteIdx := idx / 8
bitIdx := uint(7 - idx%8)
path[byteIdx] ^= 1 << bitIdx
}
func siblingIndex(nodeIndex int) int {
if nodeIndex%2 == 0 {
return nodeIndex + 1
}
return nodeIndex - 1
}