Skip to content

Commit 81cd86d

Browse files
Implement trie.put (#3473)
* first pass at put implementation [no ci] * Fix logging [no ci] * Revert optimization * Comment and test cleanup [no ci] * fix child index selection [no ci] * Finish tests * Remove .only * Add helper for `put` * Fix node copy step * lint * verkle: add and implement typeguards * verkle: add typeguard to tests * Address feedback --------- Co-authored-by: Gabriel Rocheleau <[email protected]>
1 parent b49ff15 commit 81cd86d

File tree

6 files changed

+235
-77
lines changed

6 files changed

+235
-77
lines changed

packages/verkle/src/node/util.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,10 @@ export const createCValues = (values: Uint8Array[], deletedValues = new Array(12
5757
}
5858
return expandedValues
5959
}
60+
export function isLeafNode(node: VerkleNode): node is LeafNode {
61+
return node.type === VerkleNodeType.Leaf
62+
}
63+
64+
export function isInternalNode(node: VerkleNode): node is InternalNode {
65+
return node.type === VerkleNodeType.Internal
66+
}

packages/verkle/src/util/bytes.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,13 @@ export function matchingBytesLength(bytes1: Uint8Array, bytes2: Uint8Array): num
1010
let count = 0
1111
const minLength = Math.min(bytes1.length, bytes2.length)
1212

13-
// Unroll the loop for better performance
14-
for (let i = 0; i < minLength - 3; i += 4) {
15-
// Compare 4 bytes at a time
16-
if (
17-
bytes1[i] === bytes2[i] &&
18-
bytes1[i + 1] === bytes2[i + 1] &&
19-
bytes1[i + 2] === bytes2[i + 2] &&
20-
bytes1[i + 3] === bytes2[i + 3]
21-
) {
22-
count += 4
23-
} else {
24-
// Break early if a mismatch is found
25-
break
26-
}
27-
}
28-
29-
// Handle any remaining elements
30-
for (let i = minLength - (minLength % 4); i < minLength; i++) {
13+
for (let i = 0; i < minLength; i++) {
3114
if (bytes1[i] === bytes2[i]) {
3215
count++
3316
} else {
17+
// Break early if a mismatch is found
3418
break
3519
}
3620
}
37-
3821
return count
3922
}

packages/verkle/src/verkleTree.ts

Lines changed: 175 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ValueEncoding,
66
bytesToHex,
77
equalsBytes,
8+
intToHex,
89
zeros,
910
} from '@ethereumjs/util'
1011
import debug from 'debug'
@@ -14,7 +15,7 @@ import { CheckpointDB } from './db/checkpoint.js'
1415
import { InternalNode } from './node/internalNode.js'
1516
import { LeafNode } from './node/leafNode.js'
1617
import { type VerkleNode } from './node/types.js'
17-
import { decodeNode } from './node/util.js'
18+
import { decodeNode, isLeafNode } from './node/util.js'
1819
import {
1920
type Proof,
2021
ROOT_DB_KEY,
@@ -133,7 +134,9 @@ export class VerkleTree {
133134
}
134135
}
135136

136-
return new VerkleTree(opts)
137+
const trie = new VerkleTree(opts)
138+
await trie._createRootNode()
139+
return trie
137140
}
138141

139142
database(db?: DB<Uint8Array, Uint8Array>) {
@@ -194,7 +197,6 @@ export class VerkleTree {
194197
const suffix = key[key.length - 1]
195198
this.DEBUG && this.debug(`Stem: ${bytesToHex(stem)}; Suffix: ${suffix}`, ['GET'])
196199
const res = await this.findPath(stem)
197-
198200
if (res.node instanceof LeafNode) {
199201
// The retrieved leaf node contains an array of 256 possible values.
200202
// The index of the value we want is at the key's last byte
@@ -213,11 +215,169 @@ export class VerkleTree {
213215
* @param value - the value to store
214216
* @returns A Promise that resolves once value is stored.
215217
*/
216-
// TODO: Rewrite following logic in verkle.spec.ts "findPath validation" test
217-
async put(_key: Uint8Array, _value: Uint8Array): Promise<void> {
218-
throw new Error('not implemented')
218+
async put(key: Uint8Array, value: Uint8Array): Promise<void> {
219+
if (key.length !== 32) throw new Error(`expected key with length 32; got ${key.length}`)
220+
const stem = key.slice(0, 31)
221+
const suffix = key[key.length - 1]
222+
this.DEBUG && this.debug(`Stem: ${bytesToHex(stem)}; Suffix: ${suffix}`, ['PUT'])
223+
224+
const putStack: [Uint8Array, VerkleNode][] = []
225+
// Find path to nearest node
226+
const foundPath = await this.findPath(stem)
227+
228+
// Sanity check - we should at least get the root node back
229+
if (foundPath.stack.length === 0) {
230+
throw new Error(`Root node not found in trie`)
231+
}
232+
233+
// Step 1) Create or update the leaf node
234+
let leafNode: LeafNode
235+
// First see if leaf node already exists
236+
if (foundPath.node !== null) {
237+
// Sanity check to verify we have the right node type
238+
if (!isLeafNode(foundPath.node)) {
239+
throw new Error(
240+
`expected leaf node found at ${bytesToHex(stem)}. Got internal node instead`
241+
)
242+
}
243+
leafNode = foundPath.node
244+
// Sanity check to verify we have the right leaf node
245+
if (!equalsBytes(leafNode.stem, stem)) {
246+
throw new Error(
247+
`invalid leaf node found. Expected stem: ${bytesToHex(stem)}; got ${bytesToHex(
248+
foundPath.node.stem
249+
)}`
250+
)
251+
}
252+
} else {
253+
// Leaf node doesn't exist, create a new one
254+
leafNode = await LeafNode.create(
255+
stem,
256+
new Array(256).fill(new Uint8Array(32)),
257+
this.verkleCrypto
258+
)
259+
this.DEBUG && this.debug(`Creating new leaf node at stem: ${bytesToHex(stem)}`, ['PUT'])
260+
}
261+
// Update value in leaf node and push to putStack
262+
leafNode.setValue(suffix, value)
263+
this.DEBUG &&
264+
this.debug(
265+
`Updating value for suffix: ${suffix} at leaf node with stem: ${bytesToHex(stem)}`,
266+
['PUT']
267+
)
268+
putStack.push([leafNode.hash(), leafNode])
269+
270+
// `path` is the path to the last node pushed to the `putStack`
271+
let lastPath = leafNode.stem
272+
273+
// Step 2) Determine if a new internal node is needed
274+
if (foundPath.stack.length > 1) {
275+
// Only insert new internal node if we have more than 1 node in the path
276+
// since a single node indicates only the root node is in the path
277+
const nearestNodeTuple = foundPath.stack.pop()!
278+
const nearestNode = nearestNodeTuple[0]
279+
lastPath = nearestNodeTuple[1]
280+
const updatedParentTuple = this.updateParent(leafNode, nearestNode, lastPath)
281+
putStack.push([updatedParentTuple.node.hash(), updatedParentTuple.node])
282+
lastPath = updatedParentTuple.lastPath
283+
284+
// Step 3) Walk up trie and update child references in parent internal nodes
285+
while (foundPath.stack.length > 1) {
286+
const [nextNode, nextPath] = foundPath.stack.pop()! as [InternalNode, Uint8Array]
287+
// Compute the child index to be updated on `nextNode`
288+
const childIndex = lastPath[matchingBytesLength(lastPath, nextPath)]
289+
// Update child reference
290+
nextNode.setChild(childIndex, {
291+
commitment: putStack[putStack.length - 1][1].commitment,
292+
path: lastPath,
293+
})
294+
this.DEBUG &&
295+
this.debug(
296+
`Updating child reference for node with path: ${bytesToHex(
297+
lastPath
298+
)} at index ${childIndex} in internal node at path ${bytesToHex(nextPath)}`,
299+
['PUT']
300+
)
301+
// Hold onto `path` to current node for updating next parent node child index
302+
lastPath = nextPath
303+
putStack.push([nextNode.hash(), nextNode])
304+
}
305+
}
306+
307+
// Step 4) Update root node
308+
const rootNode = foundPath.stack.pop()![0] as InternalNode
309+
rootNode.setChild(stem[0], {
310+
commitment: putStack[putStack.length - 1][1].commitment,
311+
path: lastPath,
312+
})
313+
this.root(this.verkleCrypto.serializeCommitment(rootNode.commitment))
314+
this.DEBUG &&
315+
this.debug(
316+
`Updating child reference for node with path: ${bytesToHex(lastPath)} at index ${
317+
lastPath[0]
318+
} in root node`,
319+
['PUT']
320+
)
321+
this.DEBUG && this.debug(`Updating root node hash to ${bytesToHex(this._root)}`, ['PUT'])
322+
putStack.push([this._root, rootNode])
323+
await this.saveStack(putStack)
219324
}
220325

326+
/**
327+
* Helper method for updating or creating the parent internal node for a given leaf node
328+
* @param leafNode the child leaf node that will be referenced by the new/updated internal node
329+
* returned by this method
330+
* @param nearestNode the nearest node to the new leaf node
331+
* @param pathToNode the path to `nearestNode`
332+
* @returns a tuple of the updated parent node and the path to that parent (i.e. the partial stem of the leaf node that leads to the parent)
333+
*/
334+
updateParent(
335+
leafNode: LeafNode,
336+
nearestNode: VerkleNode,
337+
pathToNode: Uint8Array
338+
): { node: InternalNode; lastPath: Uint8Array } {
339+
// Compute the portion of leafNode.stem and nearestNode.path that match (i.e. the partial path closest to leafNode.stem)
340+
const partialMatchingStemIndex = matchingBytesLength(leafNode.stem, pathToNode)
341+
let internalNode: InternalNode
342+
if (isLeafNode(nearestNode)) {
343+
// We need to create a new internal node and set nearestNode and leafNode as child nodes of it
344+
// Create new internal node
345+
internalNode = InternalNode.create(this.verkleCrypto)
346+
// Set leafNode and nextNode as children of the new internal node
347+
internalNode.setChild(leafNode.stem[partialMatchingStemIndex], {
348+
commitment: leafNode.commitment,
349+
path: leafNode.stem,
350+
})
351+
internalNode.setChild(nearestNode.stem[partialMatchingStemIndex], {
352+
commitment: nearestNode.commitment,
353+
path: nearestNode.stem,
354+
})
355+
// Find the path to the new internal node (the matching portion of the leaf node and next node's stems)
356+
pathToNode = leafNode.stem.slice(0, partialMatchingStemIndex)
357+
this.DEBUG &&
358+
this.debug(`Creating new internal node at path ${bytesToHex(pathToNode)}`, ['PUT'])
359+
} else {
360+
// Nearest node is an internal node. We need to update the appropriate child reference
361+
// to the new leaf node
362+
internalNode = nearestNode
363+
internalNode.setChild(leafNode.stem[partialMatchingStemIndex], {
364+
commitment: leafNode.commitment,
365+
path: leafNode.stem,
366+
})
367+
this.DEBUG &&
368+
this.debug(
369+
`Updating child reference for leaf node with stem: ${bytesToHex(
370+
leafNode.stem
371+
)} at index ${
372+
leafNode.stem[partialMatchingStemIndex]
373+
} in internal node at path ${bytesToHex(
374+
leafNode.stem.slice(0, partialMatchingStemIndex)
375+
)}`,
376+
['PUT']
377+
)
378+
}
379+
return { node: internalNode, lastPath: pathToNode }
380+
}
221381
/**
222382
* Tries to find a path to the node for the given key.
223383
* It returns a `stack` of nodes to the closest node.
@@ -231,12 +391,13 @@ export class VerkleTree {
231391
stack: [],
232392
remaining: key,
233393
}
234-
if (equalsBytes(this.root(), this.EMPTY_TREE_ROOT)) return result
394+
395+
// TODO: Decide if findPath should return an empty stack if we have an empty trie or a path with just the empty root node
396+
// if (equalsBytes(this.root(), this.EMPTY_TREE_ROOT)) return result
235397

236398
// Get root node
237399
let rawNode = await this._db.get(this.root())
238-
if (rawNode === undefined)
239-
throw new Error('root node should exist when root not empty tree root')
400+
if (rawNode === undefined) throw new Error('root node should exist')
240401

241402
const rootNode = decodeNode(rawNode, this.verkleCrypto) as InternalNode
242403

@@ -246,7 +407,7 @@ export class VerkleTree {
246407

247408
// Root node doesn't contain a child node's commitment on the first byte of the path so we're done
248409
if (equalsBytes(child.commitment, this.verkleCrypto.zeroCommitment)) {
249-
this.DEBUG && this.debug(`Partial Path ${key[0]} - found no child.`, ['FIND_PATH'])
410+
this.DEBUG && this.debug(`Partial Path ${intToHex(key[0])} - found no child.`, ['FIND_PATH'])
250411
return result
251412
}
252413
let finished = false
@@ -260,7 +421,7 @@ export class VerkleTree {
260421
// Calculate the index of the last matching byte in the key
261422
const matchingKeyLength = matchingBytesLength(key, child.path)
262423
const foundNode = equalsBytes(key, child.path)
263-
if (foundNode || child.path.length >= key.length || decodedNode instanceof LeafNode) {
424+
if (foundNode || child.path.length >= key.length || isLeafNode(decodedNode)) {
264425
// If the key and child.path are equal, then we found the node
265426
// If the child.path is the same length or longer than the key but doesn't match it
266427
// or the found node is a leaf node, we've found another node where this node should
@@ -282,16 +443,15 @@ export class VerkleTree {
282443
// We found a different node than the one specified by `key`
283444
// so the sought node doesn't exist
284445
result.remaining = key.slice(matchingKeyLength)
446+
const pathToNearestNode = isLeafNode(decodedNode) ? decodedNode.stem : child.path
285447
this.DEBUG &&
286448
this.debug(
287-
`Path ${bytesToHex(
288-
key.slice(0, matchingKeyLength)
289-
)} - found path to nearest node ${bytesToHex(
449+
`Path ${bytesToHex(pathToNearestNode)} - found path to nearest node ${bytesToHex(
290450
decodedNode.hash()
291451
)} but target node not found.`,
292452
['FIND_PATH']
293453
)
294-
result.stack.push([decodedNode, key.slice(0, matchingKeyLength)])
454+
result.stack.push([decodedNode, pathToNearestNode])
295455
return result
296456
}
297457
// Push internal node to path stack

packages/verkle/test/internalNode.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type VerkleCrypto, equalsBytes, randomBytes } from '@ethereumjs/util'
22
import { loadVerkleCrypto } from 'verkle-cryptography-wasm'
33
import { assert, beforeAll, describe, it } from 'vitest'
44

5-
import { NODE_WIDTH, VerkleNodeType, decodeNode } from '../src/node/index.js'
5+
import { NODE_WIDTH, VerkleNodeType, decodeNode, isInternalNode } from '../src/node/index.js'
66
import { InternalNode } from '../src/node/internalNode.js'
77

88
describe('verkle node - internal', () => {
@@ -14,6 +14,7 @@ describe('verkle node - internal', () => {
1414
const commitment = randomBytes(32)
1515
const node = new InternalNode({ commitment, verkleCrypto })
1616

17+
assert.ok(isInternalNode(node), 'typeguard should return true')
1718
assert.equal(node.type, VerkleNodeType.Internal, 'type should be set')
1819
assert.ok(equalsBytes(node.commitment, commitment), 'commitment should be set')
1920

packages/verkle/test/leafNode.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type VerkleCrypto, equalsBytes, randomBytes } from '@ethereumjs/util'
22
import { loadVerkleCrypto } from 'verkle-cryptography-wasm'
33
import { assert, beforeAll, describe, it } from 'vitest'
44

5-
import { VerkleNodeType } from '../src/node/index.js'
5+
import { VerkleNodeType, isLeafNode } from '../src/node/index.js'
66
import { LeafNode } from '../src/node/leafNode.js'
77

88
describe('verkle node - leaf', () => {
@@ -25,6 +25,7 @@ describe('verkle node - leaf', () => {
2525
verkleCrypto,
2626
})
2727

28+
assert.ok(isLeafNode(node), 'typeguard should return true')
2829
assert.equal(node.type, VerkleNodeType.Leaf, 'type should be set')
2930
assert.ok(
3031
equalsBytes(node.commitment as unknown as Uint8Array, commitment),

0 commit comments

Comments
 (0)