Skip to content

Commit

Permalink
verkle: improvements to verkle tree instantiation (#3768)
Browse files Browse the repository at this point in the history
* verkle: make all verkle tree opts optional

* verkle: simplify createVerkleTree constructor and provide MapDB default db

* verkle: make createRootNode public

* verkle: adjust createRootNode in usage

* verkle: remove database from verkle tree

* verkle: adjust examples and tests

* verkle: remove unnecessary param

* verkle: always pass in verkleCrypto

* verkle: rework constructor and createVerkletree handling and simplify defaults

* verkle: remove unnecessary interfaces

* verkle: update verkle tree usage in stateful verkle state manager

* verkle: update example

* verkle: update examples in docs

* clean up and add tests

* Fix root persistence bug

---------

Co-authored-by: acolytec3 <[email protected]>
  • Loading branch information
gabrocheleau and acolytec3 authored Oct 25, 2024
1 parent a492e24 commit 65ccf7c
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 104 deletions.
7 changes: 6 additions & 1 deletion packages/statemanager/src/statefulVerkleStateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,12 @@ export class StatefulVerkleStateManager implements StateManagerInterface {
this.common = opts.common ?? new Common({ chain: Mainnet, eips: [6800] })
this._trie =
opts.trie ??
new VerkleTree({ verkleCrypto: opts.verkleCrypto, db: new MapDB<Uint8Array, Uint8Array>() })
new VerkleTree({
verkleCrypto: opts.verkleCrypto,
db: new MapDB<Uint8Array, Uint8Array>(),
useRootPersistence: false,
cacheSize: 0,
})
this._debug = debugDefault('statemanager:verkle:stateful')
this.originalStorageCache = new OriginalStorageCache(this.getStorage.bind(this))
this._caches = opts.caches
Expand Down
9 changes: 7 additions & 2 deletions packages/verkle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,13 @@ import { loadVerkleCrypto } from 'verkle-cryptography-wasm'
const verkleCrypto = await loadVerkleCrypto()

const main = async () => {
const tree = new VerkleTree({ verkleCrypto, db: new MapDB<Uint8Array, Uint8Array>() })
await tree['_createRootNode']()
const tree = new VerkleTree({
cacheSize: 0,
db: new MapDB<Uint8Array, Uint8Array>(),
useRootPersistence: false,
verkleCrypto,
})
await tree.createRootNode()
console.log(bytesToHex(tree.root())) // 0x0000000000000000000000000000000000000000000000000000000000000000
}

Expand Down
9 changes: 7 additions & 2 deletions packages/verkle/examples/diyVerkle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ import { loadVerkleCrypto } from 'verkle-cryptography-wasm'
const verkleCrypto = await loadVerkleCrypto()

const main = async () => {
const tree = new VerkleTree({ verkleCrypto, db: new MapDB<Uint8Array, Uint8Array>() })
await tree['_createRootNode']()
const tree = new VerkleTree({
cacheSize: 0,
db: new MapDB<Uint8Array, Uint8Array>(),
useRootPersistence: false,
verkleCrypto,
})
await tree.createRootNode()
console.log(bytesToHex(tree.root())) // 0x0000000000000000000000000000000000000000000000000000000000000000
}

Expand Down
36 changes: 17 additions & 19 deletions packages/verkle/src/constructors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,34 @@ import { VerkleTree } from './verkleTree.js'

import type { VerkleTreeOpts } from './types.js'

export async function createVerkleTree(opts?: VerkleTreeOpts) {
export async function createVerkleTree(opts?: Partial<VerkleTreeOpts>) {
const key = ROOT_DB_KEY

if (opts?.db !== undefined && opts?.useRootPersistence === true) {
if (opts?.root === undefined) {
opts.root = await opts?.db.get(key, {
// Provide sensible default options
const parsedOptions = {
...opts,
db: opts?.db ?? new MapDB<Uint8Array, Uint8Array>(),
verkleCrypto: opts?.verkleCrypto ?? (await loadVerkleCrypto()),
useRootPersistence: opts?.useRootPersistence ?? false,
cacheSize: opts?.cacheSize ?? 0,
}

if (parsedOptions.useRootPersistence === true) {
if (parsedOptions.root === undefined) {
parsedOptions.root = await parsedOptions.db.get(key, {
keyEncoding: KeyEncoding.Bytes,
valueEncoding: ValueEncoding.Bytes,
})
} else {
await opts?.db.put(key, opts.root, {
await parsedOptions.db.put(key, parsedOptions.root, {
keyEncoding: KeyEncoding.Bytes,
valueEncoding: ValueEncoding.Bytes,
})
}
}

if (opts?.verkleCrypto === undefined) {
const verkleCrypto = await loadVerkleCrypto()
if (opts === undefined)
opts = {
verkleCrypto,
db: new MapDB<Uint8Array, Uint8Array>(),
}
else {
opts.verkleCrypto = verkleCrypto
}
}

const trie = new VerkleTree(opts)
await trie['_createRootNode']()
const trie = new VerkleTree(parsedOptions)
// If the root node does not exist, initialize the empty root node
if (parsedOptions.root === undefined) await trie.createRootNode()
return trie
}
9 changes: 2 additions & 7 deletions packages/verkle/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface VerkleTreeOpts {
/**
* An instantiated Verkle Cryptography interface
*/
verkleCrypto: VerkleCrypto | undefined
verkleCrypto: VerkleCrypto
/**
* A database instance.
*/
Expand All @@ -25,18 +25,13 @@ export interface VerkleTreeOpts {
/**
* Store the root inside the database after every `write` operation
*/
useRootPersistence?: boolean
useRootPersistence: boolean

/**
* LRU cache for tree nodes to allow for faster node retrieval.
*
* Default: 0 (deactivated)
*/
cacheSize?: number
}

export type VerkleTreeOptsWithDefaults = VerkleTreeOpts & {
useRootPersistence: boolean
cacheSize: number
}

Expand Down
61 changes: 14 additions & 47 deletions packages/verkle/src/verkleTree.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,16 @@
import {
Lock,
MapDB,
bytesToHex,
equalsBytes,
intToHex,
matchingBytesLength,
} from '@ethereumjs/util'
import { Lock, bytesToHex, equalsBytes, intToHex, matchingBytesLength } from '@ethereumjs/util'
import debug from 'debug'

import { CheckpointDB } from './db/checkpoint.js'
import { InternalVerkleNode } from './node/internalNode.js'
import { LeafVerkleNode } from './node/leafNode.js'
import { LeafVerkleNodeValue, type VerkleNode } from './node/types.js'
import { createZeroesLeafValue, decodeVerkleNode, isLeafVerkleNode } from './node/util.js'
import {
type Proof,
ROOT_DB_KEY,
type VerkleTreeOpts,
type VerkleTreeOptsWithDefaults,
} from './types.js'
import { type Proof, ROOT_DB_KEY, type VerkleTreeOpts } from './types.js'

// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { createVerkleTree } from './constructors.js' // Imported so intellisense can display docs
import type { DB, PutBatch, VerkleCrypto } from '@ethereumjs/util'
import type { PutBatch, VerkleCrypto } from '@ethereumjs/util'
import type { Debugger } from 'debug'
interface Path {
node: VerkleNode | null
Expand All @@ -34,12 +22,7 @@ interface Path {
* The basic verkle tree interface, use with `import { VerkleTree } from '@ethereumjs/verkle'`.
*/
export class VerkleTree {
protected readonly _opts: VerkleTreeOptsWithDefaults = {
useRootPersistence: false,
cacheSize: 0,
verkleCrypto: undefined,
db: new MapDB<Uint8Array, Uint8Array>(),
}
_opts: VerkleTreeOpts

/** The root for an empty tree */
EMPTY_TREE_ROOT: Uint8Array
Expand All @@ -60,14 +43,15 @@ export class VerkleTree {
* Creates a new verkle tree.
* @param opts Options for instantiating the verkle tree
*
* Note: in most cases, the static {@link createVerkleTree} constructor should be used. It uses the same API but provides sensible defaults
* Note: in most cases, the static {@link createVerkleTree} constructor should be used. It uses the same API but provides sensible defaults
*/
constructor(opts?: VerkleTreeOpts) {
if (opts !== undefined) {
this._opts = { ...this._opts, ...opts }
}
constructor(opts: VerkleTreeOpts) {
this._opts = opts

this.database(opts?.db)
if (opts.db instanceof CheckpointDB) {
throw new Error('Cannot pass in an instance of CheckpointDB')
}
this._db = new CheckpointDB({ db: opts.db, cacheSize: opts.cacheSize })

this.EMPTY_TREE_ROOT = new Uint8Array(32)
this._hashLen = this.EMPTY_TREE_ROOT.length
Expand All @@ -77,11 +61,7 @@ export class VerkleTree {
this.root(opts.root)
}

if (opts === undefined || opts?.verkleCrypto === undefined) {
throw new Error('instantiated verkle cryptography option required for verkle tries')
}

this.verkleCrypto = opts?.verkleCrypto
this.verkleCrypto = opts.verkleCrypto

this.DEBUG =
typeof window === 'undefined' ? (process?.env?.DEBUG?.includes('ethjs') ?? false) : false
Expand All @@ -103,18 +83,6 @@ export class VerkleTree {
|| ----------------`)
}

database(db?: DB<Uint8Array, Uint8Array>) {
if (db !== undefined) {
if (db instanceof CheckpointDB) {
throw new Error('Cannot pass in an instance of CheckpointDB')
}

this._db = new CheckpointDB({ db, cacheSize: this._opts.cacheSize })
}

return this._db
}

/**
* Gets and/or Sets the current root of the `tree`
*/
Expand Down Expand Up @@ -468,10 +436,9 @@ export class VerkleTree {

/**
* Create empty root node for initializing an empty tree.
* @private
*/

protected async _createRootNode(): Promise<void> {
async createRootNode(): Promise<void> {
const rootNode = new InternalVerkleNode({
commitment: this.verkleCrypto.zeroCommitment,
verkleCrypto: this.verkleCrypto,
Expand Down Expand Up @@ -571,7 +538,7 @@ export class VerkleTree {
* Persists the root hash in the underlying database
*/
async persistRoot() {
if (this._opts.useRootPersistence) {
if (this._opts.useRootPersistence === true) {
await this._db.put(ROOT_DB_KEY, this.root())
}
}
Expand Down
8 changes: 1 addition & 7 deletions packages/verkle/test/proof.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,7 @@ describe('lets make proofs', () => {
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0300000000000000000000000000000000000000000000000000000000000000',
].map((key) => hexToBytes(key as PrefixedHexString))
const trie = await createVerkleTree({
verkleCrypto,
db: new MapDB<Uint8Array, Uint8Array>(),
})

await trie['_createRootNode']()
const trie = await createVerkleTree()

const keyWithMultipleValues = keys[0].slice(0, 31)
await trie.put(keyWithMultipleValues, [keys[0][31], keys[1][31]], [values[0], values[1]])
Expand Down Expand Up @@ -73,7 +68,6 @@ describe('lets make proofs', () => {
it('should pass for empty trie', async () => {
const trie = await createVerkleTree({ verkleCrypto, db: new MapDB() })

await trie['_createRootNode']()
const proof = verkleCrypto.createProof([
{
// Get commitment from root node
Expand Down
46 changes: 28 additions & 18 deletions packages/verkle/test/verkle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,31 @@ describe('Verkle tree', () => {
beforeAll(async () => {
verkleCrypto = await loadVerkleCrypto()
})

it('should instantiate with verkle crypto and a MapDB if no options are provided', async () => {
const tree = await createVerkleTree()
assert.ok(tree['_db'].db instanceof MapDB)
assert.ok(tree['verkleCrypto'] !== undefined)
})

it('should not destroy a previous root', async () => {
const tree = await createVerkleTree({ useRootPersistence: true })
await tree.put(
hexToBytes('0x318dea512b6f3237a2d4763cf49bf26de3b617fb0cabe38a97807a5549df4d'),
[0],
[hexToBytes('0x01')],
)
const root = tree.root()

const tree2 = await createVerkleTree({
verkleCrypto: tree['verkleCrypto'],
db: tree['_db'].db,
useRootPersistence: true,
root,
})
assert.deepEqual(tree2.root(), root)
})

it('should insert and retrieve values', async () => {
// Testdata based on https://github.com/gballet/go-ethereum/blob/kaustinen-with-shapella/trie/verkle_test.go
const presentKeys = [
Expand Down Expand Up @@ -69,7 +94,6 @@ describe('Verkle tree', () => {

const tree = await createVerkleTree({
verkleCrypto,
db: new MapDB<Uint8Array, Uint8Array>(),
})

const res = await tree.findPath(presentKeys[0])
Expand Down Expand Up @@ -120,12 +144,7 @@ describe('Verkle tree', () => {
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0300000000000000000000000000000000000000000000000000000000000000',
].map((key) => hexToBytes(key as PrefixedHexString))
const trie = await createVerkleTree({
verkleCrypto,
db: new MapDB<Uint8Array, Uint8Array>(),
})

await trie['_createRootNode']()
const trie = await createVerkleTree({ verkleCrypto })

let putStack: [Uint8Array, VerkleNode][] = []
const stem1 = keys[0].slice(0, 31)
Expand Down Expand Up @@ -231,12 +250,7 @@ describe('Verkle tree', () => {
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0300000000000000000000000000000000000000000000000000000000000000',
].map((key) => hexToBytes(key as PrefixedHexString))
const trie = await createVerkleTree({
verkleCrypto,
db: new MapDB<Uint8Array, Uint8Array>(),
})

await trie['_createRootNode']()
const trie = await createVerkleTree({ verkleCrypto })

const keyWithMultipleValues = keys[0].slice(0, 31)
await trie.put(keyWithMultipleValues, [keys[0][31], keys[1][31]], [values[0], values[1]])
Expand All @@ -255,12 +269,8 @@ describe('Verkle tree', () => {
it('should put zeros in leaf node when del called with stem that was not in the trie before', async () => {
const keys = [hexToBytes('0x318dea512b6f3237a2d4763cf49bf26de3b617fb0cabe38a97807a5549df4d01')]

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

await trie['_createRootNode']()
assert.deepEqual(await trie.get(keys[0].slice(0, 31), [keys[0][31]]), [])

await trie.del(keys[0].slice(0, 31), [keys[0][31]])
Expand Down
2 changes: 1 addition & 1 deletion packages/vm/test/api/runBlock.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ describe.skip('run a verkle block', () => {
const blockRlp = hexToBytes(verkleJSON.blocks[0].rlp as PrefixedHexString)
const block = createBlockFromRLP(blockRlp, { common })
const sm = new StatefulVerkleStateManager({ verkleCrypto })
await sm['_trie']['_createRootNode']()
await sm['_trie'].createRootNode()
const blockchain = await createBlockchain({ common })
const vm = await setupVM({ common, stateManager: sm, blockchain, genesisBlock })
await setupPreConditions(vm.stateManager, verkleJSON)
Expand Down

0 comments on commit 65ccf7c

Please sign in to comment.