added support for updating multiple leaves under the same stem with faster commitment update

This commit is contained in:
Daniel Lamberger 2024-03-28 21:54:45 +02:00
parent ff7b268ae6
commit 7da6604946
3 changed files with 176 additions and 34 deletions

View File

@ -175,6 +175,55 @@ proc updateCommitment*(vn: ValuesNode, index: byte, newValue: ref Bytes32) =
proc updateMultipleValues*(vn: ValuesNode, newValues: array[256, ref Bytes32]) =
when DisableCommitments:
return
var oldC1, oldC2: ref Point
# We iterate the values, and we update the C1 and/or C2 commitments depending on the index.
# If any of them is touched, we save the original point so we can update the LeafNode root
# commitment. We copy the original point in oldC1 and oldC2, so we can batch their Fr transformation
# after this loop.
for i, v in newValues.pairs:
if v != nil and (vn.values[i] == nil or v[] != vn.values[i][]):
if i < 256 div 2:
# First time we touch C1? Save the original point for later.
if oldC1 == nil:
new oldC1
oldC1[] = vn.c1
# We update C1 directly in `vn`. We have our original copy in oldC1.
vn.updateCn(i.byte, v, vn.c1)
else:
# First time we touch C2? Save the original point for later.
if oldC2 == nil:
new oldC2
oldC2[] = vn.c2
# We update C2 directly in `vn`. We have our original copy in oldC2.
vn.updateCn(i.byte, v, vn.c2)
vn.values[i] = v
# We have three potential cases here:
# 1. We have touched C1 and C2: we Fr-batch old1, old2 and newC1, newC2. (4x gain ratio)
# 2. We have touched only one CX: we Fr-batch oldX and newCX. (2x gain ratio)
# 3. No C1 or C2 was touched, this is a noop.
var frs: array[4, Field]
const c1Idx = 2 # [1, stem, ->C1<-, C2]
const c2Idx = 3 # [1, stem, C1, ->C2<-]
if oldC1 != nil and oldC2 != nil: # Case 1.
banderwagonMultiMapToScalarField([addr frs[0], addr frs[1], addr frs[2], addr frs[3]], [vn.c1, oldC1[], vn.c2, oldC2[]])
vn.updateC(c1Idx, frs[0], frs[1])
vn.updateC(c2Idx, frs[2], frs[3])
elif oldC1 != nil: # Case 2. (C1 touched)
banderwagonMultiMapToScalarField([addr frs[0], addr frs[1]], [vn.c1, oldC1[]])
vn.updateC(c1Idx, frs[0], frs[1])
elif oldC2 != nil: # Case 2. (C2 touched)
banderwagonMultiMapToScalarField([addr frs[0], addr frs[1]], [vn.c2, oldC2[]])
vn.updateC(c2Idx, frs[0], frs[1])
proc snapshotChildCommitment*(node: BranchesNode, childIndex: byte) =
## Stores the current commitment of the child node denoted by `childIndex`
## into the `node`'s `commitmentsSnapshot` table, and allocates the table if

View File

@ -17,8 +17,7 @@ when TraceLogs: import std/[strformat, strutils]
proc newValuesNode*(key, value: Bytes32, depth: uint8) : ValuesNode =
## Allocates a new `ValuesNode` with a single key and value and computes its
## commitment
## Allocates a new `ValuesNode` with a single value and computes its commitment
var heapValue = new Bytes32
heapValue[] = value
result = new ValuesNode
@ -28,6 +27,15 @@ proc newValuesNode*(key, value: Bytes32, depth: uint8) : ValuesNode =
result.initializeCommitment()
proc newValuesNode*(stem: array[31, byte], values: array[256, ref Bytes32], depth: uint8) : ValuesNode =
## Allocates a new `ValuesNode` with the provided values and computes its commitment
result = new ValuesNode
result.depth = depth
result.stem = stem
result.values = values
result.initializeCommitment()
proc newBranchesNode*(depth: uint8) : BranchesNode =
## Allocates a new `BranchesNode` with the given depth
result = new BranchesNode
@ -41,60 +49,75 @@ proc newTree*() : BranchesNode =
proc setValue(node: ValuesNode, index: byte, value: Bytes32) =
## Heap-allocates the given `value` and stores it at the given `index`
## Heap-allocates the given `value` and stores it at the given `index` and
## updates the commitment
var heapValue = new Bytes32
heapValue[] = value
node.updateCommitment(index, heapValue)
node.values[index] = heapValue
proc setValue*(node: BranchesNode, key: Bytes32, value: Bytes32) =
assert node.depth == 0 # Must always be done from the tree root
## Stores the given `value` in the tree at the given `key`
proc getOrCreateValuesNodeParentBranch(node: BranchesNode, stem: seq[byte]): BranchesNode =
## Finds an existing ValuesNode's parent branch, or creates all necessary
## branches leading to it in case the ValuesNode doesn't exist yet, possibly
## pushing down the tree an existing ValuesNode with a partially-matching stem.
var current = node
when TraceLogs: echo &"Setting {key.toHex} --> {value.toHex}"
# Walk down the tree till the branch closest to the key
while current.branches[key[current.depth]] of BranchesNode:
when TraceLogs: echo &"At node {cast[uint64](current)}. Going down to branch '{key[current.depth].toHex}' at depth {current.depth}"
current.snapshotChildCommitment(key[current.depth])
current = current.branches[key[current.depth]].BranchesNode
# Walk down the tree till the branch closest to the stem
while current.branches[stem[current.depth]] of BranchesNode:
when TraceLogs: echo &"At node {cast[uint64](current)}. Going down to branch '{stem[current.depth].toHex}' at depth {current.depth}"
current.snapshotChildCommitment(stem[current.depth])
current = current.branches[stem[current.depth]].BranchesNode
# If we reached a ValuesNode...
var vn = current.branches[key[current.depth]].ValuesNode
var vn = current.branches[stem[current.depth]].ValuesNode
if vn != nil:
when TraceLogs: echo &"At node {cast[uint64](current)}. Found ValuesNode at branch '{key[current.depth].toHex}', depth {current.depth}, addr {cast[uint64](vn)}"
when TraceLogs: echo &"At node {cast[uint64](current)}. Found ValuesNode at branch '{stem[current.depth].toHex}', depth {current.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. We need to
# insert intermediate branches till the point they diverge, pushing down the
# current ValuesNode, and then proceed to create a new ValuesNode
# Todo: zip makes a memory allocation. avoid.
var divergence = vn.stem.zip(key).firstMatchAt(tup => tup[0] != tup[1])
if divergence.found:
when TraceLogs: echo &" Key: {key.toHex}"
when TraceLogs: echo &" Found difference at depth {divergence.index}; inserting intermediate branches"
while current.depth < divergence.index:
# If our stem differs from the ValuesNode's stem, we can't use that
# ValuesNode. We need to insert intermediate branches till the point they
# diverge, pushing down the current ValuesNode, and then proceed to create
# a new ValuesNode
var divergeDepth = vn.depth
while divergeDepth < 31 and vn.stem[divergeDepth] == stem[divergeDepth]:
inc divergeDepth
if divergeDepth < 31:
when TraceLogs: echo &" Stem: {stem.toHex}"
when TraceLogs: echo &" Found difference at depth {divergeDepth}; inserting intermediate branches"
while current.depth < divergeDepth:
let newBranch = newBranchesNode(current.depth + 1)
current.snapshotChildCommitment(key[current.depth])
current.branches[key[current.depth]] = newBranch
when TraceLogs: echo &"At node {cast[uint64](current)}. Assigned new branch at '{key[current.depth].toHex}', depth {current.depth}, addr {cast[uint64](newBranch)}"
current.snapshotChildCommitment(stem[current.depth])
current.branches[stem[current.depth]] = newBranch
when TraceLogs: echo &"At node {cast[uint64](current)}. Assigned new branch at '{stem[current.depth].toHex}', depth {current.depth}, addr {cast[uint64](newBranch)}"
current = newBranch
current.snapshotChildCommitment(vn.stem[current.depth])
current.branches[vn.stem[current.depth]] = vn
vn.depth = current.depth + 1
when TraceLogs: echo &"At node {cast[uint64](current)}. Assigned ValuesNode at '{vn.stem[current.depth].toHex}', depth {current.depth}, addr {cast[uint64](vn)}"
vn = nil # We can't use it
current.snapshotChildCommitment(key[current.depth])
return current
# The current branch does not contain a ValuesNode at the required offset;
proc setValue*(node: BranchesNode, key: Bytes32, value: Bytes32) =
## Stores the given `value` in the tree at the given `key`
assert node.depth == 0 # Must always be done from the tree root
when TraceLogs: echo &"Setting {key.toHex} --> {value.toHex}"
var parent = getOrCreateValuesNodeParentBranch(node, key[0..<31])
parent.snapshotChildCommitment(key[parent.depth])
var vn = parent.branches[key[parent.depth]].ValuesNode
# The parent branch does not contain a ValuesNode at the required offset;
# create one and store the value in it, as per the key's last byte offset
if vn == nil:
vn = newValuesNode(key, value, current.depth + 1)
current.branches[key[current.depth]] = vn
when TraceLogs: echo &"Created ValuesNode at depth {current.depth}, branch '{key[current.depth].toHex}', stem {vn.stem.toHex}, with value at slot '{key[^1].toHex}'"
vn = newValuesNode(key, value, parent.depth + 1)
parent.branches[key[parent.depth]] = vn
when TraceLogs: echo &"Created ValuesNode at depth {parent.depth}, branch '{key[parent.depth].toHex}', stem {vn.stem.toHex}, with value at slot '{key[^1].toHex}'"
# Store the value in the existing ValuesNode, as per the key's last byte offset
else:
@ -103,6 +126,33 @@ proc setValue*(node: BranchesNode, key: Bytes32, value: Bytes32) =
proc setMultipleValues*(node: BranchesNode, stem: array[31, byte], values: array[256, ref Bytes32]) =
## Stores multiple values in the tree underneath the given `stem`. Null values
## are ignored. The commitment is updated in bulk; prefer calling this rather
## than multiple calls to `setValue`
assert node.depth == 0 # Must always be done from the tree root
when TraceLogs: echo &"Setting multiple values at {stem.toHex}"
var parent = getOrCreateValuesNodeParentBranch(node, @stem)
parent.snapshotChildCommitment(stem[parent.depth])
var vn = parent.branches[stem[parent.depth]].ValuesNode
# The parent branch does not contain a ValuesNode at the required offset;
# create one and store the values in it, and initialize its commitment using
# these values
if vn == nil:
vn = newValuesNode(stem, values, parent.depth + 1)
parent.branches[stem[parent.depth]] = vn
when TraceLogs: echo &"Created ValuesNode at depth {parent.depth}, branch '{stem[parent.depth].toHex}', stem {vn.stem.toHex}, with multiple values"
# Otherwise, update the existing ValuesNode
else:
vn.updateMultipleValues(values)
when TraceLogs: echo &"Updated ValuesNode at depth {parent.depth}, branch '{stem[parent.depth].toHex}', stem {vn.stem.toHex}, with multiple values"
proc getValue*(node: BranchesNode, key: Bytes32): ref Bytes32 =
## Retrieves a value given a key. Returns nil if not found.
var current = node
@ -194,7 +244,7 @@ proc deleteValueRecursive(node: BranchesNode, key: Bytes32):
when TraceLogs: echo " ".repeat(node.depth) & &"At branch {cast[uint64](node)}, depth {node.depth}. Detached child from tree."
node.branches[key[node.depth]] = nil
else:
when TraceLogs: echo " ".repeat(depth) & &"At branch {cast[uint64](node)}, depth {depth}. Replaced child with inner ValuesNode."
when TraceLogs: echo " ".repeat(node.depth) & &"At branch {cast[uint64](node)}, depth {node.depth}. Replaced child with inner ValuesNode."
values.depth = node.depth + 1
node.branches[key[node.depth]] = values # propagate ValuesNode up the tree

View File

@ -114,6 +114,49 @@ suite "main":
check tree2.serializeCommitment.toHex == expectedRootCommitment3
test "multipleUpdate":
for modulo in @[1, 10]: # full, sparse
let updatedOneByOneStart = cpuTime()
var updatedOneByOne = newTree()
for i in 0..<256:
if i mod modulo == 0:
var key, value: Bytes32
key[^1] = i.byte
value[^1] = i.byte
updatedOneByOne.setValue(key, value)
updatedOneByOne.updateAllCommitments()
let updatedOneByOneEnd = cpuTime()
when TraceLogs: echo &"updatedOneByOne root commitment: {updatedOneByOne.serializeCommitment.toHex}. Took: {updatedOneByOneEnd - updatedOneByOneStart:.3f} secs"
let multiCreateStart = cpuTime()
var multiCreate = newTree()
var values: array[256, ref Bytes32]
for i in 0..<256:
if i mod modulo == 0:
var value: ref Bytes32
new value
value[^1] = i.byte
values[i] = value
var stem: array[31, byte]
multiCreate.setMultipleValues(stem, values)
multiCreate.updateAllCommitments()
let multiCreateEnd = cpuTime()
when TraceLogs: echo &"multiCreate root commitment: {multiCreate.serializeCommitment.toHex}. Took: {multiCreateEnd - multiCreateStart:.3f} secs"
check multiCreate.serializeCommitment.toHex == updatedOneByOne.serializeCommitment.toHex
let multiUpdateStart = cpuTime()
var multiUpdate = newTree()
var key, value: Bytes32
multiUpdate.setValue(key, value)
multiUpdate.setMultipleValues(stem, values)
multiUpdate.updateAllCommitments()
let multiUpdateEnd = cpuTime()
when TraceLogs: echo &"multiUpdate root commitment: {multiUpdate.serializeCommitment.toHex}. Took: {multiUpdateEnd - multiUpdateStart:.3f} secs"
check multiUpdate.serializeCommitment.toHex == updatedOneByOne.serializeCommitment.toHex
test "fetchKeys":
var tree = newTree()
for (key, value) in sampleKvps.hexKvpsToBytes32():