Skip to content

Commit

Permalink
verkle: implement verkle proof verification (#3423)
Browse files Browse the repository at this point in the history
* statemanager: proper type for verkleproof

* verkle: add verifyProof wrapper in verkle crypto

* verkle: remove unused import

* chore: update package lock

* statemanageR: add verifyProof implementation to stateless verkle statemanager

* verkle: add jsdoc for verkle verifyProof method

* verkle: verkle proof test

* verkle: add failing test case

* client: add the ability to provide and use the parentStateRoot

* Update verkle crypto dep

* Activate invalid proof test

* src: cleanup

* Update packages/client/src/config.ts

* vm: move up error check

---------

Co-authored-by: acolytec3 <17355484+acolytec3@users.noreply.github.com>
  • Loading branch information
gabrocheleau and acolytec3 committed May 17, 2024
1 parent cfe942e commit c95499c
Show file tree
Hide file tree
Showing 12 changed files with 136 additions and 60 deletions.
11 changes: 6 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/client/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,12 @@ const args: ClientOpts = yargs
boolean: true,
hidden: true,
})
.option('initialVerkleStateRoot', {
describe:
'Provides an initial stateRoot to start the StatelessVerkleStateManager. This is required to bootstrap verkle witness proof verification, since they depend on the stateRoot of the parent block',
string: true,
coerce: (initialVerkleStateRoot: PrefixedHexString) => hexToBytes(initialVerkleStateRoot),
})
.option('useJsCrypto', {
describe: 'Use pure Javascript cryptography functions',
boolean: true,
Expand Down
3 changes: 3 additions & 0 deletions packages/client/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ export interface ConfigOptions {
statelessVerkle?: boolean
startExecution?: boolean
ignoreStatelessInvalidExecs?: boolean
initialVerkleStateRoot?: Uint8Array

/**
* Enables Prometheus Metrics that can be collected for monitoring client health
Expand Down Expand Up @@ -451,6 +452,7 @@ export class Config {
public readonly statelessVerkle: boolean
public readonly startExecution: boolean
public readonly ignoreStatelessInvalidExecs: boolean
public readonly initialVerkleStateRoot: Uint8Array

public synchronized: boolean
public lastsyncronized?: boolean
Expand Down Expand Up @@ -544,6 +546,7 @@ export class Config {
this.ignoreStatelessInvalidExecs = options.ignoreStatelessInvalidExecs ?? false

this.metrics = options.prometheusMetrics
this.initialVerkleStateRoot = options.initialVerkleStateRoot ?? new Uint8Array()

// Start it off as synchronized if this is configured to mine or as single node
this.synchronized = this.isSingleNode ?? this.mine
Expand Down
6 changes: 5 additions & 1 deletion packages/client/src/execution/vmexecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,9 @@ export class VMExecution extends Execution {
return
}
this.config.logger.info(`Setting up verkleVM`)
const stateManager = await StatelessVerkleStateManager.create()
const stateManager = await StatelessVerkleStateManager.create({
initialStateRoot: this.config.initialVerkleStateRoot,
})
this.verkleVM = await VM.create({
common: this.config.execCommon,
blockchain: this.chain.blockchain,
Expand Down Expand Up @@ -439,6 +441,7 @@ export class VMExecution extends Execution {
const result = await vm.runBlock({
clearCache,
...opts,
parentStateRoot: prevVMStateRoot,
skipHeaderValidation,
reportPreimages,
})
Expand Down Expand Up @@ -733,6 +736,7 @@ export class VMExecution extends Execution {
skipBlockValidation,
skipHeaderValidation: true,
reportPreimages: this.config.savePreimages,
parentStateRoot: parentState,
})
const afterTS = Date.now()
const diffSec = Math.round((afterTS - beforeTS) / 1000)
Expand Down
2 changes: 1 addition & 1 deletion packages/statemanager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"ethereum-cryptography": "^2.1.3",
"js-sdsl": "^4.1.4",
"lru-cache": "10.1.0",
"verkle-cryptography-wasm": "^0.4.1"
"verkle-cryptography-wasm": "^0.4.2"
},
"devDependencies": {
"@ethereumjs/block": "^5.2.0",
Expand Down
70 changes: 34 additions & 36 deletions packages/statemanager/src/statelessVerkleStateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
getStem,
getTreeKeyForCodeChunk,
getTreeKeyForStorageSlot,
verifyProof,
} from '@ethereumjs/verkle'
import debugDefault from 'debug'
import { keccak256 } from 'ethereum-cryptography/keccak.js'
Expand All @@ -30,7 +31,7 @@ import { OriginalStorageCache } from './cache/originalStorageCache.js'

import type { AccessedStateWithAddress } from './accessWitness.js'
import type { DefaultStateManager } from './stateManager.js'
import type { VerkleExecutionWitness } from '@ethereumjs/block'
import type { VerkleExecutionWitness, VerkleProof } from '@ethereumjs/block'
import type {
AccountFields,
Common,
Expand Down Expand Up @@ -110,6 +111,7 @@ export interface StatelessVerkleStateManagerOpts {
codeCacheOpts?: CacheOptions
accesses?: AccessWitness
verkleCrypto?: VerkleCrypto
initialStateRoot?: Uint8Array
}

const PUSH_OFFSET = 95
Expand All @@ -135,6 +137,7 @@ export class StatelessVerkleStateManager implements EVMStateManagerInterface {
_accountCache?: AccountCache
_storageCache?: StorageCache
_codeCache?: CodeCache
_cachedStateRoot?: Uint8Array

originalStorageCache: OriginalStorageCache

Expand All @@ -156,7 +159,7 @@ export class StatelessVerkleStateManager implements EVMStateManagerInterface {
private _blockNum = BigInt(0)
private _executionWitness?: VerkleExecutionWitness

private _proof: Uint8Array | undefined
private _proof: VerkleProof | undefined

// State along execution (should update)
private _state: VerkleState = {}
Expand Down Expand Up @@ -229,6 +232,8 @@ export class StatelessVerkleStateManager implements EVMStateManagerInterface {
})
}

this._cachedStateRoot = opts.initialStateRoot

this.keccakFunction = opts.common?.customCrypto.keccak256 ?? keccak256

if (opts.verkleCrypto === undefined) {
Expand Down Expand Up @@ -261,7 +266,7 @@ export class StatelessVerkleStateManager implements EVMStateManagerInterface {
this._executionWitness = executionWitness
this.accessWitness = accessWitness ?? new AccessWitness({ verkleCrypto: this.verkleCrypto })

this._proof = executionWitness.verkleProof as unknown as Uint8Array
this._proof = executionWitness.verkleProof

// Populate the pre-state and post-state from the executionWitness
const preStateRaw = executionWitness.stateDiff.flatMap(({ stem, suffixDiffs }) => {
Expand Down Expand Up @@ -493,7 +498,7 @@ export class StatelessVerkleStateManager implements EVMStateManagerInterface {
}
}

// Note from Gabriel: This is actually not possible in Verkle.
// Note from Gabriel: Clearing storage is not actually not possible in Verkle.
// This is because the storage keys are scattered throughout the verkle tree.
/**
* Clears all storage entries for the account corresponding to `address`.
Expand Down Expand Up @@ -657,30 +662,18 @@ export class StatelessVerkleStateManager implements EVMStateManagerInterface {
getProof(_: Address, __: Uint8Array[] = []): Promise<Proof> {
throw new Error('Not implemented yet')
}
/**
* Verifies whether the execution witness matches the stateRoot
* @param {Uint8Array} stateRoot - The stateRoot to verify the executionWitness against
* @returns {boolean} - Returns true if the executionWitness matches the provided stateRoot, otherwise false
*/
verifyProof(stateRoot: Uint8Array): boolean {
if (this._executionWitness === undefined) {
debug('Missing executionWitness')
return false
}

// TODO: Re-implement this method once we have working verifyUpdate and the testnets have been updated to provide ingestible data
async verifyProof(_: Uint8Array): Promise<boolean> {
// Implementation: https://github.com/crate-crypto/rust-verkle-wasm/blob/master/src/lib.rs#L45
// The root is the root of the current (un-updated) trie
// The proof is proof of membership of all of the accessed values
// keys_values is a map from the key of the accessed value to a tuple
// the tuple contains the old value and the updated value
//
// This function returns the new root when all of the updated values are applied

// const updatedStateRoot: Uint8Array = verifyUpdate(
// parentVerkleRoot,
// this._proof!, // TODO: Convert this into a Uint8Array ingestible by the method
// new Map() // TODO: Generate the keys_values map from the old to the updated value
// )

// TODO: Not sure if this should return the updated state Root (current block) or the un-updated one (parent block)
// const verkleRoot = await this.getStateRoot()

// Verify that updatedStateRoot matches the state root of the block
// return equalsBytes(updatedStateRoot, verkleRoot)

return true
return verifyProof(this.verkleCrypto, stateRoot, this._executionWitness)
}

// Verifies that the witness post-state matches the computed post-state
Expand Down Expand Up @@ -903,21 +896,26 @@ export class StatelessVerkleStateManager implements EVMStateManagerInterface {
async flush(): Promise<void> {}

/**
* Gets the verkle root.
* NOTE: this needs some examination in the code where this is needed
* and if we have the verkle root present
* @returns {Promise<Uint8Array>} - Returns the verkle root of the `StateManager`
* Gets the cache state root.
* This is used to persist the stateRoot between blocks, so that blocks can retrieve the stateRoot of the parent block.
* This is required to verify and prove verkle execution witnesses.
* @returns {Promise<Uint8Array>} - Returns the cached state root
*/
async getStateRoot(): Promise<Uint8Array> {
return new Uint8Array(0)
if (this._cachedStateRoot === undefined) {
throw new Error('Cache state root missing')
}
return this._cachedStateRoot
}

/**
* TODO: needed?
* Maybe in this context: reset to original pre state suffice
* @param stateRoot - The verkle root to reset the instance to
* Sets the cache state root.
* This is used to persist the stateRoot between blocks, so that blocks can retrieve the stateRoot of the parent block.
* @param stateRoot - The stateRoot to set
*/
async setStateRoot(_: Uint8Array): Promise<void> {}
async setStateRoot(stateRoot: Uint8Array): Promise<void> {
this._cachedStateRoot = stateRoot
}

/**
* Dumps the RLP-encoded storage values for an `account` specified by `address`.
Expand Down
3 changes: 2 additions & 1 deletion packages/verkle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
},
"dependencies": {
"lru-cache": "10.1.0",
"verkle-cryptography-wasm": "^0.4.1",
"verkle-cryptography-wasm": "^0.4.2",
"@ethereumjs/block": "^5.2.0",
"@ethereumjs/rlp": "^5.0.2",
"@ethereumjs/util": "^9.0.3"
},
Expand Down
20 changes: 20 additions & 0 deletions packages/verkle/src/util/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
type Address,
bigIntToBytes,
bytesToHex,
int32ToBytes,
setLengthLeft,
setLengthRight,
} from '@ethereumjs/util'

import type { VerkleExecutionWitness } from '@ethereumjs/block'
import type { VerkleCrypto } from 'verkle-cryptography-wasm'

/**
Expand Down Expand Up @@ -35,4 +37,22 @@ export function getStem(
return treeStem
}

/**
* Verifies that the executionWitness is valid for the given prestateRoot.
* @param ffi The verkle ffi object from verkle-crypotography-wasm.
* @param prestateRoot The prestateRoot matching the executionWitness.
* @param executionWitness The verkle execution witness.
* @returns {boolean} Whether or not the executionWitness belongs to the prestateRoot.
*/
export function verifyProof(
ffi: VerkleCrypto,
prestateRoot: Uint8Array,
executionWitness: VerkleExecutionWitness
): boolean {
return ffi.verifyExecutionWitnessPreState(
bytesToHex(prestateRoot),
JSON.stringify(executionWitness)
)
}

export const POINT_IDENTITY = new Uint8Array(0)
23 changes: 21 additions & 2 deletions packages/verkle/test/crypto.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Address, bytesToHex } from '@ethereumjs/util'
import { Address, bytesToHex, hexToBytes, randomBytes } from '@ethereumjs/util'
import { loadVerkleCrypto } from 'verkle-cryptography-wasm'
import { assert, beforeAll, describe, it } from 'vitest'

import { getStem } from '../src/index.js'
import * as verkleBlockJSON from '../../statemanager/test/testdata/verkleKaustinen6Block72.json'
import { getStem, verifyProof } from '../src/index.js'

import type { VerkleCrypto } from '../src/index.js'
import type { VerkleExecutionWitness } from '@ethereumjs/block'

describe('Verkle cryptographic helpers', () => {
let verkle: VerkleCrypto
Expand All @@ -25,4 +27,21 @@ describe('Verkle cryptographic helpers', () => {
'0x1540dfad7755b40be0768c6aa0a5096fbf0215e0e8cf354dd928a178346466'
)
})

it('verifyProof(): should verify verkle proofs', () => {
// Src: Kaustinen6 testnet, block 71 state root (parent of block 72)
const prestateRoot = hexToBytes(
'0x64e1a647f42e5c2e3c434531ccf529e1b3e93363a40db9fc8eec81f492123510'
)
const executionWitness = verkleBlockJSON.executionWitness as VerkleExecutionWitness
assert.isTrue(verifyProof(verkle, prestateRoot, executionWitness))
})

it('verifyProof(): should return false for invalid verkle proofs', () => {
// Random preStateRoot
const prestateRoot = randomBytes(32)
const executionWitness = verkleBlockJSON.executionWitness as VerkleExecutionWitness
// Modify the proof to make it invalid
assert.isFalse(verifyProof(verkle, prestateRoot, executionWitness))
})
})

0 comments on commit c95499c

Please sign in to comment.