Now pruning tree after deletions

This commit is contained in:
Daniel Lamberger 2024-01-18 16:51:30 +02:00
parent c5cdeac8db
commit e9b7d794cc
5 changed files with 128 additions and 51 deletions

@ -1 +1 @@
Subproject commit f9834381b33d1b6d2b0b34bc7d05ddfb767f8b34
Subproject commit 72906be185fbb62c5fe746a5354ff54ea0f56cbd

View File

@ -207,7 +207,9 @@ proc updateAllCommitments*(tree: BranchesNode) =
for node in nodes:
for index, commitment in node.commitmentsSnapshot:
points.add(commitment)
points.add(node.branches[index].commitment)
if node.branches[index] != nil:
points.add(node.branches[index].commitment)
else: points.add(IdentityPoint)
childIndexes.add(index)
var frs = newSeq[Field](points.len)

View File

@ -13,7 +13,7 @@ import
./tree,
./commitment
when TraceLogs: import std/strformat
when TraceLogs: import std/[strformat, strutils]
proc newValuesNode*(key, value: Bytes32) : ValuesNode =
@ -40,13 +40,6 @@ proc setValue(node: ValuesNode, index: byte, value: Bytes32) =
node.values[index] = heapValue
proc deleteValue(node: ValuesNode, index: byte) =
## Deletes the value at the given `index`, if any
node.updateCommitment(index, nil)
node.values[index] = nil
# TODO: prevent setting a value from a non-root node
proc setValue*(node: BranchesNode, key: Bytes32, value: Bytes32) =
## Stores the given `value` in the tree at the given `key`
var current = node
@ -102,37 +95,85 @@ proc setValue*(node: BranchesNode, key: Bytes32, value: Bytes32) =
proc deleteValue(node: BranchesNode, key: Bytes32, depth: int = 0):
tuple[found: bool, empty: bool, values: ValuesNode] =
## Deletes the value associated with the given `key` from the tree, and prunes
## the tree as needed
#[
Algorithm:
- We walk down the tree and try to find the ValuesNodes that contains the
value.
- If we can't find it, or can't find the value within it, we return found=false
- If we find it and the ValuesNodes contains more than one value, we set
the target value to nil and update the ValuesNodes commitment.
We return found=true, empty=false
- If the ValuesNode contains just the target value, we simply detach it
from its parent and discard it. We don't bother with removing the value
or updating its commitment.
- After detaching it, if the parent is left empty, we signal its own parent
that it should be detached too, by returning empty=true
- If the parent is left with one other ValuesNode (and no other BranchesNode),
we signal its own parent that it should be detached, but that the other
ValuesNode should be attached in its stead. We return it in the `values`
tuple field.
- When an ancestor obtains that ValuesNode, it will attach it in case it
has at least one other brach (be it a ValuesNode or BranchesNode).
Otherwise, it will notify its own parent it should be disconnected as
well and pass the ValuesNode along.
- Meaning, the ValuesNode (sibling to the ValuesNode from which the value
was removed) starts travelling up the tree till it lands in a BranchesNode
that contains one other branch, or reaches the root.
- In any case of the tree being modified, we snapshot the commitments of
nodes whose children were modified, so they can be bulk-updated later on.
Leaves node commitments are updated on the spot though.
]#
var child = node.branches[key[depth]]
when TraceLogs: echo " ".repeat(depth) & &"At branch {cast[uint64](node)}, depth {depth}, child index {key[depth].toHex}"
if child of ValuesNode:
var vn = child.ValuesNode
var target = vn.values[key[^1]]
when TraceLogs: echo " ".repeat(depth+1) & &"At ValuesNode {cast[uint64](vn)}, depth {depth+1}"
if target == nil:
when TraceLogs: echo " ".repeat(depth+1) & &"Value not found at index {key[^1].toHex}"
return (found: false, empty: false, values: nil)
node.snapshotChildCommitment(key[depth])
var hasOtherValues = vn.values.any(v => v != nil and v != target)
if hasOtherValues:
when TraceLogs: echo " ".repeat(depth+1) & &"ValuesNode has multiple values; removing value at index {key[^1].toHex}"
vn.updateCommitment(key[^1], nil)
vn.values[key[^1]] = nil
return (found: true, empty: false, values: nil)
when TraceLogs: echo " ".repeat(depth+1) & &"ValuesNode contains only the target value at index {key[^1].toHex}; detaching from tree"
node.branches[key[depth]] = nil
elif child of BranchesNode:
var bn = child.BranchesNode
var (found, empty, values) = deleteValue(bn, key, depth + 1)
if not found:
return (found, empty, values)
node.snapshotChildCommitment(key[depth])
if not empty:
return (found, empty, values)
if values == nil:
when TraceLogs: echo " ".repeat(depth) & &"At branch {cast[uint64](node)}, depth {depth}. Detached child from tree."
node.branches[key[depth]] = nil
else:
when TraceLogs: echo " ".repeat(depth) & &"At branch {cast[uint64](node)}, depth {depth}. Replaced child with inner ValuesNode."
node.branches[key[depth]] = values # propagate ValuesNode up the tree
if node.branches.all(b => b == nil):
return (found: true, empty: true, values: nil)
elif node.branches.any(b => b of BranchesNode) or
node.branches.foldl(if b of ValuesNode: a+1 else: a, 0) >= 2:
return (found: true, empty: false, values: nil)
else:
let vn = node.branches.filter(b => b != nil and b != child)[0].ValuesNode
return (found: true, empty: true, values: vn)
proc deleteValue*(node: BranchesNode, key: Bytes32): bool =
## Deletes the value associated with the given `key` from the tree.
var current = node
var depth = 0
when TraceLogs: echo &"Deleting value for key {key.toHex}"
# Walk down the tree until the branch closest to the key
while current.branches[key[depth]] of BranchesNode:
when TraceLogs: echo &"At node {cast[uint64](current)}. Going down to branch '{key[depth].toHex}' at depth {depth}"
current.snapshotChildCommitment(key[depth])
current = current.branches[key[depth]].BranchesNode
inc(depth)
# If we reached a ValuesNode...
var vn = current.branches[key[depth]].ValuesNode
if vn != nil:
when TraceLogs: echo &"At node {cast[uint64](current)}. Found ValuesNode at branch '{key[depth].toHex}', depth {depth}, addr {cast[uint64](vn)}"
when TraceLogs: echo &" Stem: {vn.stem.toHex}"
# If the stem differs from the key, we can't use that ValuesNode.
# This means the value doesn't exist for the given key, so we return false.
var divergence = vn.stem.zip(key).firstMatchAt(tup => tup[0] != tup[1])
if divergence.found:
return false
# If the stem matches the key, we found the ValuesNode for the key.
# We remove it by setting the branch to nil.
current.snapshotChildCommitment(key[depth])
vn.deleteValue(key[^1])
return true
# If no ValuesNode was found for the key, it means the value doesn't exist.
return false
return deleteValue(node, key, 0).found

View File

@ -54,6 +54,11 @@ func hexToBits*(c: char): byte =
else: raise newException(ValueError, "Character must be hexadecimal (a-f | A-F | 0-9)")
func toHex*(b: byte): string =
result.add bitsToHex(b shr 4)
result.add bitsToHex(b and 0x0f)
proc writeAsHex*(stream: Stream, b: byte) =
## Writes a byte to the stream as two hex characters
stream.write(bitsToHex(b shr 4))

View File

@ -8,9 +8,9 @@
## The main module. Provides some tests.
import
std/[random, streams, os],
std/[random, streams, os, strformat],
unittest2,
../eth_verkle/[utils, math],
../eth_verkle/[config, utils, math],
../eth_verkle/tree/[tree, operations, commitment]
suite "main":
@ -45,11 +45,26 @@ suite "main":
let deleteKvps = @[
"1100000000000000000000000000000000000000000000000000000000010000",
"2211000000000000000000000000000000000000000000000000000000000000",
"5500000000000000000000000000000000000000000000000000000000001100"
"3300000000000000000000000000000000000000000000000000000000000001",
"5500000000000000000000000000000000000000000000000000000000000000",
"5500000000000000000000000000000000000000000000000000000000001100",
]
const expectedRootCommitment3 = "1b0b20e55d30cbd3538f98a194d955aa74b77342196046e70d68f458a7f6d084"
## Matches go-verkle commitment
const expectedRootCommitment3 = "4145d957eb624cb56af3861ebe0db2e9fee2de523b19aad55acf9085eb7cd158"
const finalKvps = @[
("0000000000000000000000000000000000000000000000000000000000000000", "0000000000000000000000000000000000000000000000000000000000000011"),
("000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f", "0000000000000000000000000000000000000000000000000000000000000002"),
("1100000000000000000000000000000000000000000000000000000000000000", "0000000000000000000000000000000000000000000000000000000000000003"),
("2200000000000000000000000000000000000000000000000000000000000000", "0000000000000000000000000000000000000000000000000000000000000004"),
("2211000000000000000000000000000000000000000000000000000000000000", "0000000000000000000000000000000000000000000000000000000000000005"),
("3300000000000000000000000000000000000000000000000000000000000000", "0000000000000000000000000000000000000000000000000000000000000006"),
("33000000000000000000000000000000000000000000000000000000000000ff", "0000000000000000000000000000000000000000000000000000000000000008"),
("4400000000000000000000000000000000000000000000000000000000000000", "0000000000000000000000000000000000000000000000000000000000000009"),
("4400000011000000000000000000000000000000000000000000000000000000", "000000000000000000000000000000000000000000000000000000000000000a"),
("4400000011000000000000000000000000000000000000000000000000000001", "0000000000000000000000000000000000000000000000000000000000000013"),
]
iterator hexKvpsToBytes32(kvps: openArray[tuple[key: string, value: string]]):
@ -61,6 +76,7 @@ suite "main":
test "sanity":
# Populate tree and check root commitment
when TraceLogs: echo "\n\n\nPopulating tree\n"
var tree = newBranchesNode()
for (key, value) in sampleKvps.hexKvpsToBytes32():
tree.setValue(key, value)
@ -69,6 +85,7 @@ suite "main":
check tree.serializeCommitment.toHex == expectedRootCommitment1
# Update some nodes in the tree and check updated root commitment
when TraceLogs: echo "\n\n\nAdding and modfying some key/values in the tree\n"
for (key, value) in updateKvps.hexKvpsToBytes32():
tree.setValue(key, value)
tree.updateAllCommitments()
@ -76,13 +93,25 @@ suite "main":
check tree.serializeCommitment.toHex == expectedRootCommitment2
# Delete some nodes in the tree and check updated root commitment
when TraceLogs: echo "\n\n\nDeleting some key/values in the tree\n"
for hexKey in deleteKvps:
when TraceLogs: echo &"Deleting key {hexKey}"
let key = hexToBytesArray[32](hexKey)
check tree.deleteValue(key) == true
tree.updateAllCommitments()
#tree.printTree(newFileStream(stdout))
#check tree.serializeCommitment.toHex == expectedRootCommitment3
# Note: currently fails since we don't deep-delete values like go-verkle does
check tree.serializeCommitment.toHex == expectedRootCommitment3
# Populate a new tree with just the values remaining in the step above;
# we expect the same commitment
when TraceLogs: echo "\n\n\nCreating new tree with final key/values from steps above, structure and commitments should match previous step\n"
var tree2 = newBranchesNode()
for (key, value) in finalKvps.hexKvpsToBytes32():
when TraceLogs: echo &"Adding {key.toHex} = {value.toHex}"
tree2.setValue(key, value)
tree2.updateAllCommitments()
#tree2.printTree(newFileStream(stdout))
check tree2.serializeCommitment.toHex == expectedRootCommitment3
# test "testDelNonExistingValues":