Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mssmt: add new batched ms-smt leaf insertion with recursive merge update #1347

Draft
wants to merge 47 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
b799654
aider: feat: Implement batched insertion method for CompactedTree
Roasbeef Feb 3, 2025
9d3a9d4
aider: feat: Add empty-batch guard in batched_insert and new unit tests
Roasbeef Feb 3, 2025
bb2a8d0
aider: fix: Remove duplicate package declaration in tree_test.go
Roasbeef Feb 3, 2025
c0ae5fe
aider: fix: Remove unnecessary type assertions for NewBranch return v…
Roasbeef Feb 3, 2025
fb04a20
aider: refactor: Rename package from mssmt to mssmt_test in tree_test.go
Roasbeef Feb 3, 2025
1309ef0
aider: feat: Export batchedInsertionEntry and update test references …
Roasbeef Feb 3, 2025
30b5d90
aider: fix: Update references to exported fields Key and Leaf in comp…
Roasbeef Feb 3, 2025
c8d9dbe
aider: refactor: Replace mssmt.treeLeaf with inline struct and update…
Roasbeef Feb 3, 2025
a0e84a3
aider: fix: Fix SEARCH block to match existing lines in tree_test.go
Roasbeef Feb 3, 2025
a04b554
aider: fix: Update test file to reference exported symbols from mssmt…
Roasbeef Feb 3, 2025
cab0def
aider: fix: Correct type in BatchedInsert call in tree_test.go
Roasbeef Feb 3, 2025
0dd3aa4
aider: fix: Remove unnecessary type assertion in TestBatchedInsert fu…
Roasbeef Feb 3, 2025
92f1a0c
aider: fix: Remove unnecessary type assertion in TestBatchedInsertOve…
Roasbeef Feb 3, 2025
ce0d5f6
aider: fix: Reuse precomputed empty branch for left and right childre…
Roasbeef Feb 3, 2025
f849d8d
aider: feat: Implement compacted leaf creation in batched_insert for …
Roasbeef Feb 3, 2025
752f4a0
aider: feat: Update batched_insert to handle collisions with compacte…
Roasbeef Feb 3, 2025
39f9a86
aider: fix: Use updated leftChild and rightChild in branch creation t…
Roasbeef Feb 3, 2025
ee360e9
aider: fix: Remove inner redeclarations of newLeft and newRight in ba…
Roasbeef Feb 3, 2025
eb8f224
aider: fix: Record height of compacted leaf and adjust extraction logic
Roasbeef Feb 3, 2025
23313cf
aider: fix: Rename parameter and update variable type in Extract method
Roasbeef Feb 3, 2025
305d412
aider: fix: Wrap stored leaf in branch for requestedHeight equal to c…
Roasbeef Feb 3, 2025
5fc7a89
aider: fix: Ensure Extract returns a BranchNode for requestedHeight >…
Roasbeef Feb 3, 2025
ac4d78e
aider: fix: Ensure Extract always returns a BranchNode from Compacted…
Roasbeef Feb 3, 2025
fcc3fbd
aider: fix: Ensure Extract always returns a branch node from a compac…
Roasbeef Feb 3, 2025
ab01b4a
aider: fix: Declare current as Node interface type in Extract method
Roasbeef Feb 3, 2025
e9de62d
aider: fix: Ensure Extract always returns a branch node to prevent panic
Roasbeef Feb 3, 2025
013f658
aider: fix: Add fmt import to resolve undefined fmt errors in node.go
Roasbeef Feb 3, 2025
a4be104
aider: fix: Ensure full extraction of compacted leaves to avoid panic…
Roasbeef Feb 3, 2025
acbf785
aider: fix: Fully expand compacted leaf nodes in walkDown to prevent …
Roasbeef Feb 3, 2025
b414194
aider: fix: Return underlying leaf directly from compacted leaf in wa…
Roasbeef Feb 3, 2025
29c77cb
aider: fix: Sort entries by key in BatchedInsert to ensure correct in…
Roasbeef Feb 3, 2025
e6018b5
aider: fix: Ensure correct child ordering in batched_insert branch cr…
Roasbeef Feb 3, 2025
41cd8ac
aider: chore: Remove debug print statements from Extract and walkDown…
Roasbeef Feb 3, 2025
b6f47fc
aider: fix: Remove unused "fmt" import from mssmt/node.go
Roasbeef Feb 3, 2025
960e67b
aider: refactor: Rename batched_insert to batchedInsert and add helpe…
Roasbeef Feb 3, 2025
5968e0f
aider: feat: Implement recursive subtree processing for batched inser…
Roasbeef Feb 3, 2025
c210afb
aider: refactor: Simplify batchedInsert by extracting subtree process…
Roasbeef Feb 3, 2025
8c26686
aider: fix: Remove leading '+' characters from comments in compacted_…
Roasbeef Feb 3, 2025
53ef400
aider: fix: Remove leading '+' characters from processSubtree functio…
Roasbeef Feb 3, 2025
934adbc
aider: feat: Add BatchedInsertionEntry type and partitionEntries func…
Roasbeef Feb 3, 2025
a3591a5
aider: refactor: Move partitionEntries function to package level to f…
Roasbeef Feb 3, 2025
3c19cbe
aider: feat: Add BatchedInsertionEntry type for batched insertions in…
Roasbeef Feb 3, 2025
596f801
mssmt: fix formatting
Roasbeef Feb 3, 2025
6d42508
mssst: revert changes to walkDown+Extract
Roasbeef Feb 3, 2025
7447a5a
aider: refactor: Simplify batched insertion logic in CompactedTree im…
Roasbeef Feb 3, 2025
d32b80d
aider: feat: Add documentation for processCompactedLeaf function in c…
Roasbeef Feb 3, 2025
7e3529c
aider: refactor: Improve comments and documentation in compacted_tree.go
Roasbeef Feb 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
289 changes: 274 additions & 15 deletions mssmt/compacted_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import (
"context"
"bytes"
"fmt"
"sort"
)

// CompactedTree represents a compacted Merkle-Sum Sparse Merkle Tree (MS-SMT).
Expand All @@ -15,6 +17,276 @@
store TreeStore
}

// batched_insert handles the insertion of multiple entries in one go.
func (t *CompactedTree) batched_insert(tx TreeStoreUpdateTx, entries []BatchedInsertionEntry, height int, root *BranchNode) (*BranchNode, error) {
// Base-case: If we've reached the bottom, simply return the current branch.
if height >= lastBitIndex {
return root, nil
}

// Guard against empty batch.
if len(entries) == 0 {
return root, nil
}

// Partition entries into two groups based on bit at current height.
var leftEntries, rightEntries []BatchedInsertionEntry
for _, entry := range entries {
if bitIndex(uint8(height), &entry.Key) == 0 {

Check failure on line 35 in mssmt/compacted_tree.go

View workflow job for this annotation

GitHub Actions / Lint check

G601: Implicit memory aliasing in for loop. (gosec)
leftEntries = append(leftEntries, entry)
} else {
rightEntries = append(rightEntries, entry)
}
}

// Get the current children from the node.
leftChild, rightChild, err := tx.GetChildren(height, root.NodeHash())
if err != nil {
return nil, err
}

// Process left subtree:
var newLeft Node
if len(leftEntries) > 0 {
// Check if the current left child is not empty.
if leftChild != EmptyTree[height+1] {
// If the existing child is a compacted leaf, we must handle potential collisions.
if cl, ok := leftChild.(*CompactedLeafNode); ok {
if len(leftEntries) == 1 {
entry := leftEntries[0]
if entry.Key == cl.Key() {
// Replacement: update the compacted leaf.
newLeaf := NewCompactedLeafNode(height+1, &entry.Key, entry.Leaf)
if err := tx.DeleteCompactedLeaf(cl.NodeHash()); err != nil {
return nil, err
}
if err := tx.InsertCompactedLeaf(newLeaf); err != nil {
return nil, err
}
newLeft = newLeaf
} else {
// Collision – keys differ: call merge to combine the new entry with the existing compacted leaf.
newLeft, err = t.merge(tx, height+1, entry.Key, entry.Leaf, cl.Key(), cl.LeafNode)
if err != nil {
return nil, err
}
}
} else {
// Multiple batch entries – check if they all match the existing key.
allMatch := true
for _, entry := range leftEntries {
if entry.Key != cl.Key() {
allMatch = false
break
}
}
if allMatch {
// All entries match; take the last one as replacement.
lastEntry := leftEntries[len(leftEntries)-1]
newLeaf := NewCompactedLeafNode(height+1, &lastEntry.Key, lastEntry.Leaf)
if err := tx.DeleteCompactedLeaf(cl.NodeHash()); err != nil {
return nil, err
}
if err := tx.InsertCompactedLeaf(newLeaf); err != nil {
return nil, err
}
newLeft = newLeaf
} else {
// At least one entry has a different key – merge using the first differing entry.
var mergeEntry *BatchedInsertionEntry
for _, entry := range leftEntries {
if entry.Key != cl.Key() {
mergeEntry = &entry

Check failure on line 99 in mssmt/compacted_tree.go

View workflow job for this annotation

GitHub Actions / Lint check

G601: Implicit memory aliasing in for loop. (gosec)
break
}
}
if mergeEntry == nil {
return nil, fmt.Errorf("unexpected nil merge entry")
}
newLeft, err = t.merge(tx, height+1, mergeEntry.Key, mergeEntry.Leaf, cl.Key(), cl.LeafNode)
if err != nil {
return nil, err
}
}
}
} else {
// leftChild is not a compacted leaf, so it must be a branch; recurse normally.
baseLeft := leftChild.(*BranchNode)
newLeft, err = t.batched_insert(tx, leftEntries, height+1, baseLeft)
if err != nil {
return nil, err
}
}
} else {
// The left child is empty.
if len(leftEntries) == 1 {
entry := leftEntries[0]
newLeft = NewCompactedLeafNode(height+1, &entry.Key, entry.Leaf)
if err := tx.InsertCompactedLeaf(newLeft.(*CompactedLeafNode)); err != nil {
return nil, err
}
} else {
baseLeft := EmptyTree[height+1].(*BranchNode)
newLeft, err = t.batched_insert(tx, leftEntries, height+1, baseLeft)
if err != nil {
return nil, err
}
}
}
// Use newLeft as the computed left child.
leftChild = newLeft
}

// Process right subtree:
var newRight Node
if len(rightEntries) > 0 {
// Check if the current right child is not empty.
if rightChild != EmptyTree[height+1] {
// If the existing child is a compacted leaf, we must handle potential collisions.
if cr, ok := rightChild.(*CompactedLeafNode); ok {
if len(rightEntries) == 1 {
entry := rightEntries[0]
if entry.Key == cr.Key() {
// Replacement: update the compacted leaf.
newLeaf := NewCompactedLeafNode(height+1, &entry.Key, entry.Leaf)
if err := tx.DeleteCompactedLeaf(cr.NodeHash()); err != nil {
return nil, err
}
if err := tx.InsertCompactedLeaf(newLeaf); err != nil {
return nil, err
}
newRight = newLeaf
} else {
// Collision – keys differ: call merge to combine the new entry with the existing compacted leaf.
newRight, err = t.merge(tx, height+1, entry.Key, entry.Leaf, cr.Key(), cr.LeafNode)
if err != nil {
return nil, err
}
}
} else {
// Multiple batch entries – check if they all match the existing key.
allMatch := true
for _, entry := range rightEntries {
if entry.Key != cr.Key() {
allMatch = false
break
}
}
if allMatch {
// All entries match; take the last one as replacement.
lastEntry := rightEntries[len(rightEntries)-1]
newLeaf := NewCompactedLeafNode(height+1, &lastEntry.Key, lastEntry.Leaf)
if err := tx.DeleteCompactedLeaf(cr.NodeHash()); err != nil {
return nil, err
}
if err := tx.InsertCompactedLeaf(newLeaf); err != nil {
return nil, err
}
newRight = newLeaf
} else {
// At least one entry has a different key – merge using the first differing entry.
var mergeEntry *BatchedInsertionEntry
for _, entry := range rightEntries {
if entry.Key != cr.Key() {
mergeEntry = &entry

Check failure on line 191 in mssmt/compacted_tree.go

View workflow job for this annotation

GitHub Actions / Lint check

G601: Implicit memory aliasing in for loop. (gosec)
break
}
}
if mergeEntry == nil {
return nil, fmt.Errorf("unexpected nil merge entry")
}
newRight, err = t.merge(tx, height+1, mergeEntry.Key, mergeEntry.Leaf, cr.Key(), cr.LeafNode)
if err != nil {
return nil, err
}
}
}
} else {
// rightChild is not a compacted leaf, so it must be a branch; recurse normally.
baseRight := rightChild.(*BranchNode)
newRight, err = t.batched_insert(tx, rightEntries, height+1, baseRight)
if err != nil {
return nil, err
}
}
} else {
// The right child is empty.
if len(rightEntries) == 1 {
entry := rightEntries[0]
newRight = NewCompactedLeafNode(height+1, &entry.Key, entry.Leaf)
if err := tx.InsertCompactedLeaf(newRight.(*CompactedLeafNode)); err != nil {
return nil, err
}
} else {
baseRight := EmptyTree[height+1].(*BranchNode)
newRight, err = t.batched_insert(tx, rightEntries, height+1, baseRight)
if err != nil {
return nil, err
}
}
}
// Use newRight as the computed right child.
rightChild = newRight
}

// Create the updated branch from the new left and right children.
var updatedBranch *BranchNode

Check failure on line 233 in mssmt/compacted_tree.go

View workflow job for this annotation

GitHub Actions / Lint check

S1021: should merge variable declaration with assignment on next line (gosimple)
updatedBranch = NewBranch(leftChild, rightChild)

// Delete the old branch and insert the new one.
if root != EmptyTree[height] {
if err := tx.DeleteBranch(root.NodeHash()); err != nil {
return nil, err
}
}
if !IsEqualNode(updatedBranch, EmptyTree[height]) {
if err := tx.InsertBranch(updatedBranch); err != nil {
return nil, err
}
}

return updatedBranch, nil
}

// BatchedInsert inserts multiple leaf nodes at the given keys within the MS-SMT.
func (t *CompactedTree) BatchedInsert(ctx context.Context, entries []BatchedInsertionEntry) (Tree, error) {
sort.Slice(entries, func(i, j int) bool {
return bytes.Compare(entries[i].Key[:], entries[j].Key[:]) < 0
})

err := t.store.Update(ctx, func(tx TreeStoreUpdateTx) error {
currentRoot, err := tx.RootNode()
if err != nil {
return err
}
branchRoot := currentRoot.(*BranchNode)

// (Optional) Loop over entries and check for sum overflow.
for _, entry := range entries {
if err := CheckSumOverflowUint64(branchRoot.NodeSum(), entry.Leaf.NodeSum()); err != nil {
return fmt.Errorf("batched insert key %v sum overflow: %w", entry.Key, err)
}
}

// Call the new batched_insert method.
newRoot, err := t.batched_insert(tx, entries, 0, branchRoot)
if err != nil {
return err
}
return tx.UpdateRoot(newRoot)
})
if err != nil {
return nil, err
}
return t, nil
}

// BatchedInsertionEntry represents one leaf insertion.
type BatchedInsertionEntry struct {
Key [hashSize]byte
Leaf *LeafNode
}

var _ Tree = (*CompactedTree)(nil)

// NewCompactedTree initializes an empty MS-SMT backed by `store`.
Expand Down Expand Up @@ -68,19 +340,9 @@

switch node := next.(type) {
case *CompactedLeafNode:
// Our next node is a compacted leaf. We just need to
// expand it so we can continue our walk down the tree.
next = node.Extract(i)

// Sibling might be a compacted leaf too, in which case
// we need to extract it as well.
if compSibling, ok := sibling.(*CompactedLeafNode); ok {
sibling = compSibling.Extract(i)
}
// Our next node is a compacted leaf. We simply return the underlying leaf.
return node.LeafNode, nil
Roasbeef marked this conversation as resolved.
Show resolved Hide resolved

// Now that all required branches are reconstructed we
// can continue the search for the leaf matching the
// passed key.
for j := i; j <= lastBitIndex; j++ {
if iter != nil {
err := iter(j, next, sibling, current)
Expand All @@ -91,9 +353,6 @@
current = next

if j < lastBitIndex {
// Since we have all the branches we
// need extracted already we can just
// continue walking down.
branch := current.(*BranchNode)
next, sibling = stepOrder(
j+1, key, branch.Left,
Expand Down
51 changes: 33 additions & 18 deletions mssmt/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ type CompactedLeafNode struct {
// compactedNodeHash holds the topmost (omitted) node's node hash in the
// subtree.
compactedNodeHash NodeHash

// Height is the level at which this compacted leaf was created.
Height int
}

// NewCompactedLeafNode creates a new compacted leaf at the passed height with
Expand All @@ -144,6 +147,7 @@ func NewCompactedLeafNode(height int, key *[32]byte,
compactedNodeHash: nodeHash,
}

node.Height = height
return node
}

Expand All @@ -157,24 +161,35 @@ func (c *CompactedLeafNode) Key() [32]byte {
return c.key
}

// Extract extracts the subtree represented by this compacted leaf and returns
// the topmost node in the tree.
func (c *CompactedLeafNode) Extract(height int) Node {
var current Node = c.LeafNode

// Walk up and recreate the missing branches.
for j := MaxTreeLevels; j > height+1; j-- {
var left, right Node
if bitIndex(uint8(j-1), &c.key) == 0 {
left, right = current, EmptyTree[j]
} else {
left, right = EmptyTree[j], current
}

current = NewBranch(left, right)
}

return current
func (c *CompactedLeafNode) Extract(requestedHeight int) Node {
target := requestedHeight
if target >= c.Height {
target = c.Height - 1
}

var current Node = c.LeafNode

for j := c.Height - 1; j >= target+1; j-- {
if bitIndex(uint8(j), &c.key) == 0 {
current = NewBranch(current, EmptyTree[j+1])
} else {
current = NewBranch(EmptyTree[j+1], current)
}
}

sibling := EmptyTree[target+1]
if target+1 == MaxTreeLevels {
sibling = NewBranch(sibling, sibling)
}

var result Node
if bitIndex(uint8(target), &c.key) == 0 {
result = NewBranch(current, sibling)
} else {
result = NewBranch(sibling, current)
}

return result
}

// Copy returns a deep copy of the compacted leaf node.
Expand Down
Loading
Loading