-
Notifications
You must be signed in to change notification settings - Fork 805
Verkle Implementation: Build out Trie Processing #3430
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
Changes from 11 commits
0b03444
f468ce2
c1ce5eb
a317698
1670155
55d644f
974d1ae
570f8c4
430d801
fa34dde
b2806cf
f9e019e
74676b0
a267be1
e3ab994
6e7e91e
4a716a7
a2ed90e
1902ebc
541240c
76da96c
52d50a8
86dd9e9
189b112
60219ae
bbd0e96
2301d2c
dcdd778
cf33476
b9285e2
1dcc64d
f42ac23
e4e257a
1b1706d
96f99aa
e4bcb66
27144c0
7f4ead6
abc442d
eeb2ec8
e7e81bf
991f4d2
58e5298
06350d0
df22e18
69267f7
911c5b1
a38cc80
ce343ae
9c7b214
ae09bdb
05bfebd
0b4904c
cd65a9a
1ca879e
3ab0700
720ab09
b47fa49
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
participants: | ||
- el_type: ethereumjs | ||
el_image: ethereumjs:local | ||
cl_type: grandine | ||
cl_image: ethpandaops/grandine:feature-electra | ||
|
||
- el_type: ethereumjs | ||
el_image: ethereumjs:local | ||
cl_type: grandine | ||
cl_image: ethpandaops/grandine:feature-electra | ||
|
||
network_params: | ||
electra_fork_epoch: 1 | ||
electra_fork_version: "0x50000038" | ||
#global_log_level: debug | ||
additional_services: | ||
- dora | ||
- tx_spammer | ||
- blob_spammer | ||
|
||
snooper_enabled: true |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,11 +1,12 @@ | ||||||
import { equalsBytes } from '@ethereumjs/util' | ||||||
import { BIGINT_0, bytesToBigInt, equalsBytes } from '@ethereumjs/util' | ||||||
|
||||||
import { POINT_IDENTITY } from '../util/crypto.js' | ||||||
|
||||||
import { BaseVerkleNode } from './baseVerkleNode.js' | ||||||
import { LeafNode } from './leafNode.js' | ||||||
import { NODE_WIDTH, VerkleNodeType } from './types.js' | ||||||
|
||||||
import type { VerkleCrypto } from '../types.js' | ||||||
import type { VerkleNode, VerkleNodeOptions } from './types.js' | ||||||
|
||||||
export class InternalNode extends BaseVerkleNode<VerkleNodeType.Internal> { | ||||||
|
@@ -14,9 +15,9 @@ export class InternalNode extends BaseVerkleNode<VerkleNodeType.Internal> { | |||||
public copyOnWrite: Record<string, Uint8Array> | ||||||
public type = VerkleNodeType.Internal | ||||||
|
||||||
/* TODO: options.children is not actually used here */ | ||||||
constructor(options: VerkleNodeOptions[VerkleNodeType.Internal]) { | ||||||
super(options) | ||||||
// TODO: Decide whether to fill with null or not - personal opinion - should never use null | ||||||
this.children = options.children ?? new Array(NODE_WIDTH).fill(null) | ||||||
this.copyOnWrite = options.copyOnWrite ?? {} | ||||||
} | ||||||
|
@@ -63,19 +64,30 @@ export class InternalNode extends BaseVerkleNode<VerkleNodeType.Internal> { | |||||
return this.children?.[index] ?? null | ||||||
} | ||||||
|
||||||
insert(key: Uint8Array, value: Uint8Array, resolver: () => void): void { | ||||||
insert( | ||||||
key: Uint8Array, | ||||||
value: Uint8Array, | ||||||
resolver: () => void, | ||||||
verkleCrypto?: VerkleCrypto | ||||||
acolytec3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
): void { | ||||||
const values = new Array<Uint8Array>(NODE_WIDTH) | ||||||
values[key[31]] = value | ||||||
this.insertStem(key.slice(0, 31), values, resolver) | ||||||
this.insertStem(key.slice(0, 31), values, resolver, verkleCrypto!) | ||||||
acolytec3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} | ||||||
|
||||||
insertStem(stem: Uint8Array, values: Uint8Array[], resolver: () => void): void { | ||||||
insertStem( | ||||||
stem: Uint8Array, | ||||||
values: Uint8Array[], | ||||||
resolver: () => void, | ||||||
verkleCrypto: VerkleCrypto | ||||||
acolytec3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
): void { | ||||||
// Index of the child pointed by the next byte in the key | ||||||
const childIndex = stem[this.depth] | ||||||
|
||||||
const child = this.children[childIndex] | ||||||
|
||||||
if (child instanceof LeafNode) { | ||||||
// TODO: Understand the intent of what cowChild is suppoded to do | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cow stands for "copy-on-write". As far as I recall cowChild is used as a record to mark the children that have been modified, in order to stack the commitment updates at the end since that is way more efficient.
acolytec3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
this.cowChild(childIndex) | ||||||
if (equalsBytes(child.stem, stem)) { | ||||||
return child.insertMultiple(stem, values) | ||||||
|
@@ -93,19 +105,26 @@ export class InternalNode extends BaseVerkleNode<VerkleNodeType.Internal> { | |||||
|
||||||
const nextByteInInsertedKey = stem[this.depth + 1] | ||||||
if (nextByteInInsertedKey === nextByteInExistingKey) { | ||||||
return newBranch.insertStem(stem, values, resolver) | ||||||
return newBranch.insertStem(stem, values, resolver, verkleCrypto) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
// Next word differs, so this was the last level. | ||||||
// Insert it directly into its final slot. | ||||||
const leafNode = LeafNode.create(stem, values) | ||||||
// TODO: Determine if this is how to create the correct commitment | ||||||
let commitment = verkleCrypto.zeroCommitment | ||||||
for (const [idx, value] of values.entries()) { | ||||||
if (bytesToBigInt(value) > BIGINT_0) | ||||||
commitment = verkleCrypto.updateCommitment(commitment, idx, new Uint8Array(32), value) | ||||||
} | ||||||
const leafNode = LeafNode.create(stem, values, this.depth + 1, commitment) | ||||||
|
||||||
// TODO - Why is the leaf node set at depth + 2 instead of + 1)? | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
leafNode.setDepth(this.depth + 2) | ||||||
newBranch.cowChild(nextByteInInsertedKey) | ||||||
newBranch.children[nextByteInInsertedKey] = leafNode | ||||||
} else if (child instanceof InternalNode) { | ||||||
this.cowChild(childIndex) | ||||||
return child.insertStem(stem, values, resolver) | ||||||
return child.insertStem(stem, values, resolver, verkleCrypto) | ||||||
acolytec3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} else { | ||||||
throw new Error('Invalid node type') | ||||||
} | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,3 @@ | ||
/* eslint-disable @typescript-eslint/no-unused-vars */ | ||
import { BaseVerkleNode } from './baseVerkleNode.js' | ||
import { NODE_WIDTH, VerkleNodeType } from './types.js' | ||
|
||
|
@@ -7,9 +6,9 @@ import type { VerkleNodeOptions } from './types.js' | |
|
||
export class LeafNode extends BaseVerkleNode<VerkleNodeType.Leaf> { | ||
public stem: Uint8Array | ||
public values: Uint8Array[] | ||
public c1: Point | ||
public c2: Point | ||
public values: Uint8Array[] // Array of 256 possible values represented as 32 byte Uint8Arrays | ||
public c1?: Point | ||
public c2?: Point | ||
public type = VerkleNodeType.Leaf | ||
|
||
constructor(options: VerkleNodeOptions[VerkleNodeType.Leaf]) { | ||
|
@@ -21,8 +20,13 @@ export class LeafNode extends BaseVerkleNode<VerkleNodeType.Leaf> { | |
this.c2 = options.c2 | ||
} | ||
|
||
static create(stem: Uint8Array, values: Uint8Array[]): LeafNode { | ||
throw new Error('Not implemented') | ||
static create( | ||
stem: Uint8Array, | ||
values: Uint8Array[], | ||
depth: number, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we remove depth here too? |
||
commitment: Uint8Array | ||
): LeafNode { | ||
return new LeafNode({ stem, values, depth, commitment }) | ||
} | ||
|
||
static fromRawNode(rawNode: Uint8Array[], depth: number): LeafNode { | ||
|
@@ -73,8 +77,8 @@ export class LeafNode extends BaseVerkleNode<VerkleNodeType.Leaf> { | |
new Uint8Array([VerkleNodeType.Leaf]), | ||
this.stem, | ||
this.commitment, | ||
this.c1.bytes(), | ||
this.c2.bytes(), | ||
this.c1?.bytes() ?? new Uint8Array(), | ||
this.c2?.bytes() ?? new Uint8Array(), | ||
...this.values, | ||
] | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,9 +13,10 @@ import { | |
type VerkleTreeOptsWithDefaults, | ||
} from './types.js' | ||
import { WalkController, matchingBytesLength } from './util/index.js' | ||
import { verifyKeyLength } from './util/keys.js' | ||
|
||
import type { VerkleNode } from './node/types.js' | ||
import type { FoundNodeFunction } from './types.js' | ||
import type { FoundNodeFunction, VerkleCrypto } from './types.js' | ||
import type { BatchDBOp, DB, PutBatch } from '@ethereumjs/util' | ||
|
||
interface Path { | ||
|
@@ -43,7 +44,7 @@ export class VerkleTree { | |
protected _lock = new Lock() | ||
protected _root: Uint8Array | ||
|
||
protected verkleCrypto: any | ||
protected verkleCrypto: VerkleCrypto | ||
/** | ||
* Creates a new verkle tree. | ||
* @param opts Options for instantiating the verkle tree | ||
|
@@ -153,14 +154,16 @@ export class VerkleTree { | |
* @returns A Promise that resolves to `Uint8Array` if a value was found or `null` if no value was found. | ||
*/ | ||
async get(key: Uint8Array, throwIfMissing = false): Promise<Uint8Array | null> { | ||
const node = await this.findLeafNode(key, throwIfMissing) | ||
if (node !== null) { | ||
const keyLastByte = key[key.length - 1] | ||
|
||
// The retrieved leaf node contains an array of 256 possible values. | ||
// The index of the value we want is at the key's last byte | ||
return node.values?.[keyLastByte] ?? null | ||
} | ||
verifyKeyLength(key) | ||
const node = await this._db.get(key) | ||
if (node !== undefined && node instanceof LeafNode) | ||
if (node instanceof LeafNode) { | ||
const keyLastByte = key[key.length - 1] | ||
|
||
// The retrieved leaf node contains an array of 256 possible values. | ||
// The index of the value we want is at the key's last byte | ||
return node.values?.[keyLastByte] ?? null | ||
} | ||
|
||
return null | ||
} | ||
|
@@ -173,14 +176,22 @@ export class VerkleTree { | |
* @returns A Promise that resolves once value is stored. | ||
*/ | ||
async put(key: Uint8Array, value: Uint8Array): Promise<void> { | ||
verifyKeyLength(key) | ||
await this._db.put(key, value) | ||
|
||
// Find or create the leaf node | ||
const leafNode = await this.findLeafNode(key, false) | ||
if (leafNode === null) { | ||
let leafNode = await this.findLeafNode(key, false) | ||
if (!(leafNode instanceof LeafNode)) { | ||
// If leafNode is missing, create it | ||
// leafNode = LeafNode.create() | ||
throw new Error('Not implemented') | ||
const values = new Array(256) as Uint8Array[] // Create new empty array of 256 values | ||
const suffix = key[31] | ||
values[suffix] = value // Set value at key suffix | ||
|
||
// Generate a commitment for the new leaf node, using the zero commitment as a base | ||
const commitment = this.verkleCrypto.zeroCommitment | ||
// TODO: Confirm the old/new scalar values we're passing in here are correct | ||
this.verkleCrypto.updateCommitment(commitment, suffix, new Uint8Array(32), value) | ||
leafNode = LeafNode.create(key.slice(0, 31), values, leafNode.length, commitment) | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe I'm not following this, but where is the part of the code where if the leafNode exists, we update it? I'm just seeing us create a new leaf if it does not exist in the trie. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, this is still very incomplete. This is where I'm currently focused. We need to actually pull parent nodes out of the DB and update the child commitment value in their values array and then update their commitment. |
||
// Walk up the tree and update internal nodes | ||
|
@@ -280,13 +291,14 @@ export class VerkleTree { | |
* @param key - the search key | ||
* @param throwIfMissing - if true, throws if any nodes are missing. Used for verifying proofs. (default: false) | ||
*/ | ||
async findLeafNode(key: Uint8Array, throwIfMissing = false): Promise<LeafNode | null> { | ||
const { node } = await this.findPath(key, throwIfMissing) | ||
async findLeafNode(key: Uint8Array, throwIfMissing = false): Promise<LeafNode | VerkleNode[]> { | ||
const { node, stack } = await this.findPath(key, throwIfMissing) | ||
if (!(node instanceof LeafNode)) { | ||
if (throwIfMissing) { | ||
throw new Error('leaf node not found') | ||
} | ||
return null | ||
// return depth of remaining nodes | ||
return stack | ||
} | ||
return node | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't seem like that should belong to this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't. Accidentally included and will remove.