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

Add trie.del #3486

Merged
merged 11 commits into from
Jul 9, 2024
12 changes: 6 additions & 6 deletions packages/verkle/src/node/leafNode.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { equalsBytes, intToBytes, setLengthLeft, setLengthRight } from '@ethereumjs/util'

import { BaseVerkleNode } from './baseVerkleNode.js'
import { NODE_WIDTH, VerkleLeafNodeValue, VerkleNodeType } from './types.js'
import {
NODE_WIDTH,
VerkleLeafNodeValue,
VerkleNodeType,
createCValues,
createDefaultLeafValues,
createDeletedLeafValue,
createUntouchedLeafValue,
} from './types.js'
import { createCValues } from './util.js'
} from './util.js'

import type { VerkleNodeOptions } from './types.js'
import type { VerkleCrypto } from '@ethereumjs/util'
Expand Down Expand Up @@ -127,8 +125,10 @@ export class LeafNode extends BaseVerkleNode<VerkleNodeType.Leaf> {
const value = this.values[index]
switch (value) {
case VerkleLeafNodeValue.Untouched:
case VerkleLeafNodeValue.Deleted:
return undefined
case VerkleLeafNodeValue.Deleted:
// Return zeroes if a value is "deleted" (i.e. overwitten with zeroes)
return new Uint8Array(32)
default:
return value
}
Expand Down
12 changes: 0 additions & 12 deletions packages/verkle/src/node/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,3 @@ export interface VerkleNodeOptions {
}

export const NODE_WIDTH = 256

export const createUntouchedLeafValue = () => new Uint8Array(32)

export const createDeletedLeafValue = () => {
const bytes = new Uint8Array(32)
// Set the 129th bit to 1 directly by setting the 17th byte (index 16) to 0x80
bytes[16] = 0x80

return bytes
}

export const createDefaultLeafValues = () => new Array(256).fill(0)
79 changes: 44 additions & 35 deletions packages/verkle/src/node/util.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { RLP } from '@ethereumjs/rlp'
import { bigIntToBytes, bytesToBigInt, setLengthRight } from '@ethereumjs/util'
import { setLengthRight } from '@ethereumjs/util'

import { InternalNode } from './internalNode.js'
import { LeafNode } from './leafNode.js'
import {
VerkleLeafNodeValue,
type VerkleNode,
VerkleNodeType,
createDeletedLeafValue,
createUntouchedLeafValue,
} from './types.js'
import { VerkleLeafNodeValue, type VerkleNode, VerkleNodeType } from './types.js'

import type { VerkleCrypto } from '@ethereumjs/util'

Expand Down Expand Up @@ -37,19 +31,46 @@ export function isRawNode(node: Uint8Array | Uint8Array[]): node is Uint8Array[]
return Array.isArray(node) && !(node instanceof Uint8Array)
}

export function isLeafNode(node: VerkleNode): node is LeafNode {
return node.type === VerkleNodeType.Leaf
}

export function isInternalNode(node: VerkleNode): node is InternalNode {
return node.type === VerkleNodeType.Internal
}

export const createUntouchedLeafValue = () => new Uint8Array(32)

/**
* Generates a 32 byte array of zeroes and sets the 129th bit to 1, which the EIP
* refers to as the leaf marker to indicate a leaf value that has been touched previously
* and contains only zeroes
*
* Note: this value should only used in the commitment update process
*
* @returns a 32 byte array of zeroes with the 129th bit set to 1
*/
export const createDeletedLeafValue = () => {
const bytes = new Uint8Array(32)
// Set the 129th bit to 1 directly by setting the 17th byte (index 16) to 0x80
bytes[16] = 0x80

return bytes
}

export const createDefaultLeafValues = () => new Array(256).fill(0)

/***
* Converts 128 32byte values of a leaf node into 16 byte values for generating a commitment for half of a
* leaf node's values
* @param values - an array of Uint8Arrays representing the first or second set of 128 values stored by the verkle trie leaf node
* @param deletedValues - an array of booleans where a value of true at a given position indicates a value
* that is being deleted - should always be false if generating C2 values
* Returns an array of 256 16byte UintArrays with the leaf marker set for each value that is deleted
* Converts 128 32byte values of a leaf node into an array of 256 32 byte values representing
* the first and second 16 bytes of each value right padded with zeroes for generating a
* commitment for half of a leaf node's values
* @param values - an array of Uint8Arrays representing the first or second set of 128 values
* stored by the verkle trie leaf node
* Returns an array of 256 32 byte UintArrays with the leaf marker set for each value that is
* deleted
*/
export const createCValues = (
values: (Uint8Array | VerkleLeafNodeValue)[],
deletedValues = new Array(128).fill(false)
) => {
if (values.length !== 128 || deletedValues.length !== 128)
export const createCValues = (values: (Uint8Array | VerkleLeafNodeValue)[]) => {
if (values.length !== 128)
throw new Error(`got wrong number of values, expected 128, got ${values.length}`)
const expandedValues: Uint8Array[] = new Array(256)
for (let x = 0; x < 128; x++) {
Expand All @@ -59,30 +80,18 @@ export const createCValues = (
case VerkleLeafNodeValue.Untouched: // Leaf value that has never been written before
val = createUntouchedLeafValue()
break
case VerkleLeafNodeValue.Deleted: // Leaf value that has been overwritten with zeros (i.e. a deleted value)
case VerkleLeafNodeValue.Deleted: // Leaf value that has been written with zeros (either zeroes or a deleted value)
val = createDeletedLeafValue()

break
default:
val = retrievedValue
break
}
// We add 16 trailing zeros to each value since all commitments are padded to an array of 32 byte values
expandedValues[x * 2] = setLengthRight(
deletedValues[x] === true
? bigIntToBytes(bytesToBigInt(val.subarray(0, 16)) + BigInt(2 ** 128))
: val.slice(0, 16),
32
)
// TODO: Decide if we should use slice or subarray here (i.e. do we need to copy these slices or not)
// TODO: Determine whether we need to apply the leaf marker (i.e. set 129th bit) for all written values
// regardless of whether the value stored is zero or not
expandedValues[x * 2] = setLengthRight(val.slice(0, 16), 32)
expandedValues[x * 2 + 1] = setLengthRight(val.slice(16), 32)
}
return expandedValues
}
export function isLeafNode(node: VerkleNode): node is LeafNode {
return node.type === VerkleNodeType.Leaf
}

export function isInternalNode(node: VerkleNode): node is InternalNode {
return node.type === VerkleNodeType.Internal
}
18 changes: 15 additions & 3 deletions packages/verkle/src/verkleTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { loadVerkleCrypto } from 'verkle-cryptography-wasm'
import { CheckpointDB } from './db/checkpoint.js'
import { InternalNode } from './node/internalNode.js'
import { LeafNode } from './node/leafNode.js'
import { type VerkleNode } from './node/types.js'
import { decodeNode, isLeafNode } from './node/util.js'
import { VerkleLeafNodeValue, type VerkleNode } from './node/types.js'
import { createDeletedLeafValue, decodeNode, isLeafNode } from './node/util.js'
import {
type Proof,
ROOT_DB_KEY,
Expand Down Expand Up @@ -255,7 +255,13 @@ export class VerkleTree {
this.DEBUG && this.debug(`Creating new leaf node at stem: ${bytesToHex(stem)}`, ['PUT'])
}
// Update value in leaf node and push to putStack
leafNode.setValue(suffix, value)
if (equalsBytes(value, createDeletedLeafValue())) {
// Special case for when the deleted leaf value or zeroes is passed to `put`
// Writing the deleted leaf value to the suffix indicated in the key
leafNode.setValue(suffix, VerkleLeafNodeValue.Deleted)
} else {
leafNode.setValue(suffix, value)
}
this.DEBUG &&
this.debug(
`Updating value for suffix: ${suffix} at leaf node with stem: ${bytesToHex(stem)}`,
Expand Down Expand Up @@ -319,6 +325,12 @@ export class VerkleTree {
await this.saveStack(putStack)
}

async del(key: Uint8Array): Promise<void> {
const stem = key.slice(0, 31)
const suffix = key[key.length - 1]
this.DEBUG && this.debug(`Stem: ${bytesToHex(stem)}; Suffix: ${suffix}`, ['DEL'])
await this.put(key, createDeletedLeafValue())
}
/**
* Helper method for updating or creating the parent internal node for a given leaf node
* @param leafNode the child leaf node that will be referenced by the new/updated internal node
Expand Down
2 changes: 1 addition & 1 deletion packages/verkle/test/leafNode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe('verkle node - leaf', () => {
node.setValue(0, setLengthLeft(Uint8Array.from([5]), 32))
assert.deepEqual(node.getValue(0), setLengthLeft(Uint8Array.from([5]), 32))
node.setValue(0, VerkleLeafNodeValue.Deleted)
assert.equal(node.getValue(0), undefined)
assert.deepEqual(node.getValue(0), new Uint8Array(32))
})

it('should update a commitment when setting a value', async () => {
Expand Down
27 changes: 26 additions & 1 deletion packages/verkle/test/verkle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { assert, beforeAll, describe, it } from 'vitest'
import {
InternalNode,
LeafNode,
VerkleLeafNodeValue,
VerkleNodeType,
decodeNode,
matchingBytesLength,
Expand Down Expand Up @@ -211,7 +212,7 @@ describe('Verkle tree', () => {
assert.deepEqual(val2, hexToBytes(values[2]), 'confirm values[2] can be retrieved from trie')
})

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also add a test for the expected behavior when attempting to delete a value that does not exist? Left a related comment in the other file (i.e. should this throw or return?)

it('should put values and find them', async () => {
it('should sequentially put->find->delete->put values', async () => {
const keys = [
// Two keys with the same stem but different suffixes
'0x318dea512b6f3237a2d4763cf49bf26de3b617fb0cabe38a97807a5549df4d01',
Expand Down Expand Up @@ -241,5 +242,29 @@ describe('Verkle tree', () => {
assert.deepEqual(await trie.get(hexToBytes(keys[0])), hexToBytes(values[0]))
assert.deepEqual(await trie.get(hexToBytes(keys[2])), hexToBytes(values[2]))
assert.deepEqual(await trie.get(hexToBytes(keys[3])), hexToBytes(values[3]))

await trie.del(hexToBytes(keys[0]))
assert.deepEqual(await trie.get(hexToBytes(keys[0])), new Uint8Array(32))

await trie.put(hexToBytes(keys[0]), hexToBytes(values[0]))
assert.deepEqual(await trie.get(hexToBytes(keys[0])), hexToBytes(values[0]))
})
it('should put zeros in leaf node when del called with stem that was not in the trie before', async () => {
const keys = ['0x318dea512b6f3237a2d4763cf49bf26de3b617fb0cabe38a97807a5549df4d01']

const trie = await VerkleTree.create({
verkleCrypto,
db: new MapDB<Uint8Array, Uint8Array>(),
})

await trie['_createRootNode']()
assert.deepEqual(await trie.get(hexToBytes(keys[0])), undefined)
await trie.del(hexToBytes(keys[0]))
const res = await trie.findPath(hexToBytes(keys[0]).slice(0, 31))
assert.ok(res.node !== null)
assert.deepEqual(
(res.node as LeafNode).values[hexToBytes(keys[0])[31]],
VerkleLeafNodeValue.Deleted
)
})
})
Loading