diff --git a/packages/block/src/block/block.ts b/packages/block/src/block/block.ts index d8da5803da7..370bc8bae23 100644 --- a/packages/block/src/block/block.ts +++ b/packages/block/src/block/block.ts @@ -339,16 +339,16 @@ export class Block { // Validation for Verkle blocks // Unnecessary in this implementation since we're providing defaults if those fields are undefined // TODO: Decide if we should actually require this or not - if (this.common.isActivatedEIP(6800)) { - if (this.executionWitness === undefined) { - throw EthereumJSErrorWithoutCode(`Invalid block: missing executionWitness`) - } - if (this.executionWitness === null) { - throw EthereumJSErrorWithoutCode( - `Invalid block: ethereumjs stateless client needs executionWitness`, - ) - } - } + // if (this.common.isActivatedEIP(6800)) { + // if (this.executionWitness === undefined) { + // throw EthereumJSErrorWithoutCode(`Invalid block: missing executionWitness`) + // } + // if (this.executionWitness === null) { + // throw EthereumJSErrorWithoutCode( + // `Invalid block: ethereumjs stateless client needs executionWitness`, + // ) + // } + // } } /** diff --git a/packages/block/src/block/constructors.ts b/packages/block/src/block/constructors.ts index 65b23ad8007..cbaf266fb5a 100644 --- a/packages/block/src/block/constructors.ts +++ b/packages/block/src/block/constructors.ts @@ -147,11 +147,11 @@ export function createBlockFromBytesArray(values: BlockBytes, opts?: BlockOption ) } - if (header.common.isActivatedEIP(6800) && executionWitnessBytes === undefined) { - throw EthereumJSErrorWithoutCode( - 'Invalid serialized block input: EIP-6800 is active, and execution witness is undefined', - ) - } + // if (header.common.isActivatedEIP(6800) && executionWitnessBytes === undefined) { + // throw EthereumJSErrorWithoutCode( + // 'Invalid serialized block input: EIP-6800 is active, and execution witness is undefined', + // ) + // } // parse transactions const transactions = [] diff --git a/packages/common/src/eips.ts b/packages/common/src/eips.ts index 58bd81db913..45f52e32840 100644 --- a/packages/common/src/eips.ts +++ b/packages/common/src/eips.ts @@ -393,6 +393,15 @@ export const eipsDict: EIPsDict = { minimumHardfork: Hardfork.Paris, requiredEIPs: [4844], }, + /** + * Description : Verkle state transition via an overlay tree + * URL : https://eips.ethereum.org/EIPS/eip-7612 + * Status : Draft + */ + 7612: { + minimumHardfork: Hardfork.Verkle, + requiredEIPs: [4762, 6800 /* 7545 */], + }, /** * Description : EOF Contract Creation * URL : https://github.com/ethereum/EIPs/blob/dd32a34cfe4473bce143641bfffe4fd67e1987ab/EIPS/eip-7620.md @@ -470,6 +479,15 @@ export const eipsDict: EIPsDict = { minimumHardfork: Hardfork.Chainstart, requiredEIPs: [2935], }, + /** + * Description : State conversion to Verkle Tree + * URL : https://eips.ethereum.org/EIPS/eip-7748 + * Status : Draft + */ + 7748: { + minimumHardfork: Hardfork.Verkle, + requiredEIPs: [7612], + }, /** * Description : Ethereum state using a unified binary tree (experimental) * URL : hhttps://eips.ethereum.org/EIPS/eip-7864 diff --git a/packages/mpt/src/mpt.ts b/packages/mpt/src/mpt.ts index f6f024b2996..9968f502384 100644 --- a/packages/mpt/src/mpt.ts +++ b/packages/mpt/src/mpt.ts @@ -115,13 +115,13 @@ export class MerklePatriciaTrie { typeof window === 'undefined' ? (process?.env?.DEBUG?.includes('ethjs') ?? false) : false this.debug = this.DEBUG ? (message: string, namespaces: string[] = []) => { - let log = this._debug - for (const name of namespaces) { - log = log.extend(name) - } - log(message) + let log = this._debug + for (const name of namespaces) { + log = log.extend(name) } - : (..._: any) => {} + log(message) + } + : (..._: any) => { } this.database(opts?.db ?? new MapDB(), valueEncoding) @@ -332,8 +332,8 @@ export class MerklePatriciaTrie { partialPath: { stack: MPTNode[] } = { - stack: [], - }, + stack: [], + }, ): Promise { const targetKey = bytesToNibbles(key) const keyLen = targetKey.length @@ -401,9 +401,8 @@ export class MerklePatriciaTrie { `Comparing node key to expected\n|| Node_Key: [${node.key()}]\n|| Expected: [${targetKey.slice( progress, progress + node.key().length, - )}]\n|| Matching: [${ - targetKey.slice(progress, progress + node.key().length).toString() === - node.key().toString() + )}]\n|| Matching: [${targetKey.slice(progress, progress + node.key().length).toString() === + node.key().toString() }] `, ['find_path', 'extension_node'], @@ -1089,6 +1088,43 @@ export class MerklePatriciaTrie { this._db.checkpoints = [] } + /** + * Returns the next key with a value in the trie, starting from the given startKey (inclusive), + * along with the subsequent key to continue iteration. + * + * @param startKey - packed key bytes to start scanning from + * @returns + * - key: the first key ≥ startKey that has a value (or null if none) + * - nextKey: the key immediately after `key` (or null if there is no further key) + */ + async getNextValue( + startKey: Uint8Array, + ): Promise<{ key: Uint8Array | undefined; nextKey: Uint8Array | undefined }> { + let foundKey: Uint8Array | undefined + let nextKey: Uint8Array | undefined + const startBigInt = bytesToBigInt(startKey) + + await this.walkAllValueNodes(async (node: MPTNode, currentKey: number[]) => { + // only care about leaf nodes + if (!(node instanceof LeafMPTNode)) return + + // reconstruct the full packed key for this leaf + const packed = nibblesTypeToPackedBytes(currentKey.concat(node.key())) + const v = bytesToBigInt(packed) + + if (foundKey === undefined) { + // haven’t hit our startKey yet + if (v < startBigInt) return + foundKey = packed + } else if (nextKey === undefined) { + // this is the very next key after foundKey + nextKey = packed + } + // (no need to return anything; traversal will simply continue but we ignore further nodes) + }) + + return { key: foundKey, nextKey } + } /** * Returns a list of values stored in the trie * @param startKey first unhashed key in the range to be returned (defaults to 0). Note, all keys must be of the same length or undefined behavior will result diff --git a/packages/statemanager/src/index.ts b/packages/statemanager/src/index.ts index 499587f5c34..f9144cdc016 100644 --- a/packages/statemanager/src/index.ts +++ b/packages/statemanager/src/index.ts @@ -6,4 +6,5 @@ export * from './simpleStateManager.ts' export * from './statefulBinaryTreeStateManager.ts' export * from './statefulVerkleStateManager.ts' export * from './statelessVerkleStateManager.ts' +export * from './overlayStateManager.ts' export * from './types.ts' diff --git a/packages/statemanager/src/merkleStateManager.ts b/packages/statemanager/src/merkleStateManager.ts index 8db067e08ec..57679f34318 100644 --- a/packages/statemanager/src/merkleStateManager.ts +++ b/packages/statemanager/src/merkleStateManager.ts @@ -44,13 +44,13 @@ import type { Debugger } from 'debug' export const CODEHASH_PREFIX = utf8ToBytes('c') /** - * Default StateManager implementation for the VM. + * Merkle StateManager implementation for the VM. * * The state manager abstracts from the underlying data store * by providing higher level access to accounts, contract code * and storage slots. * - * The default state manager implementation uses a + * The merkle state manager implementation uses a * `@ethereumjs/mpt` trie as a data backend. * * Note that there is a `SimpleStateManager` dependency-free state @@ -141,10 +141,8 @@ export class MerkleStateManager implements StateManagerInterface { async putAccount(address: Address, account: Account | undefined): Promise { if (this.DEBUG) { this._debug( - `Save account address=${address} nonce=${account?.nonce} balance=${ - account?.balance - } contract=${account && account.isContract() ? 'yes' : 'no'} empty=${ - account && account.isEmpty() ? 'yes' : 'no' + `Save account address=${address} nonce=${account?.nonce} balance=${account?.balance + } contract=${account && account.isContract() ? 'yes' : 'no'} empty=${account && account.isEmpty() ? 'yes' : 'no' }`, ) } @@ -537,6 +535,21 @@ export class MerkleStateManager implements StateManagerInterface { } } + /** + * Find the next account key in the trie after `startKey`, using lexicographical iteration. + * Returns [key, value, nextKey] where: + * - key: the matched key >= startKey + * - nextKey: the next key after this one (or undefined if none) + */ + async getNextAtKey(startKey: Uint8Array): Promise<{ key: Uint8Array, nextKey: Uint8Array | undefined } | undefined> { + const { key, nextKey } = await this._trie.getNextValue(startKey) + if (key === undefined) { + return undefined + } + return { key, nextKey } + + } + /** * 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/overlayStateManager.ts b/packages/statemanager/src/overlayStateManager.ts new file mode 100644 index 00000000000..5d63cd5db98 --- /dev/null +++ b/packages/statemanager/src/overlayStateManager.ts @@ -0,0 +1,566 @@ +import { Common, Mainnet } from '@ethereumjs/common' +import { + Account, + EthereumJSErrorWithoutCode, + KECCAK256_NULL, + bytesToBigInt, + bytesToHex, + compareBytesLexicographically, + createAddressFromString, + equalsBytes, + hexToBytes, + isHexString, + unprefixedHexToBytes, +} from '@ethereumjs/util' +import debugDefault from 'debug' +import { keccak256 } from 'ethereum-cryptography/keccak.js' + +import { OriginalStorageCache } from './cache/index.ts' + +import type { Caches, MerkleStateManager, OverlayStateManagerOpts } from './index.ts' + +import type { + AccountFields, + StateManagerInterface, + StorageDump, + StorageRange, + VerkleAccessWitnessInterface, +} from '@ethereumjs/common' +import type { Address, PrefixedHexString, VerkleCrypto, VerkleExecutionWitness } from '@ethereumjs/util' +import type { Debugger } from 'debug' + +/** + * Overlay StateManager implementation for the VM. + * + * The state manager abstracts from the underlying data store + * by providing higher level access to accounts, contract code + * and storage slots. + * + * This state manager handles two underlying data structures (e.g. an MPT and a Verkle tree) + * and allows transition between a frozen and an active one as per these EIPS: + * EIP-7612: https://eips.ethereum.org/EIPS/eip-7612 + * EIP-7748: https://eips.ethereum.org/EIPS/eip-7748 + */ +export class OverlayStateManager implements StateManagerInterface { + protected _debug: Debugger + protected _caches?: Caches + + originalStorageCache: OriginalStorageCache + verkleCrypto: VerkleCrypto + + // The frozen state manager that we are transitioning away from. Read-only. + protected _frozenStateManager: StateManagerInterface + // The new state manager that we are transitioning to. Can be written to. + protected _activeStateManager: StateManagerInterface + + public readonly common: Common + + protected _checkpointCount: number + + private keccakFunction: Function + + /** + * Next seek key for trieGetNextAtKey iteration + */ + private _nextSeek: Uint8Array = new Uint8Array(32) + // User-supplied map of hashed MPT keys to address preimages + private _frozenTreePreimages: Map + /** Total number of leaves migrated so far (purely informative). */ + private _migratedCount = 0 + private _currAccount?: Uint8Array + /** Sorted storage keys of current account. */ + private _currStorageKeys?: Uint8Array[] + /** Index into storage keys. */ + private _currStorageIndex = 0 + /** Whether account data/code has been migrated. */ + private _currCodeMigrated = false + // Whether conversion has been activated. + private _conversionActivated = false + /** Whether conversion has finished for all accounts. */ + private _conversionFinished = false + + /** + * StateManager is run in DEBUG mode (default: false) + * Taken from DEBUG environment variable + * + * Safeguards on debug() calls are added for + * performance reasons to avoid string literal evaluation + * @hidden + */ + protected readonly DEBUG: boolean = false + + constructor(opts: OverlayStateManagerOpts) { + // Skip DEBUG calls unless 'ethjs' included in environmental DEBUG variables + // Additional window check is to prevent vite browser bundling (and potentially other) to break + this.DEBUG = + typeof window === 'undefined' ? (process?.env?.DEBUG?.includes('ethjs') ?? false) : false + + this._debug = debugDefault('statemanager:transition') + + this.common = opts.common ?? new Common({ chain: Mainnet }) + + this._checkpointCount = 0 + + if (opts.common.customCrypto.verkle === undefined) { + throw EthereumJSErrorWithoutCode('verkle crypto required') + } + + // Accept state managers directly from options (update OverlayStateManagerOpts accordingly) + this._frozenStateManager = opts.frozenStateManager + this._activeStateManager = opts.activeStateManager + this._caches = opts.caches + this.keccakFunction = opts.common?.customCrypto.keccak256 ?? keccak256 + this.verkleCrypto = opts.common.customCrypto.verkle + this.originalStorageCache = new OriginalStorageCache(this.getStorage.bind(this)) + + // Optionally use user-provided preimages + this._frozenTreePreimages = opts.frozenTreePreimages ?? new Map() + } + + /** + * Gets the account associated with `address` or `undefined` if account does not exist + * @param address - Address of the `account` to get + */ + async getAccount(address: Address): Promise { + if (!this._conversionActivated) { + // Pre-conversion: use only frozen state manager + return this._frozenStateManager.getAccount(address) + } + + // Conversion mode: active first, fallback to frozen + let account = await this._activeStateManager.getAccount(address) + if (account !== undefined) { + return account + } + return this._frozenStateManager.getAccount(address) + } + + /** + * Saves an account into state under the provided `address`. + * @param address - Address under which to store `account` + * @param account - The account to store or undefined if to be deleted + */ + async putAccount(address: Address, account?: Account): Promise { + if (!this._conversionActivated) { + // Pre-conversion: always write to frozen + await this._frozenStateManager.putAccount(address, account); + } else { + await this._activeStateManager.putAccount(address, account); + } + } + /** + * Deletes an account from state under the provided `address`. + * @param address - Address of the account which should be deleted + */ + async deleteAccount(address: Address): Promise { + if (!this._conversionActivated) { + // Pre-conversion: delete from frozen + await this._frozenStateManager.deleteAccount(address) + } else { + await this._activeStateManager.deleteAccount(address) + } + } + + modifyAccountFields = async (address: Address, accountFields: AccountFields): Promise => { + if (!this._conversionActivated) { + // Pre-conversion: modify frozen + await this._frozenStateManager.modifyAccountFields(address, accountFields) + } else { + await this._activeStateManager.modifyAccountFields(address, accountFields) + } + } + + /** + * Gets the code associated with `address` or `undefined` if account does not exist + * @param address - Address of the `account` to get + */ + getCode = async (address: Address): Promise => { + if (!this._conversionActivated) { + // Pre-conversion: get from frozen + return this._frozenStateManager.getCode(address) + } + // Conversion mode: active first, fallback to frozen + let code = await this._activeStateManager.getCode(address) + if (code && code.length > 0) { + return code + } + // Fallback to frozen + return this._frozenStateManager.getCode(address) + } + + /** + * Gets the code size associated with `address` or `undefined` if account does not exist + * @param address - Address of the `account` to get + */ + getCodeSize = async (address: Address): Promise => { + if (!this._conversionActivated) { + // Pre-conversion: get from frozen + return this._frozenStateManager.getCodeSize(address) + } + // Conversion mode: active first, fallback to frozen + let size = await this._activeStateManager.getCodeSize(address) + if (size && size > 0) { + return size + } + return this._frozenStateManager.getCodeSize(address) + } + + /** + * Saves contract code for an account at the provided address. + * @param address - Address of the account + * @param value - Contract code as Uint8Array + */ + async putCode(address: Address, value: Uint8Array): Promise { + if (!this._conversionActivated) { + // Pre-conversion: write to frozen + await this._frozenStateManager.putCode(address, value) + } else { + await this._activeStateManager.putCode(address, value) + } + } + + /** + * Gets the storage associated with `address` and `key` or `undefined` if account does not exist + * @param address - Address of the `account` to get + * @param key - Key of the storage to get + */ + async getStorage(address: Address, key: Uint8Array): Promise { + if (!this._conversionActivated) { + // Pre-conversion: get from frozen + return this._frozenStateManager.getStorage(address, key) + } + // Conversion mode: active first, fallback to frozen + let value = await this._activeStateManager.getStorage(address, key) + if (value && value.length > 0) { + return value + } + return this._frozenStateManager.getStorage(address, key) + } + + + /** + * Saves a value to storage. + * @param address - Address of the `account` to save + * @param key - Key of the storage to save + * @param value - Value to save + */ + async putStorage(address: Address, key: Uint8Array, value: Uint8Array): Promise { + if (!this._conversionActivated) { + // Pre-conversion: write to frozen + await this._frozenStateManager.putStorage(address, key, value) + } else { + // Conversion mode: write to active + await this._activeStateManager.putStorage(address, key, value) + } + } + + /** + * Clears a storage slot. + * @param address - Address of the `account` to save + */ + async clearStorage(address: Address): Promise { + if (!this._conversionActivated) { + // Pre-conversion: clear frozen + await this._frozenStateManager.clearStorage(address) + } else { + // Conversion mode: clear active + await this._activeStateManager.clearStorage(address) + } + } + + checkpoint = async (): Promise => { + if (!this._conversionActivated) { + // Pre-conversion: checkpoint frozen + await this._frozenStateManager.checkpoint() + } else { + // Conversion mode: checkpoint active + await this._activeStateManager.checkpoint() + } + this._checkpointCount++ + } + + commit = async (): Promise => { + if (!this._conversionActivated) { + // Pre-conversion: commit frozen + await this._frozenStateManager.commit() + } else { + // Conversion mode: commit active + await this._activeStateManager.commit() + } + this._checkpointCount-- + if (this._checkpointCount === 0) { + this.originalStorageCache.clear() + } + } + + revert = async (): Promise => { + if (!this._conversionActivated) { + // Pre-conversion: revert frozen + await this._frozenStateManager.revert() + } else { + // Conversion mode: revert active + await this._activeStateManager.revert() + } + this._checkpointCount-- + if (this._checkpointCount === 0) { + this.originalStorageCache.clear() + } + } + + getStateRoot(): Promise { + if (!this._conversionActivated) { + // Pre-conversion: return frozen state root + return this._frozenStateManager.getStateRoot() + } + return this._activeStateManager.getStateRoot() + } + + + setStateRoot(stateRoot: Uint8Array, clearCache?: boolean): Promise { + if (!this._conversionActivated) { + // Pre-conversion: set frozen + return this._frozenStateManager.setStateRoot(stateRoot, clearCache) + } else { + // Conversion mode: set active + return this._activeStateManager.setStateRoot(stateRoot, clearCache) + } + } + + hasStateRoot(root: Uint8Array): Promise { + if (!this._conversionActivated) { + // Pre-conversion: check frozen + return this._frozenStateManager.hasStateRoot(root) + } + // Conversion mode: check active + return this._activeStateManager.hasStateRoot(root) + } + + dumpStorage?(_address: Address): Promise { + throw EthereumJSErrorWithoutCode('Method not implemented.') + } + + dumpStorageRange?(_address: Address, _startKey: bigint, _limit: number): Promise { + throw EthereumJSErrorWithoutCode('Method not implemented.') + } + + clearCaches(): void { + this._caches?.clear() + } + + shallowCopy(_downlevelCaches?: boolean): StateManagerInterface { + throw EthereumJSErrorWithoutCode('Method not implemented.') + } + + async checkChunkWitnessPresent(_address: Address, _codeOffset: number): Promise { + throw EthereumJSErrorWithoutCode('Method not implemented.') + } + + isConversionActivated(): boolean { + return this._conversionActivated + } + + activateConversion(): void { + if (this._conversionActivated) { + throw EthereumJSErrorWithoutCode('Conversion already activated') + } + this._conversionActivated = true + } + + /** + * Migrates a specified set of MPT leaves (accounts) to the Verkle tree. + * The caller is responsible for determining which leaves to migrate (stride logic). + * + * @param leafKeys Array of account keys (addresses as Uint8Array) to migrate. + */ + public async migrateLeavesToVerkle(leafKeys: Uint8Array[]): Promise { + if (!this._conversionActivated) { + throw EthereumJSErrorWithoutCode('Transition must be activated to begin migrating leaves.') + } + + for (const key of leafKeys) { + // 1. Get the account from the frozen state manager + const address = createAddressFromString(bytesToHex(key)) + const account = await this._frozenStateManager.getAccount(address) + if (!account) { + // No account at this key, skip + continue + } + // 2. Insert account into active state manager + await this._activeStateManager.putAccount(address, account) + // 3. If account has code, migrate code as well + if (account.codeHash && !equalsBytes(account.codeHash, KECCAK256_NULL)) { + const code = await this._frozenStateManager.getCode(address) + if (code) { + await this._activeStateManager.putCode(address, code) + } + } + // 4. Migrate storage if storageRoot is not empty + if (account.storageRoot && !equalsBytes(account.storageRoot, KECCAK256_NULL)) { + // For generic state managers, we need to enumerate all storage keys + // Here we assume frozenStateManager exposes a method to dump storage (or similar) + if (typeof this._frozenStateManager.dumpStorage === 'function') { + const storageDump = await this._frozenStateManager.dumpStorage(address) + for (const [keyHex, value] of Object.entries(storageDump)) { + const cleanKeyHex = keyHex.startsWith('0x') ? keyHex.slice(2) : keyHex + const storageKey = unprefixedHexToBytes(cleanKeyHex) + await this._activeStateManager.putStorage( + address, + storageKey, + hexToBytes(value as PrefixedHexString), + ) + } + } else { + throw EthereumJSErrorWithoutCode('dumpStorage not implemented on frozenStateManager') + } + } + } + } + + /** + * Returns true if there are no more leaves pending migration. + */ + public isFullyConverted(): boolean { + return this._conversionFinished + } + + /** + * Pops up to `stride` conversion units (storage slots or account data+code) per block. + */ + public async runConversionStep(stride: number): Promise { + if (this._conversionFinished || stride <= 0) { + this._debug?.(`Skipping conversion step: conversionFinished=${this._conversionFinished}, stride=${stride}`); + return; + } + let unitsLeft = stride; + while (unitsLeft > 0) { + // Find the next account in the frozen state and initialize it as current + if (!this._currAccount) { + // Get next account from frozen trie + this._debug?.(`Fetching next account from frozen state manager, seek=${bytesToHex(this._nextSeek)}`); + const res = await (this._frozenStateManager as MerkleStateManager).getNextAtKey(this._nextSeek); + if (!res) { + this._debug?.(`No more accounts in frozen trie, conversion complete`); + this._conversionFinished = true; + break; + } + const { key: hashedKey, nextKey: nextSeek } = res + this._nextSeek = nextSeek ?? new Uint8Array(32); + this._debug?.(`Found account in trie: hashedKey=${bytesToHex(hashedKey)}, nextSeek=${bytesToHex(this._nextSeek)}`); + + // Lookup address preimage + const hashedKeyHex = bytesToHex(hashedKey); + const addrBytes = this._frozenTreePreimages!.get(hashedKeyHex); + if (!addrBytes) { + throw EthereumJSErrorWithoutCode(`Missing preimage for key ${bytesToHex(hashedKey)}`); + } + this._currAccount = addrBytes; + const addr = createAddressFromString(bytesToHex(addrBytes)); + const dump = await this._frozenStateManager.dumpStorage?.(addr) ?? {}; + this._currStorageKeys = Object.keys(dump) + .map((k: string) => (isHexString(k) ? hexToBytes(k) : unprefixedHexToBytes(k))) + .sort(compareBytesLexicographically); + this._currStorageIndex = 0; + this._currCodeMigrated = false; + continue; + } + + // Dead account skip logic (only skip storage; still migrate account data+code) + const addr = createAddressFromString(bytesToHex(this._currAccount!)); + this._debug?.(`Processing account: ${bytesToHex(this._currAccount!)}`); + const account = await this._frozenStateManager.getAccount(addr); + // As per EIP-7748, an account with nonce=0 and empty code is “dead” + const isDeadAccount = + account !== undefined && + account.nonce === 0n && + (!account.codeHash || equalsBytes(account.codeHash, KECCAK256_NULL)) && + this._currStorageKeys + + this._debug?.( + `Account details: nonce=${account?.nonce ?? 0n}, ` + + `codeHash=${account?.codeHash ? bytesToHex(account.codeHash) : 'null'}, ` + + `storageSlots=${this._currStorageKeys?.length ?? 0}` + ); + + if (isDeadAccount) { + this._debug?.( + `Dead account detected (nonce=0, empty code); ` + + `skipping storage migration but will migrate account data+code: ` + + bytesToHex(this._currAccount!) + ); + + // Mark all storage slots as “done” so we jump to the account+code phase + this._currStorageIndex = this._currStorageKeys!.length; + // Leave this._currCodeMigrated === false to allow the next block to run + // continue; + } + + // Migrate storage slots, one per stride unit + if (this._currStorageKeys && this._currStorageIndex < this._currStorageKeys.length) { + const storageKey = this._currStorageKeys[this._currStorageIndex]; + this._debug?.(`Migrating storage slot ${this._currStorageIndex + 1}/${this._currStorageKeys.length}: ${bytesToHex(storageKey)}`); + const val = await this._frozenStateManager.getStorage(addr, storageKey); + await this._activeStateManager.putStorage(addr, storageKey, val); + this._currStorageIndex++; + unitsLeft--; + this._debug?.(`Storage slot migrated successfully`); + continue; + } + + // Migrate account data and code + if (!this._currCodeMigrated) { + this._debug?.(`Migrating account data and code`); + if (account !== undefined) { + await this._activeStateManager.putAccount(addr, account); + this._debug?.(`Account data migrated`); + + if (account.codeHash && !equalsBytes(account.codeHash, KECCAK256_NULL)) { + this._debug?.(`Migrating code (hash: ${bytesToHex(account.codeHash)})`); + const code = await this._frozenStateManager.getCode(addr); + if (code) { + await this._activeStateManager.putCode(addr, code); + this._debug?.(`Code migrated, size: ${code.length} bytes`); + } + } else { + this._debug?.(`No code to migrate (empty code hash)`); + } + } else { + this._debug?.(`Account is undefined, skipping migration`); + } + this._currCodeMigrated = true; + unitsLeft--; + this._debug?.(`Account migration completed`); + continue; + } + + this._migratedCount++; + this._debug?.(`Account migration finalized, total migrated: ${this._migratedCount}`); + + // Reset for next account + this._currAccount = undefined; + this._currStorageKeys = undefined; + this._currStorageIndex = 0; + this._currCodeMigrated = false; + + // Continue to next account/loop + } + } + + /** + * Add preimages to the frozen preimage mapping + */ + public addPreimages(map: Map): void { + this._frozenTreePreimages = new Map([...(this._frozenTreePreimages), ...map]) + } + + public getAppliedKey(address: Uint8Array): Uint8Array { + return this._frozenStateManager.getAppliedKey!(address) + } + + public initVerkleExecutionWitness(blockNum: bigint, executionWitness?: VerkleExecutionWitness | null): void { + this._activeStateManager.initVerkleExecutionWitness!(blockNum, executionWitness) + } + + public async verifyVerklePostState?(accessWitness: VerkleAccessWitnessInterface): Promise { + return this._activeStateManager.verifyVerklePostState!(accessWitness) + } +} diff --git a/packages/statemanager/src/statefulVerkleStateManager.ts b/packages/statemanager/src/statefulVerkleStateManager.ts index 9393d3f4208..6885e73f320 100644 --- a/packages/statemanager/src/statefulVerkleStateManager.ts +++ b/packages/statemanager/src/statefulVerkleStateManager.ts @@ -98,9 +98,9 @@ export class StatefulVerkleStateManager implements StateManagerInterface { this._checkpointCount = 0 - if (opts.common.isActivatedEIP(6800) === false) { - throw EthereumJSErrorWithoutCode('EIP-6800 required for verkle state management') - } + // if (opts.common.isActivatedEIP(6800) === false) { + // throw EthereumJSErrorWithoutCode('EIP-6800 required for verkle state management') + // } if (opts.common.customCrypto.verkle === undefined) { throw EthereumJSErrorWithoutCode('verkle crypto required') @@ -219,10 +219,8 @@ export class StatefulVerkleStateManager implements StateManagerInterface { putAccount = async (address: Address, account?: Account): Promise => { if (this.DEBUG) { this._debug( - `putAccount address=${address} nonce=${account?.nonce} balance=${ - account?.balance - } contract=${account && account.isContract() ? 'yes' : 'no'} empty=${ - account && account.isEmpty() ? 'yes' : 'no' + `putAccount address=${address} nonce=${account?.nonce} balance=${account?.balance + } contract=${account && account.isContract() ? 'yes' : 'no'} empty=${account && account.isEmpty() ? 'yes' : 'no' }`, ) } @@ -230,6 +228,10 @@ export class StatefulVerkleStateManager implements StateManagerInterface { if (account !== undefined) { const stem = getVerkleStem(this.verkleCrypto, address, 0) const basicDataBytes = encodeVerkleLeafBasicData(account) + this._debug("Verkle root before putAccount:", bytesToHex(this._trie.root())); + this._debug("Has state root?: ", await this.hasStateRoot(this._trie.root())) + this._debug("Node at root?: ", await this._trie['_db'].get(this._trie.root())) + await this._trie.put( stem, [VerkleLeafType.BasicData, VerkleLeafType.CodeHash], @@ -721,6 +723,7 @@ export class StatefulVerkleStateManager implements StateManagerInterface { } setStateRoot(stateRoot: Uint8Array, clearCache?: boolean): Promise { + this._debug(`setStateRoot: ${short(stateRoot)}`) this._trie.root(stateRoot) clearCache === true && this.clearCaches() return Promise.resolve() diff --git a/packages/statemanager/src/types.ts b/packages/statemanager/src/types.ts index 4bf29b85fe5..b8a98f1a3f5 100644 --- a/packages/statemanager/src/types.ts +++ b/packages/statemanager/src/types.ts @@ -2,9 +2,11 @@ import { type PrefixedHexString } from '@ethereumjs/util' import type { BinaryTree } from '@ethereumjs/binarytree' import type { Common } from '@ethereumjs/common' +import type { StateManagerInterface } from '@ethereumjs/common' import type { MerklePatriciaTrie } from '@ethereumjs/mpt' import type { VerkleTree } from '@ethereumjs/verkle' import type { Caches } from './index.ts' + /** * Basic state manager options (not to be used directly) */ @@ -65,6 +67,17 @@ export interface MerkleStateManagerOpts extends BaseStateManagerOpts { caches?: Caches } +export interface OverlayStateManagerOpts extends BaseStateManagerOpts { + frozenStateManager: StateManagerInterface + activeStateManager: StateManagerInterface + common: Common // Common required since it provides verkleCrypto through customCrypto + caches?: Caches + /** + * Mapping from hashed MPT keys (hex strings) to their original address preimages + */ + frozenTreePreimages?: Map +} + /** * Options dictionary. */ diff --git a/packages/util/src/conversion.ts b/packages/util/src/conversion.ts new file mode 100644 index 00000000000..ca41e21b45b --- /dev/null +++ b/packages/util/src/conversion.ts @@ -0,0 +1,2 @@ +// TODO: Replace with actual value or client param? +export const EIP7612_CONVERSION_STRIDE = 7 diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index 550eef2a430..cfb57a2c506 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -54,6 +54,7 @@ export * from './types.ts' export * from './authorization.ts' export * from './binaryTree.ts' export * from './blobs.ts' +export * from './conversion.ts' export { arrayContainsArray, fromAscii, diff --git a/packages/verkle/src/verkleTree.ts b/packages/verkle/src/verkleTree.ts index 0c205e80017..150c426a5b1 100644 --- a/packages/verkle/src/verkleTree.ts +++ b/packages/verkle/src/verkleTree.ts @@ -78,13 +78,13 @@ export class VerkleTree { typeof window === 'undefined' ? (process?.env?.DEBUG?.includes('ethjs') ?? false) : false this.debug = this.DEBUG ? (message: string, namespaces: string[] = []) => { - let log = this._debug - for (const name of namespaces) { - log = log.extend(name) - } - log(message) + let log = this._debug + for (const name of namespaces) { + log = log.extend(name) } - : (..._: any) => {} + log(message) + } + : (..._: any) => { } this.DEBUG && this.debug(`Trie created: @@ -185,6 +185,7 @@ export class VerkleTree { const putStack: [Uint8Array, VerkleNode | null][] = [] // Find path to nearest node const foundPath = await this.findPath(stem) + this.debug(`Found path: ${foundPath}`, ['put']) // Sanity check - we should at least get the root node back if (foundPath.stack.length === 0) { @@ -320,8 +321,7 @@ export class VerkleTree { this.root(this.verkleCrypto.serializeCommitment(rootNode.commitment)) this.DEBUG && this.debug( - `Updating child reference for node with path: ${bytesToHex(lastPath)} at index ${ - lastPath[0] + `Updating child reference for node with path: ${bytesToHex(lastPath)} at index ${lastPath[0] } in root node`, ['put'], ) @@ -379,9 +379,9 @@ export class VerkleTree { leafNodeWasDeleted ? null : { - commitment: leafNode.commitment, - path: leafNode.stem, - }, + commitment: leafNode.commitment, + path: leafNode.stem, + }, ) if (leafNodeWasDeleted) { // Check to see if the internal node has only one other child node @@ -406,8 +406,7 @@ export class VerkleTree { this.debug( `Updating child reference for leaf node with stem: ${bytesToHex( leafNode.stem, - )} at index ${ - leafNode.stem[partialMatchingStemIndex] + )} at index ${leafNode.stem[partialMatchingStemIndex] } in internal node at path ${bytesToHex( leafNode.stem.slice(0, partialMatchingStemIndex), )}`, @@ -437,6 +436,7 @@ export class VerkleTree { // if (equalsBytes(this.root(), this.EMPTY_TREE_ROOT)) return result // Get root node + this.DEBUG && this.debug(`Root Node: [${bytesToHex(this.root())}]`, ['find_path']) let rawNode = await this._db.get(this.root()) if (rawNode === undefined) throw EthereumJSErrorWithoutCode('root node should exist') @@ -528,13 +528,18 @@ export class VerkleTree { const rootNode = new InternalVerkleNode({ commitment: this.verkleCrypto.zeroCommitment, verkleCrypto: this.verkleCrypto, - }) + }); - this.DEBUG && this.debug(`No root node. Creating new root node`, ['initialize']) - // Set trie root to serialized (aka compressed) commitment for later use in verkle proof - this.root(this.verkleCrypto.serializeCommitment(rootNode.commitment)) - await this.saveStack([[this.root(), rootNode]]) - return + this.DEBUG && this.debug(`No root node. Creating new root node`, ['initialize']); + + // Set the root hash to the hash of the zero commitment + const rootHash = this.verkleCrypto.hashCommitment(rootNode.commitment); + this.root(rootHash); + + // Save the root node to the database + await this.saveStack([[rootHash, rootNode]]); + + return; } /** diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index c2299dc349f..59b6cd86c97 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -3,7 +3,7 @@ import { ConsensusType, Hardfork } from '@ethereumjs/common' import { type EVM, type EVMInterface, VerkleAccessWitness } from '@ethereumjs/evm' import { MerklePatriciaTrie } from '@ethereumjs/mpt' import { RLP } from '@ethereumjs/rlp' -import { type StatelessVerkleStateManager, verifyVerkleStateProof } from '@ethereumjs/statemanager' +import { OverlayStateManager, type StatelessVerkleStateManager, verifyVerkleStateProof } from '@ethereumjs/statemanager' import { TransactionType } from '@ethereumjs/tx' import { Account, @@ -25,6 +25,7 @@ import { setLengthLeft, short, unprefixedHexToBytes, + EIP7612_CONVERSION_STRIDE, } from '@ethereumjs/util' import debugDefault from 'debug' import { sha256 } from 'ethereum-cryptography/sha256.js' @@ -80,7 +81,6 @@ export async function runBlock(vm: VM, opts: RunBlockOpts): Promise= vm.common.hardforkTimestamp(Hardfork.Verkle)! && stateManager instanceof OverlayStateManager && !stateManager.isConversionActivated() + ) { + if (vm.DEBUG) { + debug(`Activate conversion`) + } + stateManager.activateConversion() + } else { + // Set state root if provided + // We skip this if we are at the first block of the conversion because the stateRoot won't match the frozen state + if (root) { + if (vm.DEBUG) { + debug(`Set provided state root ${bytesToHex(root)} clearCache=${clearCache}`) + } + await stateManager.setStateRoot(root, clearCache) + } + } + + + if (stateManager instanceof OverlayStateManager && stateManager.isConversionActivated() && !stateManager.isFullyConverted()) { if (vm.DEBUG) { - debug(`Set provided state root ${bytesToHex(root)} clearCache=${clearCache}`) + debug(`Run conversion step with stride of ${EIP7612_CONVERSION_STRIDE}`) } - await stateManager.setStateRoot(root, clearCache) + await stateManager.runConversionStep(EIP7612_CONVERSION_STRIDE) } if (vm.common.isActivatedEIP(6800) || vm.common.isActivatedEIP(7864)) { + const isConversionActive = stateManager instanceof OverlayStateManager && stateManager.isConversionActivated() && !stateManager.isFullyConverted() // Initialize the access witness - if (vm.common.customCrypto.verkle === undefined) { throw Error('verkleCrypto required when EIP-6800 is active') } @@ -146,7 +162,7 @@ export async function runBlock(vm: VM, opts: RunBlockOpts): Promise { + debug(`Adding preimage to overlay state manager :${bytesToHex(preimage)}`) + }) + vm.stateManager.addPreimages(result.preimages) + } + const results: RunBlockResult = { receipts: result.receipts, logsBloom: result.bloom.bitvector, @@ -386,8 +409,7 @@ export async function runBlock(vm: VM, opts: RunBlockOpts): Promise { } emitEVMProfile(logs.precompiles, 'Precompile performance') emitEVMProfile(logs.opcodes, 'Opcodes performance') - ;(vm.evm as EVM).clearPerformanceLogs() + ; (vm.evm as EVM).clearPerformanceLogs() } } } @@ -201,12 +202,13 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { let stateAccesses: VerkleAccessWitness | BinaryTreeAccessWitness | undefined let txAccesses: VerkleAccessWitness | BinaryTreeAccessWitness | undefined if (vm.common.isActivatedEIP(6800)) { - if (vm.evm.verkleAccessWitness === undefined) { + // Require verkle access witnesses unless we're in the conversion phase + if (vm.evm.verkleAccessWitness === undefined && (!(state instanceof OverlayStateManager) || (state instanceof OverlayStateManager && state.isConversionActivated() && state.isFullyConverted()))) { throw Error(`Verkle access witness needed for execution of verkle blocks`) } // Check if statemanager is a Verkle State Manager (stateless and stateful both have verifyVerklePostState) - if (!('verifyVerklePostState' in vm.stateManager)) { + if (!('verifyVerklePostState' in vm.stateManager) && !(state instanceof OverlayStateManager)) { throw EthereumJSErrorWithoutCode(`Verkle State Manager needed for execution of verkle blocks`) } stateAccesses = vm.evm.verkleAccessWitness @@ -245,8 +247,7 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { const caller = tx.getSenderAddress() if (vm.DEBUG) { debug( - `New tx run hash=${ - opts.tx.isSigned() ? bytesToHex(opts.tx.hash()) : 'unsigned' + `New tx run hash=${opts.tx.isSigned() ? bytesToHex(opts.tx.hash()) : 'unsigned' } sender=${caller}`, ) } @@ -308,8 +309,7 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { const baseFeePerGas = block?.header.baseFeePerGas ?? DEFAULT_HEADER.baseFeePerGas! if (maxFeePerGas < baseFeePerGas) { const msg = _errorMsg( - `Transaction's ${ - 'maxFeePerGas' in tx ? 'maxFeePerGas' : 'gasPrice' + `Transaction's ${'maxFeePerGas' in tx ? 'maxFeePerGas' : 'gasPrice' } (${maxFeePerGas}) is less than the block's baseFeePerGas (${baseFeePerGas})`, vm, block, @@ -580,10 +580,8 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { if (vm.DEBUG) { debug( - `Running tx=${ - tx.isSigned() ? bytesToHex(tx.hash()) : 'unsigned' - } with caller=${caller} gasLimit=${gasLimit} to=${ - to?.toString() ?? 'none' + `Running tx=${tx.isSigned() ? bytesToHex(tx.hash()) : 'unsigned' + } with caller=${caller} gasLimit=${gasLimit} to=${to?.toString() ?? 'none' } value=${value} data=${short(data)}`, ) } @@ -601,9 +599,9 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { })) as RunTxResult if (vm.common.isActivatedEIP(6800)) { - ;(stateAccesses as VerkleAccessWitness)?.merge(txAccesses! as VerkleAccessWitness) + ; (stateAccesses as VerkleAccessWitness)?.merge(txAccesses! as VerkleAccessWitness) } else if (vm.common.isActivatedEIP(7864)) { - ;(stateAccesses as BinaryTreeAccessWitness)?.merge(txAccesses! as BinaryTreeAccessWitness) + ; (stateAccesses as BinaryTreeAccessWitness)?.merge(txAccesses! as BinaryTreeAccessWitness) } if (enableProfiler) { @@ -623,8 +621,7 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { const { executionGasUsed, exceptionError, returnValue } = results.execResult debug('-'.repeat(100)) debug( - `Received tx execResult: [ executionGasUsed=${executionGasUsed} exceptionError=${ - exceptionError !== undefined ? `'${exceptionError.error}'` : 'none' + `Received tx execResult: [ executionGasUsed=${executionGasUsed} exceptionError=${exceptionError !== undefined ? `'${exceptionError.error}'` : 'none' } returnValue=${short(returnValue)} gasRefund=${results.gasRefund ?? 0} ]`, ) } @@ -841,8 +838,7 @@ async function _runTx(vm: VM, opts: RunTxOpts): Promise { await vm._emit('afterTx', event) if (vm.DEBUG) { debug( - `tx run finished hash=${ - opts.tx.isSigned() ? bytesToHex(opts.tx.hash()) : 'unsigned' + `tx run finished hash=${opts.tx.isSigned() ? bytesToHex(opts.tx.hash()) : 'unsigned' } sender=${caller}`, ) } @@ -902,10 +898,8 @@ export async function generateTxReceipt( let receipt if (vm.DEBUG) { debug( - `Generate tx receipt transactionType=${ - tx.type - } cumulativeBlockGasUsed=${cumulativeGasUsed} bitvector=${short(baseReceipt.bitvector)} (${ - baseReceipt.bitvector.length + `Generate tx receipt transactionType=${tx.type + } cumulativeBlockGasUsed=${cumulativeGasUsed} bitvector=${short(baseReceipt.bitvector)} (${baseReceipt.bitvector.length } bytes) logs=${baseReceipt.logs.length}`, ) } diff --git a/packages/vm/test/tester/config.ts b/packages/vm/test/tester/config.ts index 96aee521d34..4fc2b6a1b0c 100644 --- a/packages/vm/test/tester/config.ts +++ b/packages/vm/test/tester/config.ts @@ -324,12 +324,13 @@ function setupCommonForVerkle(network: string, timestamp?: number, kzg?: KZG) { } } } - - testHardforks.push({ name: 'verkle', block: 1 }) + if (network === 'shanghaiToVerkleAtTime32') { + testHardforks.push({ name: 'verkle', block: null, timestamp: 32 }) + } const common = createCustomCommon( { hardforks: testHardforks, - defaultHardfork: 'verkle', + defaultHardfork: 'shanghai', }, Mainnet, { eips: [2935, 3607], customCrypto: { kzg, verkle } }, @@ -358,7 +359,7 @@ export function getCommon(network: string, kzg?: KZG): Common { } let networkLowercase = network.toLowerCase() // Special handler for verkle tests - if (networkLowercase.includes('verkle')) { + if (network !== 'shanghaitoverkleattime32' && networkLowercase.includes('verkle')) { return setupCommonForVerkle(network, undefined, kzg) } if (network.includes('+')) { @@ -380,14 +381,16 @@ export function getCommon(network: string, kzg?: KZG): Common { return setupCommonWithNetworks('Shanghai', undefined, 15000, kzg) } else if (networkLowercase === 'cancuntopragueattime15k') { return setupCommonWithNetworks('Cancun', undefined, 15000, kzg) + } else if (networkLowercase === 'shanghaitoverkleattime32') { + return setupCommonForVerkle('Shanghai', undefined, kzg) } else { // Case 3: this is not a "default fork" network, but it is a "transition" network. Test the VM if it transitions the right way const transitionForks = network in transitionNetworks ? transitionNetworks[network as keyof typeof transitionNetworks] : transitionNetworks[ - (network.charAt(0).toUpperCase() + network.slice(1)) as keyof typeof transitionNetworks - ] + (network.charAt(0).toUpperCase() + network.slice(1)) as keyof typeof transitionNetworks + ] if (transitionForks === undefined) { throw new Error('network not supported: ' + network) } diff --git a/packages/vm/test/tester/runners/BlockchainTestsRunner.ts b/packages/vm/test/tester/runners/BlockchainTestsRunner.ts index ef6a2fea525..8329f9e6377 100644 --- a/packages/vm/test/tester/runners/BlockchainTestsRunner.ts +++ b/packages/vm/test/tester/runners/BlockchainTestsRunner.ts @@ -2,18 +2,25 @@ import { createBlock, createBlockFromRLP } from '@ethereumjs/block' import { EthashConsensus, createBlockchain } from '@ethereumjs/blockchain' import { ConsensusAlgorithm } from '@ethereumjs/common' import { Ethash } from '@ethereumjs/ethash' -import { MerklePatriciaTrie } from '@ethereumjs/mpt' +import { createMPT, MerklePatriciaTrie } from '@ethereumjs/mpt' import { RLP } from '@ethereumjs/rlp' -import { Caches, MerkleStateManager, StatefulVerkleStateManager } from '@ethereumjs/statemanager' +import { + Caches, + MerkleStateManager, + OverlayStateManager, + StatefulVerkleStateManager, +} from '@ethereumjs/statemanager' import { createTxFromRLP } from '@ethereumjs/tx' import { MapDB, bytesToBigInt, bytesToHex, + equalsBytes, hexToBytes, isHexString, stripHexPrefix, } from '@ethereumjs/util' +import type { VerkleTree } from '@ethereumjs/verkle'; import { createVerkleTree } from '@ethereumjs/verkle' import { buildBlock, createVM, runBlock } from '../../../src/index.ts' @@ -23,7 +30,6 @@ import type { Block } from '@ethereumjs/block' import type { Blockchain, ConsensusDict } from '@ethereumjs/blockchain' import type { Common, StateManagerInterface } from '@ethereumjs/common' import type { PrefixedHexString } from '@ethereumjs/util' -import type { VerkleTree } from '@ethereumjs/verkle' import type * as tape from 'tape' function formatBlockHeader(data: any) { @@ -58,7 +64,8 @@ export async function runBlockchainTest(options: any, testData: any, t: tape.Tes common: options.common, }) } else { - stateTree = new MerklePatriciaTrie({ useKeyHashing: true, common }) + stateTree = await createMPT({ useKeyHashing: true, common }) + stateManager = new MerkleStateManager({ caches: new Caches(), trie: stateTree, @@ -98,7 +105,43 @@ export async function runBlockchainTest(options: any, testData: any, t: tape.Tes }) if (validatePow) { - ;(blockchain.consensus as EthashConsensus)._ethash!.cacheDB = cacheDB + ; (blockchain.consensus as EthashConsensus)._ethash.cacheDB = cacheDB + } + + // set up pre-state + await setupPreConditions(stateManager, testData) + + t.deepEquals( + await stateManager.getStateRoot(), + genesisBlock.header.stateRoot, + 'correct pre stateRoot', + ) + + // If using the OverlayStateManager: + // - Build a frozen MPT preloaded with pre-state + // - Set up an empty Verkle trie for the active backend + // - Instantiate the OverlayStateManager with a seeded conversion queue + if (options.stateManager === 'overlay') { + // (1) frozen MPT with pre-state + const frozen = stateManager + // (2) empty Verkle trie for active writes + stateTree = await createVerkleTree() + const activeStateManager = new StatefulVerkleStateManager({ trie: stateTree, common }) + + // (3) Seed preimages map from all pre-state addresses + const preimages = new Map(); + for (const addrHex of Object.keys(testData.pre) as PrefixedHexString[]) { + const addrBytes = hexToBytes(addrHex); // 20-byte address + const hashedKey = frozen.getAppliedKey!(addrBytes) + preimages.set(bytesToHex(hashedKey), addrBytes); + } + stateManager = new OverlayStateManager({ + frozenStateManager: frozen, + activeStateManager, + common, + caches: new Caches(), + frozenTreePreimages: preimages + }) } const begin = Date.now() @@ -107,6 +150,7 @@ export async function runBlockchainTest(options: any, testData: any, t: tape.Tes bls: options.bls, bn254: options.bn254, } + let vm = await createVM({ stateManager, blockchain, @@ -118,15 +162,6 @@ export async function runBlockchainTest(options: any, testData: any, t: tape.Tes }, }) - // set up pre-state - await setupPreConditions(vm.stateManager, testData) - - t.deepEquals( - await vm.stateManager.getStateRoot(), - genesisBlock.header.stateRoot, - 'correct pre stateRoot', - ) - async function handleError(error: string | undefined, expectException: string | boolean) { if (expectException !== false) { t.pass(`Expected exception ${expectException}`) @@ -166,7 +201,7 @@ export async function runBlockchainTest(options: any, testData: any, t: tape.Tes const decoded: any = RLP.decode(blockRlp) timestamp = bytesToBigInt(decoded[0][11]) // eslint-disable-next-line no-empty - } catch {} + } catch { } common.setHardforkBy({ blockNumber: currentBlock, timestamp }) @@ -221,6 +256,7 @@ export async function runBlockchainTest(options: any, testData: any, t: tape.Tes { common, setHardfork: true, + }, ) } else { @@ -242,7 +278,8 @@ export async function runBlockchainTest(options: any, testData: any, t: tape.Tes const parentState = parentBlock.header.stateRoot // run block, update head if valid try { - await runBlock(vm, { block, root: parentState, setHardfork: true }) + const runBlockResult = await runBlock(vm, { block, root: parentState, setHardfork: true, reportPreimages: stateManager instanceof OverlayStateManager }) + // t.equal(bytesToHex(runBlockResult.stateRoot), bytesToHex(block.header.stateRoot), 'stateRoot matches') // set as new head block } catch (error: any) { // remove invalid block diff --git a/packages/vm/test/tester/testLoader.ts b/packages/vm/test/tester/testLoader.ts index ca38269c6cd..71293a814f3 100644 --- a/packages/vm/test/tester/testLoader.ts +++ b/packages/vm/test/tester/testLoader.ts @@ -113,6 +113,7 @@ export async function getTestsFromArgs(testType: string, onFile: Function, args: skipFn = (name: string) => { return skipTest(name, args.skipTests) } + if (new RegExp(`BlockchainTests`).test(testType)) { const forkFilter = new RegExp(`${args.forkConfig}$`) skipFn = (name: string, test: any) => { diff --git a/packages/vm/test/util.ts b/packages/vm/test/util.ts index 7b019760119..62fe7ad5031 100644 --- a/packages/vm/test/util.ts +++ b/packages/vm/test/util.ts @@ -84,8 +84,7 @@ export function verifyAccountPostConditions( t.comment('Account: ' + address) if (!equalsBytes(format(account.balance, true), format(acctData.balance, true))) { t.comment( - `Expected balance of ${bytesToBigInt(format(acctData.balance, true))}, but got ${ - account.balance + `Expected balance of ${bytesToBigInt(format(acctData.balance, true))}, but got ${account.balance }`, ) } @@ -119,8 +118,7 @@ export function verifyAccountPostConditions( if (val !== hashedStorage[key]) { t.comment( - `Expected storage key ${bytesToHex(data.key)} at address ${address} to have value ${ - hashedStorage[key] ?? '0x' + `Expected storage key ${bytesToHex(data.key)} at address ${address} to have value ${hashedStorage[key] ?? '0x' }, but got ${val}}`, ) } @@ -391,17 +389,17 @@ export function makeBlockFromEnv(env: any, opts?: BlockOptions): Block { /** * setupPreConditions given JSON testData - * @param state - the state DB/trie + * @param stateManager - the stateManager * @param testData - JSON from tests repo */ -export async function setupPreConditions(state: StateManagerInterface, testData: any) { - await state.checkpoint() +export async function setupPreConditions(stateManager: StateManagerInterface, testData: any) { + await stateManager.checkpoint() for (const addressStr of Object.keys(testData.pre)) { const { nonce, balance, code, storage } = testData.pre[addressStr] const addressBuf = format(addressStr) const address = new Address(addressBuf) - await state.putAccount(address, new Account()) + await stateManager.putAccount(address, new Account()) const codeBuf = format(code) const codeHash = keccak256(codeBuf) @@ -413,13 +411,13 @@ export async function setupPreConditions(state: StateManagerInterface, testData: continue } const key = setLengthLeft(format(storageKey), 32) - await state.putStorage(address, key, val) + await stateManager.putStorage(address, key, val) } // Put contract code - await state.putCode(address, codeBuf) + await stateManager.putCode(address, codeBuf) - const storageRoot = (await state.getAccount(address))!.storageRoot + const storageRoot = (await stateManager.getAccount(address))!.storageRoot if (testData.exec?.address === addressStr) { testData.root(storageRoot) @@ -433,9 +431,9 @@ export async function setupPreConditions(state: StateManagerInterface, testData: storageRoot, codeSize: codeBuf.byteLength, }) - await state.putAccount(address, account) + await stateManager.putAccount(address, account) } - await state.commit() + await stateManager.commit() } /**