go-ethereum/nomt/core/update.go
weiihann 2db24e85a3 nomt/core: redesign for EIP-7864 compatibility (Phase A)
Replace Keccak256+MSB tagging with SHA256. Remove leaf nodes entirely,
replacing them with opaque stem hashes. Simplify NodeKind to just
Terminator and Internal. Add HashStem (8-level binary SHA256 tree
matching bintrie StemNode.Hash). Reduce max trie depth from 256 to 248
(31-byte stem path). Replace BuildTrie/LeafOp with
BuildInternalTree/StemKeyValue.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 21:59:53 +08:00

205 lines
4.9 KiB
Go

package core
// StemKeyValue is a resolved (stemPath, stemHash) pair for the page tree.
// The stem hash is precomputed by the integration layer using HashStem.
type StemKeyValue struct {
Stem StemPath // 31-byte stem (248 bits)
Hash Node // precomputed SHA256 stem hash
}
// WriteNodeKind enumerates the types of write commands from BuildInternalTree.
type WriteNodeKind int
const (
WriteNodeStem WriteNodeKind = iota // opaque stem hash placed at tree bottom
WriteNodeInternal // internal node (hash of left+right)
WriteNodeTerminator // empty sub-trie
)
// WriteNode represents a node to be written during trie building.
type WriteNode struct {
Kind WriteNodeKind
Node Node
InternalData *InternalData // set for internal writes
// Navigation: move up 1 before writing (true for internal nodes and
// non-first stem placements).
GoUp bool
// Navigation: bits to descend after going up (only for stem writes).
DownBits []bool
}
// StemSharedBits counts the number of shared prefix bits between two stem
// paths, starting after `skip` bits.
func StemSharedBits(a, b *StemPath, skip int) int {
count := 0
maxBits := StemSize * 8 // 248
for i := skip; i < maxBits; i++ {
aBit := (a[i/8] >> (7 - i%8)) & 1
bBit := (b[i/8] >> (7 - i%8)) & 1
if aBit != bBit {
break
}
count++
}
return count
}
// BuildInternalTree builds a compact internal-node sub-trie from sorted
// (stem, hash) pairs.
//
// skip: the number of prefix bits already consumed (all ops share this prefix).
// ops: sorted StemKeyValue pairs (by stem path).
// visit: callback invoked for each computed node, bottom-up.
//
// Returns the root node of the built sub-trie.
//
// This replaces the old BuildTrie. The key difference: there are no leaf
// nodes — stem hashes are opaque values placed at tree positions, and
// internal nodes are always SHA256(left || right) with no MSB tagging
// or leaf compaction.
func BuildInternalTree(skip int, ops []StemKeyValue, visit func(WriteNode)) Node {
if len(ops) == 0 {
visit(WriteNode{Kind: WriteNodeTerminator, Node: Terminator})
return Terminator
}
if len(ops) == 1 {
visit(WriteNode{
Kind: WriteNodeStem,
Node: ops[0].Hash,
GoUp: false,
})
return ops[0].Hash
}
// 3-pointer left-frontier algorithm (same structure as old BuildTrie
// but without leaf hashing or leaf compaction).
type pendingSibling struct {
node Node
layer int
}
pendingSiblings := make([]pendingSibling, 0, 16)
commonAfterPrefix := func(s1, s2 *StemPath) int {
return StemSharedBits(s1, s2, skip)
}
var aStem *StemPath
for bIdx := 0; bIdx < len(ops); bIdx++ {
thisStem := &ops[bIdx].Stem
thisHash := ops[bIdx].Hash
var n1 *int
if aStem != nil {
v := commonAfterPrefix(aStem, thisStem)
n1 = &v
}
var n2 *int
if bIdx+1 < len(ops) {
v := commonAfterPrefix(&ops[bIdx+1].Stem, thisStem)
n2 = &v
}
var stemDepth, hashUpLayers int
switch {
case n1 == nil && n2 == nil:
stemDepth = 0
hashUpLayers = 0
case n1 == nil && n2 != nil:
stemDepth = *n2 + 1
hashUpLayers = 0
case n1 != nil && n2 == nil:
stemDepth = *n1 + 1
hashUpLayers = *n1 + 1
default:
stemDepth = max(*n1, *n2) + 1
hashUpLayers = 0
if *n1 > *n2 {
hashUpLayers = *n1 - *n2
}
}
layer := stemDepth
lastNode := thisHash
// Compute down bits for the visitor.
downStart := skip
if n1 != nil {
downStart = skip + *n1
}
stemEndBit := skip + stemDepth
var downBits []bool
if stemEndBit > downStart {
downBits = make([]bool, stemEndBit-downStart)
for i := downStart; i < stemEndBit; i++ {
downBits[i-downStart] = stemBitAt(thisStem, i)
}
}
visit(WriteNode{
Kind: WriteNodeStem,
Node: thisHash,
GoUp: n1 != nil,
DownBits: downBits,
})
// Hash upward.
for h := 0; h < hashUpLayers; h++ {
layer--
bitIdx := skip + layer
bit := stemBitAt(thisStem, bitIdx)
var sibling Node
if len(pendingSiblings) > 0 &&
pendingSiblings[len(pendingSiblings)-1].layer == layer+1 {
sibling = pendingSiblings[len(pendingSiblings)-1].node
pendingSiblings = pendingSiblings[:len(pendingSiblings)-1]
}
var id InternalData
if bit {
id = InternalData{Left: sibling, Right: lastNode}
} else {
id = InternalData{Left: lastNode, Right: sibling}
}
lastNode = HashInternal(&id)
visit(WriteNode{
Kind: WriteNodeInternal,
Node: lastNode,
InternalData: &id,
GoUp: true,
})
}
pendingSiblings = append(pendingSiblings,
pendingSibling{node: lastNode, layer: layer})
aStem = thisStem
}
if len(pendingSiblings) > 0 {
return pendingSiblings[len(pendingSiblings)-1].node
}
return Terminator
}
func stemBitAt(stem *StemPath, idx int) bool {
return (stem[idx/8]>>(7-idx%8))&1 == 1
}
func stemPathCmp(a, b *StemPath) int {
for i := range a {
if a[i] < b[i] {
return -1
}
if a[i] > b[i] {
return 1
}
}
return 0
}