diff --git a/nomt/core/hasher.go b/nomt/core/hasher.go index ce9850d333..b897c92935 100644 --- a/nomt/core/hasher.go +++ b/nomt/core/hasher.go @@ -1,46 +1,62 @@ package core -import "golang.org/x/crypto/sha3" +import "crypto/sha256" -// HashLeaf computes the hash of a leaf node: keccak256(keyPath || valueHash) -// with the MSB of byte 0 set to 1. -func HashLeaf(data *LeafData) Node { - h := sha3.NewLegacyKeccak256() - h.Write(data.KeyPath[:]) - h.Write(data.ValueHash[:]) - var out Node - h.Sum(out[:0]) - setMSB(&out) - return out -} +const ( + // StemSize is the number of bytes in a stem path (248 bits). + StemSize = 31 -// HashInternal computes the hash of an internal node: keccak256(left || right) -// with the MSB of byte 0 cleared to 0. + // StemNodeWidth is the number of value slots per stem node. + StemNodeWidth = 256 + + // HashSize is the size of a SHA256 hash in bytes. + HashSize = 32 +) + +// HashInternal computes SHA256(left || right) matching EIP-7864's InternalNode.Hash(). func HashInternal(data *InternalData) Node { - h := sha3.NewLegacyKeccak256() + h := sha256.New() h.Write(data.Left[:]) h.Write(data.Right[:]) var out Node h.Sum(out[:0]) - clearMSB(&out) return out } -// HashValue computes keccak256 of an arbitrary-length value. -func HashValue(value []byte) ValueHash { - h := sha3.NewLegacyKeccak256() - h.Write(value) - var out ValueHash +// HashStem computes the stem node hash matching EIP-7864's StemNode.Hash(). +// +// Algorithm: +// 1. SHA256 each non-nil value to get 256 leaf hashes (nil → zero hash) +// 2. Build an 8-level binary SHA256 tree (256 → 128 → ... → 1 root) +// Skip pairs where both children are zero (produce zero parent) +// 3. Final hash: SHA256(stem || 0x00 || subtreeRoot) +func HashStem(stem [StemSize]byte, values [StemNodeWidth][]byte) Node { + var data [StemNodeWidth]Node + for i, v := range values { + if v != nil { + data[i] = sha256.Sum256(v) + } + } + + h := sha256.New() + for level := 1; level <= 8; level++ { + for i := range StemNodeWidth / (1 << level) { + if data[i*2] == (Node{}) && data[i*2+1] == (Node{}) { + data[i] = Node{} + continue + } + h.Reset() + h.Write(data[i*2][:]) + h.Write(data[i*2+1][:]) + h.Sum(data[i][:0]) + } + } + + h.Reset() + h.Write(stem[:]) + h.Write([]byte{0x00}) + h.Write(data[0][:]) + var out Node h.Sum(out[:0]) return out } - -// setMSB sets the most significant bit (bit 7 of byte 0) to 1. -func setMSB(n *Node) { - n[0] |= 0x80 -} - -// clearMSB clears the most significant bit (bit 7 of byte 0) to 0. -func clearMSB(n *Node) { - n[0] &= 0x7F -} diff --git a/nomt/core/node.go b/nomt/core/node.go index aa4615996a..3aad934ee7 100644 --- a/nomt/core/node.go +++ b/nomt/core/node.go @@ -1,42 +1,40 @@ // Package core defines the fundamental data structures for a NOMT binary -// merkle trie. All types are pure computation with no I/O dependencies. +// merkle trie aligned with EIP-7864. All types are pure computation with +// no I/O dependencies. package core // Node is a 256-bit hash representing a node in the binary merkle trie. -// The MSB of byte 0 discriminates leaves (MSB=1) from internal nodes (MSB=0). -// The all-zeros value is reserved as the Terminator. +// The all-zeros value is reserved as the Terminator (empty sub-trie). +// Unlike the previous NOMT design, there is no MSB tagging — nodes are +// either terminators (zero) or opaque hashes (non-zero). type Node = [32]byte -// KeyPath is the 256-bit lookup path for a key in the trie. +// KeyPath is a full 256-bit key (31-byte stem + 1-byte suffix). +// Used for flat state lookups where the full 32-byte key is needed. type KeyPath = [32]byte -// ValueHash is the 256-bit hash of a value stored at a leaf. -type ValueHash = [32]byte +// StemPath is the 248-bit (31-byte) stem portion of a key. +// In the EIP-7864 trie, internal nodes traverse bits 0-247, then +// stem nodes hold 256 value slots indexed by the last byte. +type StemPath = [StemSize]byte // Terminator is the special node value denoting an empty sub-trie. // When this appears at a location, no key with a matching path prefix has a value. var Terminator Node -// NodeKind discriminates the three kinds of trie nodes. +// NodeKind discriminates the two kinds of trie nodes in the page tree. type NodeKind int const ( // NodeTerminator indicates an empty sub-trie (all-zero node). NodeTerminator NodeKind = iota - // NodeLeaf indicates a leaf node (MSB of byte 0 is 1). - NodeLeaf - // NodeInternal indicates an internal (branch) node (MSB of byte 0 is 0, non-zero). + // NodeInternal indicates a non-zero hash (internal node or stem hash). NodeInternal ) -// NodeKindOf returns the kind of the given node using MSB discrimination. -// -// If the MSB of byte 0 is set, it is a leaf. If the node is all zeros, -// it is a terminator. Otherwise it is an internal node. +// NodeKindOf returns the kind of the given node. +// In EIP-7864, the page tree only stores terminators and opaque hashes. func NodeKindOf(n *Node) NodeKind { - if n[0]>>7 == 1 { - return NodeLeaf - } if *n == Terminator { return NodeTerminator } @@ -48,24 +46,8 @@ func IsTerminator(n *Node) bool { return *n == Terminator } -// IsLeaf reports whether the node's MSB indicates a leaf. -func IsLeaf(n *Node) bool { - return n[0]>>7 == 1 -} - -// IsInternal reports whether the node is a non-terminator internal node. -func IsInternal(n *Node) bool { - return n[0]>>7 == 0 && *n != Terminator -} - // InternalData holds the preimage of an internal (branch) node. type InternalData struct { Left Node Right Node } - -// LeafData holds the preimage of a leaf node. -type LeafData struct { - KeyPath KeyPath - ValueHash ValueHash -} diff --git a/nomt/core/node_test.go b/nomt/core/node_test.go index 824969ad11..e9e19d40d8 100644 --- a/nomt/core/node_test.go +++ b/nomt/core/node_test.go @@ -1,6 +1,7 @@ package core import ( + "crypto/sha256" "testing" "github.com/stretchr/testify/assert" @@ -24,23 +25,18 @@ func TestNodeKindOf(t *testing.T) { want: NodeTerminator, }, { - name: "leaf with MSB set", + name: "non-zero hash is internal", node: Node{0x80, 0x01, 0x02}, - want: NodeLeaf, + want: NodeInternal, }, { - name: "leaf with all bits set in first byte", - node: Node{0xFF, 0x01}, - want: NodeLeaf, - }, - { - name: "internal node", + name: "any non-zero is internal", node: Node{0x01, 0x02, 0x03}, want: NodeInternal, }, { - name: "internal with MSB clear", - node: Node{0x7F, 0xFF, 0xFF}, + name: "high bits set is still internal", + node: Node{0xFF, 0xFF, 0xFF}, want: NodeInternal, }, } @@ -60,55 +56,6 @@ func TestIsTerminator(t *testing.T) { assert.False(t, IsTerminator(&nonZero)) } -func TestIsLeaf(t *testing.T) { - leaf := Node{0x80} - assert.True(t, IsLeaf(&leaf)) - - internal := Node{0x7F, 0xFF} - assert.False(t, IsLeaf(&internal)) -} - -func TestIsInternal(t *testing.T) { - internal := Node{0x01} - assert.True(t, IsInternal(&internal)) - - leaf := Node{0x80} - assert.False(t, IsInternal(&leaf)) - - assert.False(t, IsInternal(&Terminator)) -} - -func TestHashLeafSetsMSB(t *testing.T) { - data := &LeafData{ - KeyPath: KeyPath{0x01, 0x02, 0x03}, - ValueHash: ValueHash{0x04, 0x05, 0x06}, - } - result := HashLeaf(data) - require.True(t, IsLeaf(&result), "HashLeaf must produce a leaf node") - require.False(t, IsTerminator(&result)) -} - -func TestHashInternalClearsMSB(t *testing.T) { - data := &InternalData{ - Left: Node{0xFF, 0x01}, - Right: Node{0x80, 0x02}, - } - result := HashInternal(data) - require.True(t, IsInternal(&result), - "HashInternal must produce an internal node") - require.False(t, IsLeaf(&result)) -} - -func TestHashLeafDeterministic(t *testing.T) { - data := &LeafData{ - KeyPath: KeyPath{0xAB, 0xCD}, - ValueHash: ValueHash{0xEF, 0x01}, - } - h1 := HashLeaf(data) - h2 := HashLeaf(data) - assert.Equal(t, h1, h2, "same inputs must produce same hash") -} - func TestHashInternalDeterministic(t *testing.T) { data := &InternalData{ Left: Node{0x11, 0x22}, @@ -119,32 +66,77 @@ func TestHashInternalDeterministic(t *testing.T) { assert.Equal(t, h1, h2, "same inputs must produce same hash") } -func TestHashLeafDiffersFromInternal(t *testing.T) { - // Using the same 64-byte preimage for both should produce different - // hashes due to MSB tagging (even if the raw keccak is the same, - // the MSB bit will differ). - var key KeyPath - var val ValueHash - for i := range key { - key[i] = byte(i) - } - for i := range val { - val[i] = byte(i + 32) - } +func TestHashInternalMatchesSHA256(t *testing.T) { + left := Node{0x01, 0x02, 0x03} + right := Node{0x04, 0x05, 0x06} - leaf := HashLeaf(&LeafData{KeyPath: key, ValueHash: val}) - internal := HashInternal(&InternalData{Left: Node(key), Right: Node(val)}) + data := &InternalData{Left: left, Right: right} + got := HashInternal(data) - // They share the same keccak input, but MSB tagging makes them differ. - assert.NotEqual(t, leaf, internal, - "leaf and internal hashes must differ due to MSB tagging") + // Manual SHA256(left || right) + h := sha256.New() + h.Write(left[:]) + h.Write(right[:]) + expected := h.Sum(nil) + + assert.Equal(t, expected, got[:]) } -func TestHashValue(t *testing.T) { - v1 := HashValue([]byte("hello")) - v2 := HashValue([]byte("hello")) - v3 := HashValue([]byte("world")) - - assert.Equal(t, v1, v2, "same value must produce same hash") - assert.NotEqual(t, v1, v3, "different values must differ") +func TestHashInternalNoMSBTagging(t *testing.T) { + // With SHA256, the MSB is determined by the hash output, not forced. + // Just verify it produces a non-zero, non-terminator result. + data := &InternalData{ + Left: Node{0xFF, 0x01}, + Right: Node{0x80, 0x02}, + } + result := HashInternal(data) + require.False(t, IsTerminator(&result)) +} + +func TestHashStemDeterministic(t *testing.T) { + var stem StemPath + stem[0] = 0xAB + stem[1] = 0xCD + + var values [StemNodeWidth][]byte + values[0] = make([]byte, 32) + values[0][0] = 0x01 + + h1 := HashStem(stem, values) + h2 := HashStem(stem, values) + assert.Equal(t, h1, h2, "same inputs must produce same hash") +} + +func TestHashStemAllNilIsNotZero(t *testing.T) { + // Even with all nil values, the stem hash includes the stem bytes, + // so it should NOT be the zero hash (unless stem is also zero and + // subtree root is zero... let's check). + var stem StemPath + var values [StemNodeWidth][]byte + + // With all-zero stem and all-nil values, the subtree root is zero. + // Final = SHA256(zero_stem || 0x00 || zero_hash) + result := HashStem(stem, values) + assert.False(t, IsTerminator(&result), + "stem hash should not be terminator even with empty values") +} + +func TestHashStemSingleValue(t *testing.T) { + var stem StemPath + stem[0] = 0x42 + + var values [StemNodeWidth][]byte + val := make([]byte, 32) + val[0] = 0xFF + values[0] = val + + result := HashStem(stem, values) + assert.False(t, IsTerminator(&result)) +} + +func TestStemPathType(t *testing.T) { + // Verify StemPath is 31 bytes. + var sp StemPath + assert.Equal(t, StemSize, len(sp)) + assert.Equal(t, 31, len(sp)) } diff --git a/nomt/core/triepos.go b/nomt/core/triepos.go index 4a7d555e9f..61cb32d3be 100644 --- a/nomt/core/triepos.go +++ b/nomt/core/triepos.go @@ -13,13 +13,17 @@ func NewTriePosition() TriePosition { return TriePosition{} } +// MaxTrieDepth is the maximum depth of the internal node tree (248 bits = 31 bytes). +// Stem nodes exist at this depth; the last 8 bits are the stem suffix. +const MaxTrieDepth = StemSize * 8 // 248 + // 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 { + if depth > MaxTrieDepth { panic("triepos: depth out of range") } pagePath := lastPagePath(path[:], depth) @@ -57,8 +61,8 @@ func (p *TriePosition) NodeIndex() int { // 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 p.depth >= MaxTrieDepth { + panic("triepos: can't descend past 248 bits") } if int(p.depth)%PageDepth == 0 { // Entering a new page: node index resets. diff --git a/nomt/core/triepos_test.go b/nomt/core/triepos_test.go index 37800f0fe5..466c424743 100644 --- a/nomt/core/triepos_test.go +++ b/nomt/core/triepos_test.go @@ -158,13 +158,23 @@ func TestTriePositionChildPageIndex(t *testing.T) { assert.Equal(t, uint8(0), p.ChildPageIndex()) } -func TestTriePositionMax255Depth(t *testing.T) { +func TestTriePositionMax248Depth(t *testing.T) { p := NewTriePosition() - for range 255 { + for range 247 { p.Down(true) } - assert.Equal(t, uint16(255), p.Depth()) - // One more descent should work (to 256). + assert.Equal(t, uint16(247), p.Depth()) + // One more descent should work (to 248, the max). p.Down(false) - assert.Equal(t, uint16(256), p.Depth()) + assert.Equal(t, uint16(248), p.Depth()) +} + +func TestTriePositionPanicsBeyond248(t *testing.T) { + p := NewTriePosition() + for range 248 { + p.Down(true) + } + assert.Equal(t, uint16(248), p.Depth()) + // Going one more should panic. + assert.Panics(t, func() { p.Down(false) }) } diff --git a/nomt/core/update.go b/nomt/core/update.go index 28911ab70c..acd33c3631 100644 --- a/nomt/core/update.go +++ b/nomt/core/update.go @@ -1,48 +1,40 @@ package core -import "sort" - -// LeafOp represents a leaf operation: set or delete. -// A nil ValueHash pointer means delete. -type LeafOp struct { - Key KeyPath - Value *ValueHash +// 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 } -// KeyValue is a resolved (key, value) pair for trie building. -type KeyValue struct { - Key KeyPath - Value ValueHash -} - -// WriteNodeKind enumerates the types of write commands from BuildTrie. +// WriteNodeKind enumerates the types of write commands from BuildInternalTree. type WriteNodeKind int const ( - WriteNodeLeaf WriteNodeKind = iota - WriteNodeInternal - WriteNodeTerminator + 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 - LeafData *LeafData // set for leaf writes InternalData *InternalData // set for internal writes // Navigation: move up 1 before writing (true for internal nodes and - // non-first leaves). + // non-first stem placements). GoUp bool - // Navigation: bits to descend after going up (only for leaf writes). + // Navigation: bits to descend after going up (only for stem writes). DownBits []bool } -// SharedBits counts the number of shared prefix bits between two key paths, -// starting after `skip` bits. -func SharedBits(a, b *KeyPath, skip int) int { +// 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 - for i := skip; i < 256; i++ { + 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 { @@ -53,163 +45,104 @@ func SharedBits(a, b *KeyPath, skip int) int { return count } -// LeafOpsSpliced creates a combined operation list from an existing leaf and -// new operations. If the existing leaf's key is not in ops, it is spliced in. -// Deletions (nil value) are filtered out. -func LeafOpsSpliced(existingLeaf *LeafData, ops []LeafOp) []KeyValue { - // Find splice position: where the existing leaf would be inserted. - spliceIndex := -1 - if existingLeaf != nil { - idx := sort.Search(len(ops), func(i int) bool { - return keyPathCmp(&ops[i].Key, &existingLeaf.KeyPath) >= 0 - }) - if idx >= len(ops) || ops[idx].Key != existingLeaf.KeyPath { - spliceIndex = idx - } - } - - result := make([]KeyValue, 0, len(ops)+1) - - if spliceIndex < 0 { - // No splicing needed — just filter out deletes. - for _, op := range ops { - if op.Value != nil { - result = append(result, KeyValue{op.Key, *op.Value}) - } - } - return result - } - - // Before splice point. - for _, op := range ops[:spliceIndex] { - if op.Value != nil { - result = append(result, KeyValue{op.Key, *op.Value}) - } - } - - // The existing leaf. - result = append(result, KeyValue{ - existingLeaf.KeyPath, - existingLeaf.ValueHash, - }) - - // After splice point. - for _, op := range ops[spliceIndex:] { - if op.Value != nil { - result = append(result, KeyValue{op.Key, *op.Value}) - } - } - - return result -} - -// BuildTrie builds a compact sub-trie from sorted (key, value) pairs. +// 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 (KeyPath, ValueHash) pairs. +// 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. // -// The algorithm uses a 3-pointer sliding window (a, b, c) over the sorted -// ops to determine each leaf's depth based on shared bits with its neighbors. -// Internal nodes are computed by hashing up a left-frontier stack. -func BuildTrie(skip int, ops []KeyValue, visit func(WriteNode)) Node { +// 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 { - ld := LeafData{ - KeyPath: ops[0].Key, - ValueHash: ops[0].Value, - } - h := HashLeaf(&ld) visit(WriteNode{ - Kind: WriteNodeLeaf, - Node: h, - LeafData: &ld, - GoUp: false, + Kind: WriteNodeStem, + Node: ops[0].Hash, + GoUp: false, }) - return h + return ops[0].Hash } - // 3-pointer left-frontier algorithm. + // 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(k1, k2 *KeyPath) int { - return SharedBits(k1, k2, skip) + commonAfterPrefix := func(s1, s2 *StemPath) int { + return StemSharedBits(s1, s2, skip) } - // Sliding window: a, b, c. - var aKey *KeyPath - var aVal *ValueHash + var aStem *StemPath for bIdx := 0; bIdx < len(ops); bIdx++ { - thisKey := &ops[bIdx].Key - thisVal := &ops[bIdx].Value + thisStem := &ops[bIdx].Stem + thisHash := ops[bIdx].Hash var n1 *int - if aKey != nil { - v := commonAfterPrefix(aKey, thisKey) + if aStem != nil { + v := commonAfterPrefix(aStem, thisStem) n1 = &v } var n2 *int if bIdx+1 < len(ops) { - v := commonAfterPrefix(&ops[bIdx+1].Key, thisKey) + v := commonAfterPrefix(&ops[bIdx+1].Stem, thisStem) n2 = &v } - ld := LeafData{KeyPath: *thisKey, ValueHash: *thisVal} - leaf := HashLeaf(&ld) - - var leafDepth, hashUpLayers int + var stemDepth, hashUpLayers int switch { case n1 == nil && n2 == nil: - leafDepth = 0 + stemDepth = 0 hashUpLayers = 0 case n1 == nil && n2 != nil: - leafDepth = *n2 + 1 + stemDepth = *n2 + 1 hashUpLayers = 0 case n1 != nil && n2 == nil: - leafDepth = *n1 + 1 + stemDepth = *n1 + 1 hashUpLayers = *n1 + 1 default: - leafDepth = max(*n1, *n2) + 1 + stemDepth = max(*n1, *n2) + 1 hashUpLayers = 0 if *n1 > *n2 { hashUpLayers = *n1 - *n2 } } - layer := leafDepth - lastNode := leaf + layer := stemDepth + lastNode := thisHash // Compute down bits for the visitor. downStart := skip if n1 != nil { downStart = skip + *n1 } - leafEndBit := skip + leafDepth + stemEndBit := skip + stemDepth var downBits []bool - if leafEndBit > downStart { - downBits = make([]bool, leafEndBit-downStart) - for i := downStart; i < leafEndBit; i++ { - downBits[i-downStart] = bitAt(thisKey, i) + if stemEndBit > downStart { + downBits = make([]bool, stemEndBit-downStart) + for i := downStart; i < stemEndBit; i++ { + downBits[i-downStart] = stemBitAt(thisStem, i) } } visit(WriteNode{ - Kind: WriteNodeLeaf, - Node: leaf, - LeafData: &ld, + Kind: WriteNodeStem, + Node: thisHash, GoUp: n1 != nil, DownBits: downBits, }) @@ -217,10 +150,9 @@ func BuildTrie(skip int, ops []KeyValue, visit func(WriteNode)) Node { // Hash upward. for h := 0; h < hashUpLayers; h++ { layer-- - bitIdx := skip + layer // the bit at this layer - bit := bitAt(thisKey, bitIdx) + bitIdx := skip + layer + bit := stemBitAt(thisStem, bitIdx) - // Pop sibling from pending if it matches. var sibling Node if len(pendingSiblings) > 0 && pendingSiblings[len(pendingSiblings)-1].layer == layer+1 { @@ -247,10 +179,8 @@ func BuildTrie(skip int, ops []KeyValue, visit func(WriteNode)) Node { pendingSiblings = append(pendingSiblings, pendingSibling{node: lastNode, layer: layer}) - aKey = thisKey - aVal = thisVal + aStem = thisStem } - _ = aVal // used in the loop to track state if len(pendingSiblings) > 0 { return pendingSiblings[len(pendingSiblings)-1].node @@ -258,11 +188,11 @@ func BuildTrie(skip int, ops []KeyValue, visit func(WriteNode)) Node { return Terminator } -func bitAt(key *KeyPath, idx int) bool { - return (key[idx/8]>>(7-idx%8))&1 == 1 +func stemBitAt(stem *StemPath, idx int) bool { + return (stem[idx/8]>>(7-idx%8))&1 == 1 } -func keyPathCmp(a, b *KeyPath) int { +func stemPathCmp(a, b *StemPath) int { for i := range a { if a[i] < b[i] { return -1 diff --git a/nomt/core/update_test.go b/nomt/core/update_test.go index a6725b5fc1..ada18e3547 100644 --- a/nomt/core/update_test.go +++ b/nomt/core/update_test.go @@ -7,43 +7,45 @@ import ( "github.com/stretchr/testify/require" ) -func TestSharedBits(t *testing.T) { +func TestStemSharedBits(t *testing.T) { tests := []struct { name string - a, b KeyPath + a, b StemPath skip int expected int }{ - {"identical", KeyPath{0xFF}, KeyPath{0xFF}, 0, 256}, - {"differ at bit 0", KeyPath{0x80}, KeyPath{0x00}, 0, 0}, - {"share 4 bits", KeyPath{0xF0}, KeyPath{0xF8}, 0, 4}, - {"with skip", KeyPath{0xF0}, KeyPath{0xF8}, 4, 0}, + {"identical", StemPath{0xFF}, StemPath{0xFF}, 0, 248}, + {"differ at bit 0", StemPath{0x80}, StemPath{0x00}, 0, 0}, + {"share 4 bits", StemPath{0xF0}, StemPath{0xF8}, 0, 4}, + {"with skip", StemPath{0xF0}, StemPath{0xF8}, 4, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := SharedBits(&tt.a, &tt.b, tt.skip) + got := StemSharedBits(&tt.a, &tt.b, tt.skip) assert.Equal(t, tt.expected, got) }) } } -// makeKV creates a (key, value) pair where both key and value are filled with b. -func makeKV(b byte) KeyValue { - var key KeyPath - var val ValueHash - for i := range key { - key[i] = b +// makeSKV creates a StemKeyValue where the stem is filled with b and +// the hash is a deterministic non-zero value derived from b. +func makeSKV(b byte) StemKeyValue { + var stem StemPath + for i := range stem { + stem[i] = b } - for i := range val { - val[i] = b + // Use a simple deterministic hash for testing. + var hash Node + for i := range hash { + hash[i] = b ^ byte(i) } - return KeyValue{Key: key, Value: val} + return StemKeyValue{Stem: stem, Hash: hash} } -func TestBuildTrieEmpty(t *testing.T) { +func TestBuildInternalTreeEmpty(t *testing.T) { var visited []WriteNode - root := BuildTrie(0, nil, func(wn WriteNode) { + root := BuildInternalTree(0, nil, func(wn WriteNode) { visited = append(visited, wn) }) @@ -52,121 +54,95 @@ func TestBuildTrieEmpty(t *testing.T) { assert.Equal(t, Terminator, root) } -func TestBuildTrieSingleLeaf(t *testing.T) { - kv := makeKV(0xFF) +func TestBuildInternalTreeSingleStem(t *testing.T) { + skv := makeSKV(0xFF) var visited []WriteNode - root := BuildTrie(0, []KeyValue{kv}, func(wn WriteNode) { + root := BuildInternalTree(0, []StemKeyValue{skv}, func(wn WriteNode) { visited = append(visited, wn) }) require.Len(t, visited, 1) - assert.Equal(t, WriteNodeLeaf, visited[0].Kind) + assert.Equal(t, WriteNodeStem, visited[0].Kind) assert.False(t, visited[0].GoUp) - assert.True(t, IsLeaf(&root)) - - expected := HashLeaf(&LeafData{ - KeyPath: kv.Key, - ValueHash: kv.Value, - }) - assert.Equal(t, expected, root) + assert.False(t, IsTerminator(&root)) + assert.Equal(t, skv.Hash, root) } -func TestBuildTrieTwoLeaves(t *testing.T) { - // Keys: 0x00... and 0xFF... differ at bit 0. - kv0 := makeKV(0x00) - kvF := makeKV(0xFF) +func TestBuildInternalTreeTwoStems(t *testing.T) { + // Stems: 0x00... and 0xFF... differ at bit 0. + skv0 := makeSKV(0x00) + skvF := makeSKV(0xFF) var visited []WriteNode - root := BuildTrie(0, []KeyValue{kv0, kvF}, func(wn WriteNode) { + root := BuildInternalTree(0, []StemKeyValue{skv0, skvF}, func(wn WriteNode) { visited = append(visited, wn) }) - // Should visit: leaf_0, leaf_F, internal(leaf_0, leaf_F). + // Should visit: stem_0, stem_F, internal(stem_0, stem_F). require.Len(t, visited, 3) - assert.Equal(t, WriteNodeLeaf, visited[0].Kind) - assert.Equal(t, WriteNodeLeaf, visited[1].Kind) + assert.Equal(t, WriteNodeStem, visited[0].Kind) + assert.Equal(t, WriteNodeStem, visited[1].Kind) assert.Equal(t, WriteNodeInternal, visited[2].Kind) - leaf0 := HashLeaf(&LeafData{KeyPath: kv0.Key, ValueHash: kv0.Value}) - leafF := HashLeaf(&LeafData{KeyPath: kvF.Key, ValueHash: kvF.Value}) - expected := HashInternal(&InternalData{Left: leaf0, Right: leafF}) - + expected := HashInternal(&InternalData{Left: skv0.Hash, Right: skvF.Hash}) assert.Equal(t, expected, root) - assert.True(t, IsInternal(&root)) } -func TestBuildTrieThreeLeaves(t *testing.T) { - // Three keys sharing common prefixes. - // 0b00010001... = 0x11 - // 0b00010010... = 0x12 - // 0b00010100... = 0x14 - kv1 := makeKV(0x11) - kv2 := makeKV(0x12) - kv3 := makeKV(0x14) +func TestBuildInternalTreeThreeStems(t *testing.T) { + skv1 := makeSKV(0x11) + skv2 := makeSKV(0x12) + skv3 := makeSKV(0x14) var visited []WriteNode - root := BuildTrie(0, []KeyValue{kv1, kv2, kv3}, func(wn WriteNode) { + root := BuildInternalTree(0, []StemKeyValue{skv1, skv2, skv3}, func(wn WriteNode) { visited = append(visited, wn) }) - assert.True(t, IsInternal(&root) || IsLeaf(&root), - "root should be non-terminator") + assert.False(t, IsTerminator(&root)) // Verify determinism. var visited2 []WriteNode - root2 := BuildTrie(0, []KeyValue{kv1, kv2, kv3}, func(wn WriteNode) { + root2 := BuildInternalTree(0, []StemKeyValue{skv1, skv2, skv3}, func(wn WriteNode) { visited2 = append(visited2, wn) }) assert.Equal(t, root, root2) assert.Equal(t, len(visited), len(visited2)) } -func TestBuildTrieWithSkip(t *testing.T) { - // Keys all share prefix 0001 (4 bits): 0x11, 0x12, 0x14. - kv1 := makeKV(0x11) - kv2 := makeKV(0x12) - kv3 := makeKV(0x14) +func TestBuildInternalTreeWithSkip(t *testing.T) { + skv1 := makeSKV(0x11) + skv2 := makeSKV(0x12) + skv3 := makeSKV(0x14) var visited []WriteNode - root := BuildTrie(4, []KeyValue{kv1, kv2, kv3}, func(wn WriteNode) { + root := BuildInternalTree(4, []StemKeyValue{skv1, skv2, skv3}, func(wn WriteNode) { visited = append(visited, wn) }) - // Should produce a non-trivial sub-trie. assert.False(t, IsTerminator(&root)) assert.True(t, len(visited) >= 3) } -func TestBuildTrieMultiValue(t *testing.T) { - // Matches the Rust multi_value test pattern. - // 0b00010000 = 0x10 - // 0b00100000 = 0x20 - // 0b01000000 = 0x40 - // 0b10100000 = 0xA0 - // 0b10110000 = 0xB0 - kvA := makeKV(0x10) - kvB := makeKV(0x20) - kvC := makeKV(0x40) - kvD := makeKV(0xA0) - kvE := makeKV(0xB0) +func TestBuildInternalTreeMultiValue(t *testing.T) { + skvA := makeSKV(0x10) + skvB := makeSKV(0x20) + skvC := makeSKV(0x40) + skvD := makeSKV(0xA0) + skvE := makeSKV(0xB0) var nodes []Node - root := BuildTrie(0, []KeyValue{kvA, kvB, kvC, kvD, kvE}, + root := BuildInternalTree(0, []StemKeyValue{skvA, skvB, skvC, skvD, skvE}, func(wn WriteNode) { nodes = append(nodes, wn.Node) }) // Manually verify the trie structure. - leafA := HashLeaf(&LeafData{KeyPath: kvA.Key, ValueHash: kvA.Value}) - leafB := HashLeaf(&LeafData{KeyPath: kvB.Key, ValueHash: kvB.Value}) - leafC := HashLeaf(&LeafData{KeyPath: kvC.Key, ValueHash: kvC.Value}) - leafD := HashLeaf(&LeafData{KeyPath: kvD.Key, ValueHash: kvD.Value}) - leafE := HashLeaf(&LeafData{KeyPath: kvE.Key, ValueHash: kvE.Value}) + // A=0x10 (0001...), B=0x20 (0010...), C=0x40 (0100...) + // D=0xA0 (1010...), E=0xB0 (1011...) + branchAB := HashInternal(&InternalData{Left: skvA.Hash, Right: skvB.Hash}) + branchABC := HashInternal(&InternalData{Left: branchAB, Right: skvC.Hash}) - branchAB := HashInternal(&InternalData{Left: leafA, Right: leafB}) - branchABC := HashInternal(&InternalData{Left: branchAB, Right: leafC}) - - branchDE1 := HashInternal(&InternalData{Left: leafD, Right: leafE}) + branchDE1 := HashInternal(&InternalData{Left: skvD.Hash, Right: skvE.Hash}) branchDE2 := HashInternal(&InternalData{Left: Terminator, Right: branchDE1}) branchDE3 := HashInternal(&InternalData{Left: branchDE2, Right: Terminator}) @@ -174,65 +150,3 @@ func TestBuildTrieMultiValue(t *testing.T) { assert.Equal(t, expected, root) } - -func TestLeafOpsSplicedNoExisting(t *testing.T) { - val := ValueHash{0x01} - ops := []LeafOp{ - {Key: KeyPath{0x10}, Value: &val}, - {Key: KeyPath{0x20}, Value: &val}, - } - - result := LeafOpsSpliced(nil, ops) - assert.Len(t, result, 2) -} - -func TestLeafOpsSplicedWithExistingLeaf(t *testing.T) { - val := ValueHash{0x01} - ops := []LeafOp{ - {Key: KeyPath{0x10}, Value: &val}, - {Key: KeyPath{0x30}, Value: &val}, - } - - existing := &LeafData{ - KeyPath: KeyPath{0x20}, - ValueHash: ValueHash{0x02}, - } - - result := LeafOpsSpliced(existing, ops) - assert.Len(t, result, 3) - assert.Equal(t, KeyPath{0x10}, result[0].Key) - assert.Equal(t, KeyPath{0x20}, result[1].Key) - assert.Equal(t, KeyPath{0x30}, result[2].Key) -} - -func TestLeafOpsSplicedDeleteFiltered(t *testing.T) { - val := ValueHash{0x01} - ops := []LeafOp{ - {Key: KeyPath{0x10}, Value: &val}, - {Key: KeyPath{0x20}, Value: nil}, // delete - {Key: KeyPath{0x30}, Value: &val}, - } - - result := LeafOpsSpliced(nil, ops) - assert.Len(t, result, 2) - assert.Equal(t, KeyPath{0x10}, result[0].Key) - assert.Equal(t, KeyPath{0x30}, result[1].Key) -} - -func TestLeafOpsSplicedExistingKeyInOps(t *testing.T) { - val := ValueHash{0x01} - newVal := ValueHash{0x99} - ops := []LeafOp{ - {Key: KeyPath{0x20}, Value: &newVal}, - } - - existing := &LeafData{ - KeyPath: KeyPath{0x20}, - ValueHash: val, - } - - // The existing leaf should NOT be spliced because its key is in ops. - result := LeafOpsSpliced(existing, ops) - assert.Len(t, result, 1) - assert.Equal(t, newVal, result[0].Value) -}