From d7a9ff2104280579acf225873f867a57fc22f7ac Mon Sep 17 00:00:00 2001 From: Scorbajio Date: Tue, 17 Sep 2024 04:33:49 -0700 Subject: [PATCH] Statemanager proof function tree shake (#3672) * Move proof functions from Merkle sm over to being standalone functions * Remove unneeded parameter from fromMerkleStateProof * Export merkle sm proof functions * Refactor tests to use new standalone sm proof functions * stateManager: Fix linting issues * Make debugger a class field * Move proof functions from verkle sm over to being standalone functions * Remove getProof and verifyProof for the stateless verkle sm * Remove unused accountExists function on RPCSm * Remove accountExists function on RPCStateeManager * Move getProof function of RPCStateManager over to being standalone * Export proof functions from statemanager package * Fix test * Fix and reembed example * Fix getProof rpc method by checking which proof function to use for given sm * Remove getProof and verifyProof from sm interface and fix errors * Fix linting issues * Fix lint issue --------- Co-authored-by: Holger Drewes --- packages/client/src/rpc/modules/eth.ts | 19 +- packages/common/src/interfaces.ts | 2 - packages/statemanager/README.md | 26 +- .../examples/fromProofInstantiation.ts | 18 +- packages/statemanager/src/index.ts | 1 + .../statemanager/src/merkleStateManager.ts | 235 +--------------- packages/statemanager/src/proofs/index.ts | 3 + packages/statemanager/src/proofs/merkle.ts | 259 ++++++++++++++++++ packages/statemanager/src/proofs/rpc.ts | 24 ++ packages/statemanager/src/proofs/verkle.ts | 26 ++ packages/statemanager/src/rpcStateManager.ts | 45 +-- .../src/statefulVerkleStateManager.ts | 7 - .../src/statelessVerkleStateManager.ts | 72 ++--- .../test/proofStateManager.spec.ts | 38 +-- .../statemanager/test/rpcStateManager.spec.ts | 13 +- .../statemanager/test/stateManager.spec.ts | 45 +-- packages/vm/src/runBlock.ts | 6 +- 17 files changed, 454 insertions(+), 385 deletions(-) create mode 100644 packages/statemanager/src/proofs/index.ts create mode 100644 packages/statemanager/src/proofs/merkle.ts create mode 100644 packages/statemanager/src/proofs/rpc.ts create mode 100644 packages/statemanager/src/proofs/verkle.ts diff --git a/packages/client/src/rpc/modules/eth.ts b/packages/client/src/rpc/modules/eth.ts index 48a58e9f36..d34c4434f8 100644 --- a/packages/client/src/rpc/modules/eth.ts +++ b/packages/client/src/rpc/modules/eth.ts @@ -1,5 +1,11 @@ import { createBlock } from '@ethereumjs/block' import { Hardfork } from '@ethereumjs/common' +import { + MerkleStateManager, + StatelessVerkleStateManager, + getMerkleStateProof, + getVerkleStateProof, +} from '@ethereumjs/statemanager' import { Capability, createBlob4844TxFromSerializedNetworkWrapper, @@ -1246,14 +1252,19 @@ export class Eth { const vm = await this._vm.shallowCopy() - if (!('getProof' in vm.stateManager)) { - throw new Error('getProof RPC method not supported with the StateManager provided') - } await vm.stateManager.setStateRoot(block.header.stateRoot) const address = createAddressFromString(addressHex) const slots = slotsHex.map((slotHex) => setLengthLeft(hexToBytes(slotHex), 32)) - const proof = await vm.stateManager.getProof!(address, slots) + let proof: Proof + if (vm.stateManager instanceof MerkleStateManager) { + proof = await getMerkleStateProof(vm.stateManager, address, slots) + } else if (vm.stateManager instanceof StatelessVerkleStateManager) { + proof = await getVerkleStateProof(vm.stateManager, address, slots) + } else { + throw new Error('getProof RPC method not supported with the StateManager provided') + } + for (const p of proof.storageProof) { p.key = bigIntToHex(BigInt(p.key)) } diff --git a/packages/common/src/interfaces.ts b/packages/common/src/interfaces.ts index 7accea4b60..792663a646 100644 --- a/packages/common/src/interfaces.ts +++ b/packages/common/src/interfaces.ts @@ -150,7 +150,6 @@ export interface StateManagerInterface { * on usage (check for existence) */ // Client RPC - getProof?(address: Address, storageSlots: Uint8Array[]): Promise dumpStorage?(address: Address): Promise dumpStorageRange?(address: Address, startKey: bigint, limit: number): Promise @@ -169,7 +168,6 @@ export interface StateManagerInterface { executionWitness?: VerkleExecutionWitness | null, accessWitness?: AccessWitnessInterface, ): void - verifyVerkleProof?(): boolean verifyPostState?(): boolean checkChunkWitnessPresent?(contract: Address, programCounter: number): Promise getAppliedKey?(address: Uint8Array): Uint8Array // only for preimages diff --git a/packages/statemanager/README.md b/packages/statemanager/README.md index 05b5bdf9bf..65bf9cab93 100644 --- a/packages/statemanager/README.md +++ b/packages/statemanager/README.md @@ -39,11 +39,11 @@ It also includes a checkpoint/revert/commit mechanism to either persist or rever ```ts // ./examples/basicUsage.ts -import { DefaultStateManager } from '@ethereumjs/statemanager' +import { MerkleStateManager } from '@ethereumjs/statemanager' import { Account, Address, hexToBytes } from '@ethereumjs/util' const main = async () => { - const stateManager = new DefaultStateManager() + const stateManager = new MerkleStateManager() const address = new Address(hexToBytes('0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b')) const account = new Account(BigInt(0), BigInt(1000)) await stateManager.checkpoint() @@ -106,12 +106,17 @@ See below example for common usage: ```ts // ./examples/fromProofInstantiation.ts -import { DefaultStateManager } from '@ethereumjs/statemanager' +import { + MerkleStateManager, + getMerkleStateProof, + fromMerkleStateProof, + addMerkleStateProofData, +} from '@ethereumjs/statemanager' import { Address, hexToBytes } from '@ethereumjs/util' const main = async () => { // setup `stateManager` with some existing address - const stateManager = new DefaultStateManager() + const stateManager = new MerkleStateManager() const contractAddress = new Address(hexToBytes('0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b')) const byteCode = hexToBytes('0x67ffffffffffffffff600160006000fb') const storageKey1 = hexToBytes( @@ -127,12 +132,15 @@ const main = async () => { await stateManager.putStorage(contractAddress, storageKey1, storageValue1) await stateManager.putStorage(contractAddress, storageKey2, storageValue2) - const proof = await stateManager.getProof(contractAddress) - const proofWithStorage = await stateManager.getProof(contractAddress, [storageKey1, storageKey2]) - const partialStateManager = await DefaultStateManager.fromProof(proof) + const proof = await getMerkleStateProof(stateManager, contractAddress) + const proofWithStorage = await getMerkleStateProof(stateManager, contractAddress, [ + storageKey1, + storageKey2, + ]) + const partialStateManager = await fromMerkleStateProof(proof) // To add more proof data, use `addProofData` - await partialStateManager.addProofData(proofWithStorage) + await addMerkleStateProofData(partialStateManager, proofWithStorage) console.log(await partialStateManager.getCode(contractAddress)) // contract bytecode is not included in proof console.log(await partialStateManager.getStorage(contractAddress, storageKey1), storageValue1) // should match console.log(await partialStateManager.getStorage(contractAddress, storageKey2), storageValue2) // should match @@ -197,7 +205,7 @@ const main = async () => { const state = new RPCStateManager({ provider, blockTag }) const evm = await createEVM({ blockchain, stateManager: state }) // note that evm is ready to run BLOCKHASH opcodes (over RPC) } catch (e) { - console.log(e.message) // fetch would fail because provider url is not real. please replace provider with a valid rpc url string. + console.log(e.message) // fetch would fail because provider url is not real. please replace provider with a valid RPC url string. } } void main() diff --git a/packages/statemanager/examples/fromProofInstantiation.ts b/packages/statemanager/examples/fromProofInstantiation.ts index 581bfd5773..6d14ebc1dd 100644 --- a/packages/statemanager/examples/fromProofInstantiation.ts +++ b/packages/statemanager/examples/fromProofInstantiation.ts @@ -1,4 +1,9 @@ -import { MerkleStateManager } from '@ethereumjs/statemanager' +import { + MerkleStateManager, + addMerkleStateProofData, + fromMerkleStateProof, + getMerkleStateProof, +} from '@ethereumjs/statemanager' import { Address, hexToBytes } from '@ethereumjs/util' const main = async () => { @@ -19,12 +24,15 @@ const main = async () => { await stateManager.putStorage(contractAddress, storageKey1, storageValue1) await stateManager.putStorage(contractAddress, storageKey2, storageValue2) - const proof = await stateManager.getProof(contractAddress) - const proofWithStorage = await stateManager.getProof(contractAddress, [storageKey1, storageKey2]) - const partialStateManager = await MerkleStateManager.fromProof(proof) + const proof = await getMerkleStateProof(stateManager, contractAddress) + const proofWithStorage = await getMerkleStateProof(stateManager, contractAddress, [ + storageKey1, + storageKey2, + ]) + const partialStateManager = await fromMerkleStateProof(proof) // To add more proof data, use `addProofData` - await partialStateManager.addProofData(proofWithStorage) + await addMerkleStateProofData(partialStateManager, proofWithStorage) console.log(await partialStateManager.getCode(contractAddress)) // contract bytecode is not included in proof console.log(await partialStateManager.getStorage(contractAddress, storageKey1), storageValue1) // should match console.log(await partialStateManager.getStorage(contractAddress, storageKey2), storageValue2) // should match diff --git a/packages/statemanager/src/index.ts b/packages/statemanager/src/index.ts index c8e3fd0dd0..ae9a906743 100644 --- a/packages/statemanager/src/index.ts +++ b/packages/statemanager/src/index.ts @@ -1,6 +1,7 @@ export * from './accessWitness.js' export * from './cache/index.js' export * from './merkleStateManager.js' +export * from './proofs/index.js' export * from './rpcStateManager.js' export * from './simpleStateManager.js' export * from './statefulVerkleStateManager.js' diff --git a/packages/statemanager/src/merkleStateManager.ts b/packages/statemanager/src/merkleStateManager.ts index 01092819a3..f3225a2d09 100644 --- a/packages/statemanager/src/merkleStateManager.ts +++ b/packages/statemanager/src/merkleStateManager.ts @@ -1,20 +1,8 @@ import { Common, Mainnet } from '@ethereumjs/common' import { RLP } from '@ethereumjs/rlp' -import { - Trie, - createMerkleProof, - createTrieFromProof, - updateTrieFromMerkleProof, - verifyTrieProof, -} from '@ethereumjs/trie' +import { Trie } from '@ethereumjs/trie' import { Account, - KECCAK256_NULL, - KECCAK256_NULL_S, - KECCAK256_RLP, - KECCAK256_RLP_S, - bigIntToHex, - bytesToHex, bytesToUnprefixedHex, concatBytes, createAccount, @@ -22,7 +10,6 @@ import { createAddressFromString, equalsBytes, hexToBytes, - setLengthLeft, short, toBytes, unpadBytes, @@ -37,15 +24,14 @@ import { modifyAccountFields } from './util.js' import { type MerkleStateManagerOpts } from './index.js' -import type { Caches, StorageProof } from './index.js' +import type { Caches } from './index.js' import type { AccountFields, - Proof, StateManagerInterface, StorageDump, StorageRange, } from '@ethereumjs/common' -import type { Address, DB, PrefixedHexString } from '@ethereumjs/util' +import type { Address, DB } from '@ethereumjs/util' import type { Debugger } from 'debug' /** @@ -552,221 +538,6 @@ export class MerkleStateManager implements StateManagerInterface { } } - /** - * Get an EIP-1186 proof - * @param address address to get proof of - * @param storageSlots storage slots to get proof of - */ - async getProof(address: Address, storageSlots: Uint8Array[] = []): Promise { - await this.flush() - const account = await this.getAccount(address) - if (!account) { - const returnValue: Proof = { - address: address.toString(), - balance: '0x0', - codeHash: KECCAK256_NULL_S, - nonce: '0x0', - storageHash: KECCAK256_RLP_S, - accountProof: (await createMerkleProof(this._trie, address.bytes)).map((p) => - bytesToHex(p), - ), - storageProof: [], - } - return returnValue - } - const accountProof: PrefixedHexString[] = ( - await createMerkleProof(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 createMerkleProof(storageTrie, storageKey)).map((p) => bytesToHex(p)) - const value = bytesToHex(await this.getStorage(address, storageKey)) - const proofItem: StorageProof = { - key: bytesToHex(storageKey), - value: value === '0x' ? '0x0' : value, // Return '0x' values as '0x0' since this is a JSON RPC response - proof, - } - storageProof.push(proofItem) - } - - const returnValue: Proof = { - address: address.toString(), - balance: bigIntToHex(account.balance), - codeHash: bytesToHex(account.codeHash), - nonce: bigIntToHex(account.nonce), - storageHash: bytesToHex(account.storageRoot), - accountProof, - storageProof, - } - return returnValue - } - - /** - * Create a StateManager and initialize this with proof(s) gotten previously from getProof - * This generates a (partial) StateManager where one can retrieve all items from the proof - * @param proof Either a proof retrieved from `getProof`, or an array of those proofs - * @param safe Whether or not to verify that the roots of the proof items match the reported roots - * @param opts a dictionary of StateManager opts - * @returns A new MerkleStateManager with elements from the given proof included in its backing state trie - */ - static async fromProof( - proof: Proof | Proof[], - safe: boolean = false, - opts: MerkleStateManagerOpts = {}, - ): Promise { - if (Array.isArray(proof)) { - if (proof.length === 0) { - return new MerkleStateManager(opts) - } else { - const trie = - opts.trie ?? - (await createTrieFromProof( - proof[0].accountProof.map((e) => hexToBytes(e)), - { useKeyHashing: true }, - )) - const sm = new MerkleStateManager({ ...opts, trie }) - const address = createAddressFromString(proof[0].address) - await sm.addStorageProof(proof[0].storageProof, proof[0].storageHash, address, safe) - for (let i = 1; i < proof.length; i++) { - const proofItem = proof[i] - await sm.addProofData(proofItem, true) - } - await sm.flush() // TODO verify if this is necessary - return sm - } - } else { - return MerkleStateManager.fromProof([proof], safe, opts) - } - } - - /** - * Adds a storage proof to the state manager - * @param storageProof The storage proof - * @param storageHash The root hash of the storage trie - * @param address The address - * @param safe Whether or not to verify if the reported roots match the current storage root - */ - private async addStorageProof( - storageProof: StorageProof[], - storageHash: PrefixedHexString, - address: Address, - safe: boolean = false, - ) { - const trie = this._getStorageTrie(address) - trie.root(hexToBytes(storageHash)) - for (let i = 0; i < storageProof.length; i++) { - await updateTrieFromMerkleProof( - trie, - storageProof[i].proof.map((e) => hexToBytes(e)), - safe, - ) - } - } - - /** - * Add proof(s) into an already existing trie - * @param proof The proof(s) retrieved from `getProof` - * @param verifyRoot verify that all proof root nodes match statemanager's stateroot - should be - * set to `false` when constructing a state manager where the underlying trie has proof nodes from different state roots - */ - async addProofData(proof: Proof | Proof[], safe: boolean = false) { - if (Array.isArray(proof)) { - for (let i = 0; i < proof.length; i++) { - await updateTrieFromMerkleProof( - this._trie, - proof[i].accountProof.map((e) => hexToBytes(e)), - safe, - ) - await this.addStorageProof( - proof[i].storageProof, - proof[i].storageHash, - createAddressFromString(proof[i].address), - safe, - ) - } - } else { - await this.addProofData([proof], safe) - } - } - - /** - * Verify an EIP-1186 proof. Throws if proof is invalid, otherwise returns true. - * @param proof the proof to prove - */ - async verifyProof(proof: Proof): Promise { - const key = hexToBytes(proof.address) - const accountProof = proof.accountProof.map((rlpString: PrefixedHexString) => - hexToBytes(rlpString), - ) - - // This returns the account if the proof is valid. - // Verify that it matches the reported account. - const value = await verifyTrieProof(key, accountProof, { - useKeyHashing: true, - }) - - if (value === null) { - // Verify that the account is empty in the proof. - const emptyBytes = new Uint8Array(0) - const notEmptyErrorMsg = 'Invalid proof provided: account is not empty' - const nonce = unpadBytes(hexToBytes(proof.nonce)) - if (!equalsBytes(nonce, emptyBytes)) { - throw new Error(`${notEmptyErrorMsg} (nonce is not zero)`) - } - const balance = unpadBytes(hexToBytes(proof.balance)) - if (!equalsBytes(balance, emptyBytes)) { - throw new Error(`${notEmptyErrorMsg} (balance is not zero)`) - } - const storageHash = hexToBytes(proof.storageHash) - if (!equalsBytes(storageHash, KECCAK256_RLP)) { - throw new Error(`${notEmptyErrorMsg} (storageHash does not equal KECCAK256_RLP)`) - } - const codeHash = hexToBytes(proof.codeHash) - if (!equalsBytes(codeHash, KECCAK256_NULL)) { - throw new Error(`${notEmptyErrorMsg} (codeHash does not equal KECCAK256_NULL)`) - } - } else { - const account = createAccountFromRLP(value) - const { nonce, balance, storageRoot, codeHash } = account - const invalidErrorMsg = 'Invalid proof provided:' - if (nonce !== BigInt(proof.nonce)) { - throw new Error(`${invalidErrorMsg} nonce does not match`) - } - if (balance !== BigInt(proof.balance)) { - throw new Error(`${invalidErrorMsg} balance does not match`) - } - if (!equalsBytes(storageRoot, hexToBytes(proof.storageHash))) { - throw new Error(`${invalidErrorMsg} storageHash does not match`) - } - if (!equalsBytes(codeHash, hexToBytes(proof.codeHash))) { - throw new Error(`${invalidErrorMsg} codeHash does not match`) - } - } - - for (const stProof of proof.storageProof) { - const storageProof = stProof.proof.map((value: PrefixedHexString) => hexToBytes(value)) - const storageValue = setLengthLeft(hexToBytes(stProof.value), 32) - const storageKey = hexToBytes(stProof.key) - const proofValue = await verifyTrieProof(storageKey, storageProof, { - useKeyHashing: true, - }) - const reportedValue = setLengthLeft( - RLP.decode(proofValue ?? new Uint8Array(0)) as Uint8Array, - 32, - ) - if (!equalsBytes(reportedValue, storageValue)) { - throw new Error( - `Reported trie value does not match storage, key: ${stProof.key}, reported: ${bytesToHex( - reportedValue, - )}, actual: ${bytesToHex(storageValue)}`, - ) - } - } - return true - } - /** * Gets the state-root of the Merkle-Patricia trie representation * of the state of this StateManager. Will error if there are uncommitted diff --git a/packages/statemanager/src/proofs/index.ts b/packages/statemanager/src/proofs/index.ts new file mode 100644 index 0000000000..95e416c906 --- /dev/null +++ b/packages/statemanager/src/proofs/index.ts @@ -0,0 +1,3 @@ +export * from './merkle.js' +export * from './rpc.js' +export * from './verkle.js' diff --git a/packages/statemanager/src/proofs/merkle.ts b/packages/statemanager/src/proofs/merkle.ts new file mode 100644 index 0000000000..6cfde8828b --- /dev/null +++ b/packages/statemanager/src/proofs/merkle.ts @@ -0,0 +1,259 @@ +import { RLP } from '@ethereumjs/rlp' +import { + createMerkleProof, + createTrieFromProof, + updateTrieFromMerkleProof, + verifyTrieProof, +} from '@ethereumjs/trie' +import { + KECCAK256_NULL, + KECCAK256_NULL_S, + KECCAK256_RLP, + KECCAK256_RLP_S, + bigIntToHex, + bytesToHex, + createAccountFromRLP, + createAddressFromString, + equalsBytes, + hexToBytes, + setLengthLeft, + unpadBytes, +} from '@ethereumjs/util' + +import { MerkleStateManager } from '../merkleStateManager.js' + +import type { MerkleStateManagerOpts } from '../index.js' +import type { Proof, StorageProof } from '@ethereumjs/common' +import type { Address, PrefixedHexString } from '@ethereumjs/util' + +/** + * Get an EIP-1186 proof + * @param address address to get proof of + * @param storageSlots storage slots to get proof of + */ +export async function getMerkleStateProof( + sm: MerkleStateManager, + address: Address, + storageSlots: Uint8Array[] = [], +): Promise { + await sm['flush']() + const account = await sm.getAccount(address) + if (!account) { + const returnValue: Proof = { + address: address.toString(), + balance: '0x0', + codeHash: KECCAK256_NULL_S, + nonce: '0x0', + storageHash: KECCAK256_RLP_S, + accountProof: (await createMerkleProof(sm['_trie'], address.bytes)).map((p) => bytesToHex(p)), + storageProof: [], + } + return returnValue + } + const accountProof: PrefixedHexString[] = ( + await createMerkleProof(sm['_trie'], address.bytes) + ).map((p) => bytesToHex(p)) + const storageProof: StorageProof[] = [] + const storageTrie = sm['_getStorageTrie'](address, account) + + for (const storageKey of storageSlots) { + const proof = (await createMerkleProof(storageTrie, storageKey)).map((p) => bytesToHex(p)) + const value = bytesToHex(await sm.getStorage(address, storageKey)) + const proofItem: StorageProof = { + key: bytesToHex(storageKey), + value: value === '0x' ? '0x0' : value, // Return '0x' values as '0x0' since this is a JSON RPC response + proof, + } + storageProof.push(proofItem) + } + + const returnValue: Proof = { + address: address.toString(), + balance: bigIntToHex(account.balance), + codeHash: bytesToHex(account.codeHash), + nonce: bigIntToHex(account.nonce), + storageHash: bytesToHex(account.storageRoot), + accountProof, + storageProof, + } + return returnValue +} + +/** + * Adds a storage proof to the state manager + * @param storageProof The storage proof + * @param storageHash The root hash of the storage trie + * @param address The address + * @param safe Whether or not to verify if the reported roots match the current storage root + */ +export async function addMerkleStateStorageProof( + sm: MerkleStateManager, + storageProof: StorageProof[], + storageHash: PrefixedHexString, + address: Address, + safe: boolean = false, +) { + const trie = sm['_getStorageTrie'](address) + trie.root(hexToBytes(storageHash)) + for (let i = 0; i < storageProof.length; i++) { + await updateTrieFromMerkleProof( + trie, + storageProof[i].proof.map((e) => hexToBytes(e)), + safe, + ) + } +} + +/** + * Create a StateManager and initialize this with proof(s) gotten previously from getProof + * This generates a (partial) StateManager where one can retrieve all items from the proof + * @param proof Either a proof retrieved from `getProof`, or an array of those proofs + * @param safe Whether or not to verify that the roots of the proof items match the reported roots + * @param opts a dictionary of StateManager opts + * @returns A new MerkleStateManager with elements from the given proof included in its backing state trie + */ +export async function fromMerkleStateProof( + proof: Proof | Proof[], + safe: boolean = false, + opts: MerkleStateManagerOpts = {}, +): Promise { + if (Array.isArray(proof)) { + if (proof.length === 0) { + return new MerkleStateManager(opts) + } else { + const trie = + opts.trie ?? + (await createTrieFromProof( + proof[0].accountProof.map((e) => hexToBytes(e)), + { useKeyHashing: true }, + )) + const sm = new MerkleStateManager({ ...opts, trie }) + const address = createAddressFromString(proof[0].address) + await addMerkleStateStorageProof( + sm, + proof[0].storageProof, + proof[0].storageHash, + address, + safe, + ) + for (let i = 1; i < proof.length; i++) { + const proofItem = proof[i] + await addMerkleStateProofData(sm, proofItem, true) + } + await sm.flush() // TODO verify if this is necessary + return sm + } + } else { + return fromMerkleStateProof([proof], safe, opts) + } +} + +/** + * Add proof(s) into an already existing trie + * @param proof The proof(s) retrieved from `getProof` + * @param verifyRoot verify that all proof root nodes match statemanager's stateroot - should be + * set to `false` when constructing a state manager where the underlying trie has proof nodes from different state roots + */ +export async function addMerkleStateProofData( + sm: MerkleStateManager, + proof: Proof | Proof[], + safe: boolean = false, +) { + if (Array.isArray(proof)) { + for (let i = 0; i < proof.length; i++) { + await updateTrieFromMerkleProof( + sm['_trie'], + proof[i].accountProof.map((e) => hexToBytes(e)), + safe, + ) + await addMerkleStateStorageProof( + sm, + proof[i].storageProof, + proof[i].storageHash, + createAddressFromString(proof[i].address), + safe, + ) + } + } else { + await addMerkleStateProofData(sm, [proof], safe) + } +} + +/** + * Verify an EIP-1186 proof. Throws if proof is invalid, otherwise returns true. + * @param proof the proof to prove + */ +export async function verifyMerkleStateProof( + sm: MerkleStateManager, + proof: Proof, +): Promise { + const key = hexToBytes(proof.address) + const accountProof = proof.accountProof.map((rlpString: PrefixedHexString) => + hexToBytes(rlpString), + ) + + // This returns the account if the proof is valid. + // Verify that it matches the reported account. + const value = await verifyTrieProof(key, accountProof, { + useKeyHashing: true, + }) + + if (value === null) { + // Verify that the account is empty in the proof. + const emptyBytes = new Uint8Array(0) + const notEmptyErrorMsg = 'Invalid proof provided: account is not empty' + const nonce = unpadBytes(hexToBytes(proof.nonce)) + if (!equalsBytes(nonce, emptyBytes)) { + throw new Error(`${notEmptyErrorMsg} (nonce is not zero)`) + } + const balance = unpadBytes(hexToBytes(proof.balance)) + if (!equalsBytes(balance, emptyBytes)) { + throw new Error(`${notEmptyErrorMsg} (balance is not zero)`) + } + const storageHash = hexToBytes(proof.storageHash) + if (!equalsBytes(storageHash, KECCAK256_RLP)) { + throw new Error(`${notEmptyErrorMsg} (storageHash does not equal KECCAK256_RLP)`) + } + const codeHash = hexToBytes(proof.codeHash) + if (!equalsBytes(codeHash, KECCAK256_NULL)) { + throw new Error(`${notEmptyErrorMsg} (codeHash does not equal KECCAK256_NULL)`) + } + } else { + const account = createAccountFromRLP(value) + const { nonce, balance, storageRoot, codeHash } = account + const invalidErrorMsg = 'Invalid proof provided:' + if (nonce !== BigInt(proof.nonce)) { + throw new Error(`${invalidErrorMsg} nonce does not match`) + } + if (balance !== BigInt(proof.balance)) { + throw new Error(`${invalidErrorMsg} balance does not match`) + } + if (!equalsBytes(storageRoot, hexToBytes(proof.storageHash))) { + throw new Error(`${invalidErrorMsg} storageHash does not match`) + } + if (!equalsBytes(codeHash, hexToBytes(proof.codeHash))) { + throw new Error(`${invalidErrorMsg} codeHash does not match`) + } + } + + for (const stProof of proof.storageProof) { + const storageProof = stProof.proof.map((value: PrefixedHexString) => hexToBytes(value)) + const storageValue = setLengthLeft(hexToBytes(stProof.value), 32) + const storageKey = hexToBytes(stProof.key) + const proofValue = await verifyTrieProof(storageKey, storageProof, { + useKeyHashing: true, + }) + const reportedValue = setLengthLeft( + RLP.decode(proofValue ?? new Uint8Array(0)) as Uint8Array, + 32, + ) + if (!equalsBytes(reportedValue, storageValue)) { + throw new Error( + `Reported trie value does not match storage, key: ${stProof.key}, reported: ${bytesToHex( + reportedValue, + )}, actual: ${bytesToHex(storageValue)}`, + ) + } + } + return true +} diff --git a/packages/statemanager/src/proofs/rpc.ts b/packages/statemanager/src/proofs/rpc.ts new file mode 100644 index 0000000000..78dbe928c0 --- /dev/null +++ b/packages/statemanager/src/proofs/rpc.ts @@ -0,0 +1,24 @@ +import { bytesToHex, fetchFromProvider } from '@ethereumjs/util' + +import type { Proof, RPCStateManager } from '../index.js' +import type { Address } from '@ethereumjs/util' + +/** + * Get an EIP-1186 proof from the provider + * @param address address to get proof of + * @param storageSlots storage slots to get proof of + * @returns an EIP-1186 formatted proof + */ +export async function getRPCStateProof( + sm: RPCStateManager, + address: Address, + storageSlots: Uint8Array[] = [], +): Promise { + if (sm['DEBUG']) sm['_debug'](`retrieving proof from provider for ${address.toString()}`) + const proof = await fetchFromProvider(sm['_provider'], { + method: 'eth_getProof', + params: [address.toString(), storageSlots.map(bytesToHex), sm['_blockTag']], + }) + + return proof +} diff --git a/packages/statemanager/src/proofs/verkle.ts b/packages/statemanager/src/proofs/verkle.ts new file mode 100644 index 0000000000..89bc733434 --- /dev/null +++ b/packages/statemanager/src/proofs/verkle.ts @@ -0,0 +1,26 @@ +import { verifyVerkleProof } from '@ethereumjs/util' + +import type { Proof } from '../index.js' +import type { StatelessVerkleStateManager } from '../statelessVerkleStateManager.js' +import type { Address } from '@ethereumjs/util' + +export function getVerkleStateProof( + sm: StatelessVerkleStateManager, + _: Address, + __: Uint8Array[] = [], +): Promise { + 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 + */ +export function verifyVerkleStateProof(sm: StatelessVerkleStateManager): boolean { + if (sm['_executionWitness'] === undefined) { + sm['DEBUG'] && sm['_debug']('Missing executionWitness') + return false + } + + return verifyVerkleProof(sm.verkleCrypto, sm['_executionWitness']) +} diff --git a/packages/statemanager/src/rpcStateManager.ts b/packages/statemanager/src/rpcStateManager.ts index a22c32366d..2c847ea765 100644 --- a/packages/statemanager/src/rpcStateManager.ts +++ b/packages/statemanager/src/rpcStateManager.ts @@ -1,6 +1,5 @@ import { Common, Mainnet } from '@ethereumjs/common' import { RLP } from '@ethereumjs/rlp' -import { verifyTrieProof } from '@ethereumjs/trie' import { Account, bigIntToHex, @@ -19,9 +18,9 @@ import { keccak256 } from 'ethereum-cryptography/keccak.js' import { Caches, OriginalStorageCache } from './cache/index.js' import { modifyAccountFields } from './util.js' -import type { Proof, RPCStateManagerOpts } from './index.js' +import type { RPCStateManagerOpts } from './index.js' import type { AccountFields, StateManagerInterface, StorageDump } from '@ethereumjs/common' -import type { Address, PrefixedHexString } from '@ethereumjs/util' +import type { Address } from '@ethereumjs/util' import type { Debugger } from 'debug' const KECCAK256_RLP_EMPTY_ACCOUNT = RLP.encode(new Account().serialize()).slice(2) @@ -196,30 +195,6 @@ export class RPCStateManager implements StateManagerInterface { return Promise.resolve(dump) } - /** - * Checks if an `account` exists at `address` - * @param address - Address of the `account` to check - */ - async accountExists(address: Address): Promise { - if (this.DEBUG) this._debug?.(`verify if ${address.toString()} exists`) - - const localAccount = this._caches.account?.get(address) - if (localAccount !== undefined) return true - // Get merkle proof for `address` from provider - const proof = await fetchFromProvider(this._provider, { - method: 'eth_getProof', - params: [address.toString(), [] as any, this._blockTag], - }) - - const proofBuf = proof.accountProof.map((proofNode: PrefixedHexString) => toBytes(proofNode)) - - const verified = await verifyTrieProof(address.bytes, proofBuf, { - useKeyHashing: true, - }) - // if not verified (i.e. verifyProof returns null), account does not exist - return verified === null ? false : true - } - /** * Gets the account associated with `address` or `undefined` if account does not exist * @param address - Address of the `account` to get @@ -319,22 +294,6 @@ export class RPCStateManager implements StateManagerInterface { this._caches.account?.del(address) } - /** - * Get an EIP-1186 proof from the provider - * @param address address to get proof of - * @param storageSlots storage slots to get proof of - * @returns an EIP-1186 formatted proof - */ - async getProof(address: Address, storageSlots: Uint8Array[] = []): Promise { - if (this.DEBUG) this._debug(`retrieving proof from provider for ${address.toString()}`) - const proof = await fetchFromProvider(this._provider, { - method: 'eth_getProof', - params: [address.toString(), storageSlots.map(bytesToHex), this._blockTag], - }) - - return proof - } - /** * Returns the applied key for a given address * Used for saving preimages diff --git a/packages/statemanager/src/statefulVerkleStateManager.ts b/packages/statemanager/src/statefulVerkleStateManager.ts index 99e811ce1e..6bc15969d2 100644 --- a/packages/statemanager/src/statefulVerkleStateManager.ts +++ b/packages/statemanager/src/statefulVerkleStateManager.ts @@ -37,7 +37,6 @@ import type { Caches } from './cache/caches.js' import type { StatefulVerkleStateManagerOpts } from './types.js' import type { AccountFields, - Proof, StateManagerInterface, StorageDump, StorageRange, @@ -455,18 +454,12 @@ export class StatefulVerkleStateManager implements StateManagerInterface { hasStateRoot(_root: Uint8Array): Promise { throw new Error('Method not implemented.') } - getProof?(_address: Address, _storageSlots: Uint8Array[]): Promise { - throw new Error('Method not implemented.') - } dumpStorage?(_address: Address): Promise { throw new Error('Method not implemented.') } dumpStorageRange?(_address: Address, _startKey: bigint, _limit: number): Promise { throw new Error('Method not implemented.') } - verifyVerkleProof?(): boolean { - throw new Error('Method not implemented.') - } clearCaches(): void { throw new Error('Method not implemented.') } diff --git a/packages/statemanager/src/statelessVerkleStateManager.ts b/packages/statemanager/src/statelessVerkleStateManager.ts index 4e4a5dfdf4..56c832b4c1 100644 --- a/packages/statemanager/src/statelessVerkleStateManager.ts +++ b/packages/statemanager/src/statelessVerkleStateManager.ts @@ -20,7 +20,6 @@ import { setLengthRight, short, toBytes, - verifyVerkleProof, } from '@ethereumjs/util' import debugDefault from 'debug' import { keccak256 } from 'ethereum-cryptography/keccak.js' @@ -33,7 +32,7 @@ import type { AccessedStateWithAddress } from './accessWitness.js' import type { Caches } from './cache/index.js' import type { StatelessVerkleStateManagerOpts, VerkleState } from './index.js' import type { MerkleStateManager } from './merkleStateManager.js' -import type { AccountFields, Proof, StateManagerInterface } from '@ethereumjs/common' +import type { AccountFields, StateManagerInterface } from '@ethereumjs/common' import type { Address, PrefixedHexString, @@ -41,8 +40,7 @@ import type { VerkleExecutionWitness, VerkleProof, } from '@ethereumjs/util' - -const debug = debugDefault('statemanager:verkle:stateless') +import type { Debugger } from 'debug' const PUSH_OFFSET = 95 // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -72,11 +70,13 @@ export class StatelessVerkleStateManager implements StateManagerInterface { protected _caches?: Caches + protected _debug: Debugger + /** * StateManager is run in DEBUG mode (default: false) * Taken from DEBUG environment variable * - * Safeguards on debug() calls are added for + * Safeguards on this._debug() calls are added for * performance reasons to avoid string literal evaluation * @hidden */ @@ -111,6 +111,8 @@ export class StatelessVerkleStateManager implements StateManagerInterface { this.keccakFunction = opts.common?.customCrypto.keccak256 ?? keccak256 + this._debug = debugDefault('statemanager:verkle:stateless') + if (opts.verkleCrypto === undefined) { throw new Error('verkle crypto required') } @@ -134,7 +136,7 @@ export class StatelessVerkleStateManager implements StateManagerInterface { this._blockNum = blockNum if (executionWitness === null || executionWitness === undefined) { const errorMsg = `Invalid executionWitness=${executionWitness} for initVerkleExecutionWitness` - debug(errorMsg) + this._debug(errorMsg) throw Error(errorMsg) } @@ -189,8 +191,8 @@ export class StatelessVerkleStateManager implements StateManagerInterface { }, {}) this._postState = postState - debug('initVerkleExecutionWitness preState', this._state) - debug('initVerkleExecutionWitness postState', this._postState) + this._debug('initVerkleExecutionWitness preState', this._state) + this._debug('initVerkleExecutionWitness postState', this._postState) } async checkChunkWitnessPresent(address: Address, codeOffset: number) { @@ -222,7 +224,7 @@ export class StatelessVerkleStateManager implements StateManagerInterface { */ async putCode(address: Address, value: Uint8Array): Promise { if (this.DEBUG) { - debug(`putCode address=${address.toString()} value=${short(value)}`) + this._debug(`putCode address=${address.toString()} value=${short(value)}`) } this._caches?.code?.put(address, value) @@ -246,7 +248,7 @@ export class StatelessVerkleStateManager implements StateManagerInterface { */ async getCode(address: Address): Promise { if (this.DEBUG) { - debug(`getCode address=${address.toString()}`) + this._debug(`getCode address=${address.toString()}`) } const elem = this._caches?.code?.get(address) @@ -275,7 +277,7 @@ export class StatelessVerkleStateManager implements StateManagerInterface { const codeChunk = this._state[chunkKey] if (codeChunk === null) { const errorMsg = `Invalid access to a non existent code chunk with chunkKey=${chunkKey}` - this.DEBUG && debug(errorMsg) + this.DEBUG && this._debug(errorMsg) throw Error(errorMsg) } @@ -306,7 +308,7 @@ export class StatelessVerkleStateManager implements StateManagerInterface { elem.accountRLP !== undefined ? createPartialAccountFromRLP(elem.accountRLP) : undefined if (account === undefined) { const errorMsg = `account=${account} in cache` - this.DEBUG && debug(errorMsg) + this.DEBUG && this._debug(errorMsg) throw Error(errorMsg) } return account.codeSize @@ -404,7 +406,7 @@ export class StatelessVerkleStateManager implements StateManagerInterface { const errorMsg = `Invalid witness for a non existing address=${address} stem=${bytesToHex( stem, )}` - this.DEBUG && debug(errorMsg) + this.DEBUG && this._debug(errorMsg) throw Error(errorMsg) } else { return undefined @@ -416,13 +418,13 @@ export class StatelessVerkleStateManager implements StateManagerInterface { const errorMsg = `Invalid codeHashRaw=${codeHashRaw} for address=${address} chunkKey=${bytesToHex( codeHashKey, )}` - this.DEBUG && debug(errorMsg) + this.DEBUG && this._debug(errorMsg) throw Error(errorMsg) } if (basicDataRaw === undefined && codeHashRaw === undefined) { const errorMsg = `No witness bundled for address=${address} stem=${bytesToHex(stem)}` - this.DEBUG && debug(errorMsg) + this.DEBUG && this._debug(errorMsg) throw Error(errorMsg) } @@ -443,7 +445,7 @@ export class StatelessVerkleStateManager implements StateManagerInterface { }) if (this.DEBUG) { - debug(`getAccount address=${address.toString()} stem=${short(stem)}`) + this._debug(`getAccount address=${address.toString()} stem=${short(stem)}`) } this._caches?.account?.put(address, account, true) @@ -453,7 +455,7 @@ export class StatelessVerkleStateManager implements StateManagerInterface { async putAccount(address: Address, account: Account): Promise { if (this.DEBUG) { - debug(`putAccount address=${address.toString()}`) + this._debug(`putAccount address=${address.toString()}`) } if (this._caches?.account === undefined) { @@ -477,7 +479,7 @@ export class StatelessVerkleStateManager implements StateManagerInterface { */ async deleteAccount(address: Address) { if (this.DEBUG) { - debug(`Delete account ${address}`) + this._debug(`Delete account ${address}`) } this._caches?.deleteAccount(address) @@ -487,23 +489,6 @@ export class StatelessVerkleStateManager implements StateManagerInterface { await modifyAccountFields(this, address, accountFields) } - getProof(_: Address, __: Uint8Array[] = []): Promise { - 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 - */ - verifyVerkleProof(): boolean { - if (this._executionWitness === undefined) { - this.DEBUG && debug('Missing executionWitness') - return false - } - - return verifyVerkleProof(this.verkleCrypto, this._executionWitness) - } - // Verifies that the witness post-state matches the computed post-state verifyPostState(): boolean { // track what all chunks were accessed so as to compare in the end if any chunks were missed @@ -526,7 +511,7 @@ export class StatelessVerkleStateManager implements StateManagerInterface { const computedValue = this.getComputedValue(accessedState) ?? this._preState[chunkKey] if (computedValue === undefined) { this.DEBUG && - debug( + this._debug( `Block accesses missing in canonical address=${address} type=${type} ${extraMeta} chunkKey=${chunkKey}`, ) postFailures++ @@ -537,7 +522,7 @@ export class StatelessVerkleStateManager implements StateManagerInterface { if (canonicalValue === undefined) { this.DEBUG && - debug( + this._debug( `Block accesses missing in canonical address=${address} type=${type} ${extraMeta} chunkKey=${chunkKey}`, ) postFailures++ @@ -571,24 +556,25 @@ export class StatelessVerkleStateManager implements StateManagerInterface { : `${canonicalValue} (${decodedCanonicalValue})` this.DEBUG && - debug( + this._debug( `Block accesses mismatch address=${address} type=${type} ${extraMeta} chunkKey=${chunkKey}`, ) - this.DEBUG && debug(`expected=${displayCanonicalValue}`) - this.DEBUG && debug(`computed=${displayComputedValue}`) + this.DEBUG && this._debug(`expected=${displayCanonicalValue}`) + this.DEBUG && this._debug(`computed=${displayComputedValue}`) postFailures++ } } for (const canChunkKey of Object.keys(this._postState)) { if (accessedChunks.get(canChunkKey) === undefined) { - this.DEBUG && debug(`Missing chunk access for canChunkKey=${canChunkKey}`) + this.DEBUG && this._debug(`Missing chunk access for canChunkKey=${canChunkKey}`) postFailures++ } } const verifyPassed = postFailures === 0 - this.DEBUG && debug(`verifyPostState verifyPassed=${verifyPassed} postFailures=${postFailures}`) + this.DEBUG && + this._debug(`verifyPostState verifyPassed=${verifyPassed} postFailures=${postFailures}`) return verifyPassed } @@ -644,7 +630,7 @@ export class StatelessVerkleStateManager implements StateManagerInterface { const account = createPartialAccountFromRLP(encodedAccount) if (account.isContract()) { const errorMsg = `Code cache not found for address=${address.toString()}` - this.DEBUG && debug(errorMsg) + this.DEBUG && this._debug(errorMsg) throw Error(errorMsg) } else { return null diff --git a/packages/statemanager/test/proofStateManager.spec.ts b/packages/statemanager/test/proofStateManager.spec.ts index 2d129b7f61..c0bb4442ad 100644 --- a/packages/statemanager/test/proofStateManager.spec.ts +++ b/packages/statemanager/test/proofStateManager.spec.ts @@ -16,6 +16,7 @@ import { keccak256 } from 'ethereum-cryptography/keccak.js' import { assert, describe, it } from 'vitest' import { MerkleStateManager } from '../src/index.js' +import { getMerkleStateProof, verifyMerkleStateProof } from '../src/proofs/index.js' import { ropstenContractWithStorageData } from './testdata/ropsten_contractWithStorage.js' import { ropstenNonexistentAccountData } from './testdata/ropsten_nonexistentAccount.js' @@ -29,7 +30,7 @@ describe('ProofStateManager', () => { const key = zeros(32) const stateManager = new MerkleStateManager() - const proof = await stateManager.getProof(address, [key]) + const proof = await getMerkleStateProof(stateManager, address, [key]) assert.equal(proof.balance, '0x0', 'Balance is in quantity-encoded RPC representation') assert.equal(proof.nonce, '0x0', 'Nonce is in quantity-encoded RPC representation') }) @@ -44,7 +45,7 @@ describe('ProofStateManager', () => { await stateManager.putStorage(address, key, new Uint8Array([10])) - const proof = await stateManager.getProof(address, [key]) + const proof = await getMerkleStateProof(stateManager, address, [key]) assert.ok(!equalsBytes(hexToBytes(proof.storageHash), storageRoot)) }) @@ -56,14 +57,14 @@ describe('ProofStateManager', () => { const account = new Account() await stateManager.putAccount(address, account) - const proof = await stateManager.getProof(address, [key]) + const proof = await getMerkleStateProof(stateManager, address, [key]) assert.equal(proof.balance, '0x0', 'Balance is in quantity-encoded RPC representation') assert.equal(proof.nonce, '0x0', 'Nonce is in quantity-encoded RPC representation') account.balance = BigInt(1) await stateManager.putAccount(address, account) - const proof2 = await stateManager.getProof(address, [key]) + const proof2 = await getMerkleStateProof(stateManager, address, [key]) assert.equal(proof2.balance, '0x1', 'Balance correctly encoded') assert.equal(proof2.nonce, '0x0', 'Nonce is in quantity-encoded RPC representation') @@ -71,7 +72,7 @@ describe('ProofStateManager', () => { account.nonce = BigInt(1) await stateManager.putAccount(address, account) - const proof3 = await stateManager.getProof(address, [key]) + const proof3 = await getMerkleStateProof(stateManager, address, [key]) assert.equal(proof3.balance, '0x0', 'Balance is in quantity-encoded RPC representation') assert.equal(proof3.nonce, '0x1', 'Nonce is correctly encoded') }) @@ -97,13 +98,14 @@ describe('ProofStateManager', () => { await stateManager.commit() await stateManager.flush() - const proof = await stateManager.getProof(address, [key]) - assert.ok(await stateManager.verifyProof(proof)) - const nonExistenceProof = await stateManager.getProof( + const proof = await getMerkleStateProof(stateManager, address, [key]) + assert.ok(await verifyMerkleStateProof(stateManager, proof)) + const nonExistenceProof = await getMerkleStateProof( + stateManager, createAddressFromPrivateKey(randomBytes(32)), ) assert.equal( - await stateManager.verifyProof(nonExistenceProof), + await verifyMerkleStateProof(stateManager, nonExistenceProof), true, 'verified proof of non-existence of account', ) @@ -128,9 +130,9 @@ describe('ProofStateManager', () => { await trie['_db'].put(key, bufferData) } trie.root(stateRoot!) - const proof = await stateManager.getProof(address) + const proof = await getMerkleStateProof(stateManager, address) assert.deepEqual(ropstenValidAccountData, proof) - assert.ok(await stateManager.verifyProof(ropstenValidAccountData)) + assert.ok(await verifyMerkleStateProof(stateManager, ropstenValidAccountData)) }) it('should report data equal to geth output for EIP 1178 proofs - nonexistent account', async () => { @@ -152,9 +154,9 @@ describe('ProofStateManager', () => { await trie['_db'].put(key, bufferData) } trie.root(stateRoot!) - const proof = await stateManager.getProof(address) + const proof = await getMerkleStateProof(stateManager, address) assert.deepEqual(ropstenNonexistentAccountData, proof) - assert.ok(await stateManager.verifyProof(ropstenNonexistentAccountData)) + assert.ok(await verifyMerkleStateProof(stateManager, ropstenNonexistentAccountData)) }) it('should report data equal to geth output for EIP 1178 proofs - account with storage', async () => { @@ -190,9 +192,9 @@ describe('ProofStateManager', () => { stateManager['_storageTries'][addressHex] = storageTrie trie.root(stateRoot!) - const proof = await stateManager.getProof(address, storageKeys) + const proof = await getMerkleStateProof(stateManager, address, storageKeys) assert.deepEqual(ropstenContractWithStorageData, proof) - await stateManager.verifyProof(ropstenContractWithStorageData) + await verifyMerkleStateProof(stateManager, ropstenContractWithStorageData) }) it(`should throw on invalid proofs - existing accounts/slots`, async () => { @@ -235,7 +237,7 @@ describe('ProofStateManager', () => { try { ;(testData[tamper as keyof typeof testData] as PrefixedHexString) = `0x9${original.slice(3)}` - await stateManager.verifyProof(testData) + await verifyMerkleStateProof(stateManager, testData) // note: this implicitly means that newField !== original, // if newField === original then the proof would be valid and test would fail assert.fail('should throw') @@ -251,7 +253,7 @@ describe('ProofStateManager', () => { const original = slot.value slot.value = `0x9${original.slice(3)}` try { - await stateManager.verifyProof(testData) + await verifyMerkleStateProof(stateManager, testData) assert.fail('should throw') } catch { assert.ok(true, 'threw on invalid proof') @@ -293,7 +295,7 @@ describe('ProofStateManager', () => { try { const newField = `0x9${original.slice(3)}` testdata[tamper] = newField - await stateManager.verifyProof(testdata) + await verifyMerkleStateProof(stateManager, testdata) // note: this implicitly means that newField !== original, // if newField === original then the proof would be valid and test would fail assert.fail('should throw') diff --git a/packages/statemanager/test/rpcStateManager.spec.ts b/packages/statemanager/test/rpcStateManager.spec.ts index d2fac42c63..310dda9879 100644 --- a/packages/statemanager/test/rpcStateManager.spec.ts +++ b/packages/statemanager/test/rpcStateManager.spec.ts @@ -1,6 +1,7 @@ import { createBlockFromJSONRPCProvider, createBlockFromRPC } from '@ethereumjs/block' import { Common, Hardfork, Mainnet } from '@ethereumjs/common' import { type EVMRunCallOpts, createEVM } from '@ethereumjs/evm' +import { verifyTrieProof } from '@ethereumjs/trie' import { createFeeMarket1559Tx, createTxFromRPC } from '@ethereumjs/tx' import { Address, @@ -12,12 +13,14 @@ import { equalsBytes, hexToBytes, setLengthLeft, + toBytes, utf8ToBytes, } from '@ethereumjs/util' import { createVM, runBlock, runTx } from '@ethereumjs/vm' import { assert, describe, expect, it, vi } from 'vitest' import { MerkleStateManager } from '../src/merkleStateManager.js' +import { getRPCStateProof } from '../src/proofs/index.js' import { RPCBlockChain, RPCStateManager } from '../src/rpcStateManager.js' import { block as blockData } from './testdata/providerData/blocks/block0x7a120.js' @@ -25,6 +28,7 @@ import { getValues } from './testdata/providerData/mockProvider.js' import { tx as txData } from './testdata/providerData/transactions/0xed1960aa7d0d7b567c946d94331dddb37a1c67f51f30bf51f256ea40db88cfb0.js' import type { EVMMockBlockchainInterface } from '@ethereumjs/evm' +import type { PrefixedHexString } from '@ethereumjs/util' const provider = process.env.PROVIDER ?? 'http://cheese' // To run the tests with a live provider, set the PROVIDER environmental variable with a valid provider url @@ -83,9 +87,12 @@ describe('RPC State Manager API tests', () => { ) assert.ok(retrievedVitalikAccount.nonce > 0n, 'Vitalik.eth is stored in cache') - const doesThisAccountExist = await state.accountExists( - createAddressFromString('0xccAfdD642118E5536024675e776d32413728DD07'), - ) + const address = createAddressFromString('0xccAfdD642118E5536024675e776d32413728DD07') + const proof = await getRPCStateProof(state, address) + const proofBuf = proof.accountProof.map((proofNode: PrefixedHexString) => toBytes(proofNode)) + const doesThisAccountExist = await verifyTrieProof(address.bytes, proofBuf, { + useKeyHashing: true, + }) assert.ok(!doesThisAccountExist, 'getAccount returns undefined for non-existent account') assert.ok(state.getAccount(vitalikDotEth) !== undefined, 'vitalik.eth does exist') diff --git a/packages/statemanager/test/stateManager.spec.ts b/packages/statemanager/test/stateManager.spec.ts index 001a02f635..c0783792e3 100644 --- a/packages/statemanager/test/stateManager.spec.ts +++ b/packages/statemanager/test/stateManager.spec.ts @@ -16,6 +16,11 @@ import { import { assert, describe, it } from 'vitest' import { CacheType, Caches, MerkleStateManager } from '../src/index.js' +import { + addMerkleStateProofData, + fromMerkleStateProof, + getMerkleStateProof, +} from '../src/proofs/index.js' import type { PrefixedHexString } from '@ethereumjs/util' @@ -214,15 +219,15 @@ describe('StateManager -> General', () => { stateSetup[addressStr].storageRoot = (await stateManager.getAccount(address)!)?.storageRoot } - const proof1 = await stateManager.getProof(address1) + const proof1 = await getMerkleStateProof(stateManager, address1) - const partialStateManager = await MerkleStateManager.fromProof(proof1) + const partialStateManager = await fromMerkleStateProof(proof1) let account1 = await partialStateManager.getAccount(address1)! verifyAccount(account1!, state1) - const proof2 = await stateManager.getProof(address2) - await partialStateManager.addProofData(proof2) + const proof2 = await getMerkleStateProof(stateManager, address2) + await addMerkleStateProofData(partialStateManager, proof2) let account2 = await partialStateManager.getAccount(address2) verifyAccount(account2!, state2) @@ -231,8 +236,11 @@ describe('StateManager -> General', () => { assert.ok(account3 === undefined) // Input proofs - const stProof = await stateManager.getProof(address1, [state1.keys[0], state1.keys[1]]) - await partialStateManager.addProofData(stProof) + const stProof = await getMerkleStateProof(stateManager, address1, [ + state1.keys[0], + state1.keys[1], + ]) + await addMerkleStateProofData(partialStateManager, stProof) let stSlot1_0 = await partialStateManager.getStorage(address1, state1.keys[0]) assert.ok(equalsBytes(stSlot1_0, state1.values[0])) @@ -244,7 +252,7 @@ describe('StateManager -> General', () => { assert.ok(equalsBytes(stSlot1_2, new Uint8Array())) // Check Array support as input - const newPartialStateManager = await MerkleStateManager.fromProof([proof2, stProof]) + const newPartialStateManager = await fromMerkleStateProof([proof2, stProof]) async function postVerify(sm: MerkleStateManager) { account1 = await sm.getAccount(address1) @@ -269,31 +277,31 @@ describe('StateManager -> General', () => { await postVerify(newPartialStateManager) // Check: empty proof input - const newPartialStateManager2 = await MerkleStateManager.fromProof([]) + const newPartialStateManager2 = await fromMerkleStateProof([]) try { - await newPartialStateManager2.addProofData([proof2, stProof], true) + await addMerkleStateProofData(newPartialStateManager2, [proof2, stProof], true) assert.fail('cannot reach this') } catch (e: any) { assert.ok(e.message.includes('proof does not have the expected trie root')) } - await newPartialStateManager2.addProofData([proof2, stProof]) + await addMerkleStateProofData(newPartialStateManager2, [proof2, stProof]) await newPartialStateManager2.setStateRoot(await partialStateManager.getStateRoot()) await postVerify(newPartialStateManager2) const zeroAddressNonce = BigInt(100) await stateManager.putAccount(createZeroAddress(), new Account(zeroAddressNonce)) - const zeroAddressProof = await stateManager.getProof(createZeroAddress()) + const zeroAddressProof = await getMerkleStateProof(stateManager, createZeroAddress()) try { - await MerkleStateManager.fromProof([proof1, zeroAddressProof], true) + await fromMerkleStateProof([proof1, zeroAddressProof], true) assert.fail('cannot reach this') } catch (e: any) { assert.ok(e.message.includes('proof does not have the expected trie root')) } - await newPartialStateManager2.addProofData(zeroAddressProof) + await addMerkleStateProofData(newPartialStateManager2, zeroAddressProof) let zeroAccount = await newPartialStateManager2.getAccount(createZeroAddress()) assert.ok(zeroAccount === undefined) @@ -318,16 +326,17 @@ describe('StateManager -> General', () => { await sm.putStorage(address, setLengthLeft(intToBytes(0), 32), intToBytes(32)) const storage = await sm.dumpStorage(address) const keys = Object.keys(storage) as PrefixedHexString[] - const proof = await sm.getProof( + const proof = await getMerkleStateProof( + sm, address, keys.map((key) => hexToBytes(key)), ) - const proof2 = await sm.getProof(address2) + const proof2 = await getMerkleStateProof(sm, address2) const newTrie = await createTrieFromProof( proof.accountProof.map((e) => hexToBytes(e)), { useKeyHashing: false }, ) - const partialSM = await MerkleStateManager.fromProof([proof, proof2], true, { + const partialSM = await fromMerkleStateProof([proof, proof2], true, { trie: newTrie, }) assert.equal( @@ -337,10 +346,10 @@ describe('StateManager -> General', () => { ) assert.deepEqual(intToBytes(32), await partialSM.getStorage(address, hexToBytes(keys[0]))) assert.equal((await partialSM.getAccount(address2))?.balance, 100n) - const partialSM2 = await MerkleStateManager.fromProof(proof, true, { + const partialSM2 = await fromMerkleStateProof(proof, true, { trie: newTrie, }) - await partialSM2.addProofData(proof2, true) + await addMerkleStateProofData(partialSM2, proof2, true) assert.equal( partialSM2['_trie']['_opts'].useKeyHashing, false, diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 97ff199044..a5d9be682b 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -1,6 +1,7 @@ import { createBlock, genRequestsTrieRoot } from '@ethereumjs/block' import { ConsensusType, Hardfork } from '@ethereumjs/common' import { RLP } from '@ethereumjs/rlp' +import { StatelessVerkleStateManager, verifyVerkleStateProof } from '@ethereumjs/statemanager' import { Trie } from '@ethereumjs/trie' import { TransactionType } from '@ethereumjs/tx' import { @@ -150,7 +151,10 @@ export async function runBlock(vm: VM, opts: RunBlockOpts): Promise