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

Proof function util helpers refactor #3551

Merged
merged 12 commits into from
Jul 31, 2024
20 changes: 14 additions & 6 deletions packages/statemanager/src/stateManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { Chain, Common } from '@ethereumjs/common'
import { RLP } from '@ethereumjs/rlp'
import { Trie, createTrieFromProof, verifyTrieProof } from '@ethereumjs/trie'
import {
Trie,
createProof,
createTrieFromProof,
updateFromProof,
verifyTrieProof,
} from '@ethereumjs/trie'
import {
Account,
KECCAK256_NULL,
Expand Down Expand Up @@ -629,19 +635,19 @@ export class DefaultStateManager implements StateManagerInterface {
codeHash: KECCAK256_NULL_S,
nonce: '0x0',
storageHash: KECCAK256_RLP_S,
accountProof: (await this._trie.createProof(address.bytes)).map((p) => bytesToHex(p)),
accountProof: (await createProof(this._trie, address.bytes)).map((p) => bytesToHex(p)),
storageProof: [],
}
return returnValue
}
const accountProof: PrefixedHexString[] = (await this._trie.createProof(address.bytes)).map(
const accountProof: PrefixedHexString[] = (await createProof(this._trie, address.bytes)).map(
(p) => bytesToHex(p),
)
const storageProof: StorageProof[] = []
const storageTrie = this._getStorageTrie(address, account)

for (const storageKey of storageSlots) {
const proof = (await storageTrie.createProof(storageKey)).map((p) => bytesToHex(p))
const proof = (await createProof(storageTrie, storageKey)).map((p) => bytesToHex(p))
const value = bytesToHex(await this.getStorage(address, storageKey))
const proofItem: StorageProof = {
key: bytesToHex(storageKey),
Expand Down Expand Up @@ -717,7 +723,8 @@ export class DefaultStateManager implements StateManagerInterface {
const trie = this._getStorageTrie(address)
trie.root(hexToBytes(storageHash))
for (let i = 0; i < storageProof.length; i++) {
await trie.updateFromProof(
await updateFromProof(
trie,
storageProof[i].proof.map((e) => hexToBytes(e)),
safe,
)
Expand All @@ -733,7 +740,8 @@ export class DefaultStateManager implements StateManagerInterface {
async addProofData(proof: Proof | Proof[], safe: boolean = false) {
if (Array.isArray(proof)) {
for (let i = 0; i < proof.length; i++) {
await this._trie.updateFromProof(
await updateFromProof(
this._trie,
proof[i].accountProof.map((e) => hexToBytes(e)),
safe,
)
Expand Down
8 changes: 4 additions & 4 deletions packages/trie/examples/createFromProof.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Trie, createTrieFromProof } from '@ethereumjs/trie'
import { Trie, createProof, createTrieFromProof, updateFromProof } from '@ethereumjs/trie'
import { bytesToUtf8, utf8ToBytes } from '@ethereumjs/util'

async function main() {
Expand All @@ -9,12 +9,12 @@ async function main() {
await someOtherTrie.put(k1, utf8ToBytes('valueOne'))
await someOtherTrie.put(k2, utf8ToBytes('valueTwo'))

const proof = await someOtherTrie.createProof(k1)
const proof = await createProof(someOtherTrie, k1)
const trie = await createTrieFromProof(proof, { useKeyHashing: true })
const otherProof = await someOtherTrie.createProof(k2)
const otherProof = await createProof(someOtherTrie, k2)

// To add more proofs to the trie, use `updateFromProof`
await trie.updateFromProof(otherProof)
await updateFromProof(trie, otherProof)

const value = await trie.get(k1)
console.log(bytesToUtf8(value!)) // valueOne
Expand Down
6 changes: 3 additions & 3 deletions packages/trie/examples/logDemo.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Run with DEBUG=ethjs,trie:* to see debug log ouput
*/
import { Trie } from '@ethereumjs/trie'
import { Trie, createProof, verifyProof } from '@ethereumjs/trie'
import { utf8ToBytes } from '@ethereumjs/util'

const trie_entries: [string, string | null][] = [
Expand All @@ -22,8 +22,8 @@ const main = async () => {
for (const [key, value] of trie_entries) {
await trie.put(utf8ToBytes(key), value === null ? Uint8Array.from([]) : utf8ToBytes(value))
}
const proof = await trie.createProof(utf8ToBytes('doge'))
const valid = await trie.verifyProof(trie.root(), utf8ToBytes('doge'), proof)
const proof = await createProof(trie, utf8ToBytes('doge'))
const valid = await verifyProof(trie, trie.root(), utf8ToBytes('doge'), proof)
console.log('valid', valid)
}

Expand Down
14 changes: 7 additions & 7 deletions packages/trie/examples/proofs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Trie } from '@ethereumjs/trie'
import { Trie, createProof, verifyProof } from '@ethereumjs/trie'
import { bytesToUtf8, utf8ToBytes } from '@ethereumjs/util'

const trie = new Trie()
Expand All @@ -11,24 +11,24 @@ async function main() {

// proof-of-inclusion
await trie.put(k1, v1)
let proof = await trie.createProof(k1)
let value = await trie.verifyProof(trie.root(), k1, proof)
let proof = await createProof(trie, k1)
let value = await verifyProof(trie, trie.root(), k1, proof)
console.log(value ? bytesToUtf8(value) : 'not found') // 'one'

// proof-of-exclusion
await trie.put(k1, v1)
await trie.put(k2, v2)
proof = await trie.createProof(utf8ToBytes('key3'))
value = await trie.verifyProof(trie.root(), utf8ToBytes('key3'), proof)
proof = await createProof(trie, utf8ToBytes('key3'))
value = await verifyProof(trie, trie.root(), utf8ToBytes('key3'), proof)
console.log(value ? bytesToUtf8(value) : 'null') // null

// invalid proof
await trie.put(k1, v1)
await trie.put(k2, v2)
proof = await trie.createProof(k2)
proof = await createProof(trie, k2)
proof[0].reverse()
try {
const _value = await trie.verifyProof(trie.root(), k2, proof) // results in error
const _value = await verifyProof(trie, trie.root(), k2, proof) // results in error
} catch (err) {
console.log(err)
}
Expand Down
4 changes: 2 additions & 2 deletions packages/trie/src/constructors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import { keccak256 } from 'ethereum-cryptography/keccak'
import { concatBytes } from 'ethereum-cryptography/utils'

import { ROOT_DB_KEY, Trie } from './index.js'
import { ROOT_DB_KEY, Trie, updateFromProof } from './index.js'

import type { Proof, TrieOpts } from './index.js'

Expand Down Expand Up @@ -63,7 +63,7 @@ export async function createTrie(opts?: TrieOpts) {
export async function createTrieFromProof(proof: Proof, trieOpts?: TrieOpts) {
const shouldVerifyRoot = trieOpts?.root !== undefined
const trie = new Trie(trieOpts)
const root = await trie.updateFromProof(proof, shouldVerifyRoot)
const root = await updateFromProof(trie, proof, shouldVerifyRoot)
trie.root(root)
await trie.persistRoot()
return trie
Expand Down
109 changes: 105 additions & 4 deletions packages/trie/src/proof/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { bytesToHex, concatBytes, equalsBytes } from '@ethereumjs/util'
import { keccak256 } from 'ethereum-cryptography/keccak'

import { createTrieFromProof } from '../constructors.js'
import { verifyRangeProof } from '../index.js'
import { Trie, verifyRangeProof } from '../index.js'
import { bytesToNibbles } from '../util/nibbles.js'

import type { Proof, TrieOpts } from '../index.js'
import type { PutBatch } from '@ethereumjs/util'

/**
* Static version of verifyProof function with the same behavior. An (EIP-1186)[https://eips.ethereum.org/EIPS/eip-1186] proof contains the encoded trie nodes
* An (EIP-1186)[https://eips.ethereum.org/EIPS/eip-1186] proof contains the encoded trie nodes
* from the root node to the leaf node storing state data.
* @param rootHash Root hash of the trie that this proof was created from and is being verified for
* @param key Key that is being verified and that the proof is created for
Expand All @@ -33,8 +35,7 @@ export async function verifyTrieProof(
// /**
// * A range proof is a proof that includes the encoded trie nodes from the root node to leaf node for one or more branches of a trie,
// * allowing an entire range of leaf nodes to be validated. This is useful in applications such as snap sync where contiguous ranges
// * of state trie data is received and validated for constructing world state, locally. Also see {@link verifyRangeProof}. A static
// * version of this function also exists.
// * of state trie data is received and validated for constructing world state, locally. Also see {@link verifyRangeProof}.
// * @param rootHash - root hash of state trie this proof is being verified against.
// * @param firstKey - first key of range being proven.
// * @param lastKey - last key of range being proven.
Expand Down Expand Up @@ -64,4 +65,104 @@ export function verifyTrieRangeProof(
)
}

/**
* Creates a proof from a trie and key that can be verified using {@link verifyTrieProof}. An (EIP-1186)[https://eips.ethereum.org/EIPS/eip-1186] proof contains
* the encoded trie nodes from the root node to the leaf node storing state data. The returned proof will be in the format of an array that contains Uint8Arrays of
* serialized branch, extension, and/or leaf nodes.
* @param key key to create a proof for
*/
export async function createProof(trie: Trie, key: Uint8Array): Promise<Proof> {
trie['DEBUG'] && trie['debug'](`Creating Proof for Key: ${bytesToHex(key)}`, ['CREATE_PROOF'])
const { stack } = await trie.findPath(trie['appliedKey'](key))
const p = stack.map((stackElem) => {
return stackElem.serialize()
})
trie['DEBUG'] && trie['debug'](`Proof created with (${stack.length}) nodes`, ['CREATE_PROOF'])
return p
}

/**
* Updates a trie from a proof by putting all the nodes in the proof into the trie. Pass {@param shouldVerifyRoot} as true to check
* that root key of proof matches root of trie and throw if not.
* An (EIP-1186)[https://eips.ethereum.org/EIPS/eip-1186] proof contains the encoded trie nodes from the root node to the leaf node storing state data.
* @param proof An (EIP-1186)[https://eips.ethereum.org/EIPS/eip-1186] proof to update the trie from.
* @param shouldVerifyRoot - defaults to false. If `true`, verifies that the root key of the proof matches the trie root and throws if not (i.e invalid proof).
* @returns The root of the proof
*/
export async function updateFromProof(trie: Trie, proof: Proof, shouldVerifyRoot: boolean = false) {
trie['DEBUG'] && trie['debug'](`Saving (${proof.length}) proof nodes in DB`, ['FROM_PROOF'])
const opStack = proof.map((nodeValue) => {
let key = Uint8Array.from(trie['hash'](nodeValue))
key = trie['_opts'].keyPrefix ? concatBytes(trie['_opts'].keyPrefix, key) : key
return {
type: 'put',
key,
value: nodeValue,
} as PutBatch
})

if (shouldVerifyRoot) {
if (opStack[0] !== undefined && opStack[0] !== null) {
if (!equalsBytes(trie.root(), opStack[0].key)) {
throw new Error('The provided proof does not have the expected trie root')
}
}
}

await trie['_db'].batch(opStack)
if (opStack[0] !== undefined) {
return opStack[0].key
}
}

/**
* Verifies a proof by putting all of its nodes into a trie and attempting to get the proven key. An (EIP-1186)[https://eips.ethereum.org/EIPS/eip-1186] proof
* contains the encoded trie nodes from the root node to the leaf node storing state data.
* @param rootHash Root hash of the trie that this proof was created from and is being verified for
* @param key Key that is being verified and that the proof is created for
* @param proof an EIP-1186 proof to verify the key against
* @throws If proof is found to be invalid.
* @returns The value from the key, or null if valid proof of non-existence.
*/
export async function verifyProof(
trie: Trie,
rootHash: Uint8Array,
key: Uint8Array,
proof: Proof,
): Promise<Uint8Array | null> {
trie['DEBUG'] &&
trie['debug'](
`Verifying Proof:\n|| Key: ${bytesToHex(key)}\n|| Root: ${bytesToHex(
rootHash,
)}\n|| Proof: (${proof.length}) nodes
`,
['VERIFY_PROOF'],
)
const proofTrie = new Trie({
root: rootHash,
useKeyHashingFunction: trie['_opts'].useKeyHashingFunction,
common: trie['_opts'].common,
})
try {
await updateFromProof(proofTrie, proof, true)
} catch (e: any) {
throw new Error('Invalid proof nodes given')
}
try {
trie['DEBUG'] &&
trie['debug'](`Verifying proof by retrieving key: ${bytesToHex(key)} from proof trie`, [
'VERIFY_PROOF',
])
const value = await proofTrie.get(trie['appliedKey'](key), true)
trie['DEBUG'] && trie['debug'](`PROOF VERIFIED`, ['VERIFY_PROOF'])
return value
} catch (err: any) {
if (err.message === 'Missing node in DB') {
throw new Error('Invalid proof provided')
} else {
throw err
}
}
}

export * from './range.js'
Loading
Loading