UNPKG

@ethereumjs/evm

Version:
458 lines (407 loc) 15.3 kB
import { VerkleAccessedStateType } from '@ethereumjs/common' import { BIGINT_0, EthereumJSErrorWithoutCode, VERKLE_BASIC_DATA_LEAF_KEY, VERKLE_CODE_HASH_LEAF_KEY, VERKLE_CODE_OFFSET, VERKLE_HEADER_STORAGE_OFFSET, VERKLE_MAIN_STORAGE_OFFSET, VERKLE_NODE_WIDTH, bytesToHex, equalsBytes, getVerkleKey, getVerkleStem, getVerkleTreeIndicesForCodeChunk, getVerkleTreeIndicesForStorageSlot, hexToBytes, intToBytes, } from '@ethereumjs/util' import debugDefault from 'debug' import { ChunkCache } from './chunkCache.ts' import { StemCache } from './stemCache.ts' import type { AccessEventFlags, RawVerkleAccessedState, VerkleAccessWitnessInterface, VerkleAccessedState, VerkleAccessedStateWithAddress, } from '@ethereumjs/common' import type { StatefulVerkleStateManager } from '@ethereumjs/statemanager' import type { Address, PrefixedHexString, VerkleCrypto, VerkleExecutionWitness, } from '@ethereumjs/util' import type { VerkleTree } from '@ethereumjs/verkle' const debug = debugDefault('evm:verkle:aw') /** * Tree key constants. */ const WitnessBranchReadCost = BigInt(1900) const WitnessChunkReadCost = BigInt(200) const WitnessBranchWriteCost = BigInt(3000) const WitnessChunkWriteCost = BigInt(500) const WitnessChunkFillCost = BigInt(6200) // read is a default access event if stem or chunk is present export type StemAccessEvent = { write?: boolean } // chunk fill access event is not being charged right now in kaustinen but will be rectified // in upcoming iterations export type ChunkAccessEvent = StemAccessEvent & { fill?: boolean } // Since stem is pedersen hashed, it is useful to maintain the reverse relationship export type StemMeta = { address: Address; treeIndex: number | bigint } export function decodeAccessedState( treeIndex: number | bigint, chunkIndex: number, ): VerkleAccessedState { const position = BigInt(treeIndex) * BigInt(VERKLE_NODE_WIDTH) + BigInt(chunkIndex) switch (position) { case BigInt(0): return { type: VerkleAccessedStateType.BasicData } case BigInt(1): return { type: VerkleAccessedStateType.CodeHash } default: if (position < VERKLE_HEADER_STORAGE_OFFSET) { throw Error(`No attribute yet stored >=2 and <${VERKLE_HEADER_STORAGE_OFFSET}`) } if (position >= VERKLE_HEADER_STORAGE_OFFSET && position < VERKLE_CODE_OFFSET) { const slot = position - BigInt(VERKLE_HEADER_STORAGE_OFFSET) return { type: VerkleAccessedStateType.Storage, slot } } else if (position >= VERKLE_CODE_OFFSET && position < VERKLE_MAIN_STORAGE_OFFSET) { const codeChunkIdx = Number(position) - VERKLE_CODE_OFFSET return { type: VerkleAccessedStateType.Code, codeOffset: codeChunkIdx * 31, } } else if (position >= VERKLE_MAIN_STORAGE_OFFSET) { const slot = BigInt(position - VERKLE_MAIN_STORAGE_OFFSET) return { type: VerkleAccessedStateType.Storage, slot } } else { throw Error( `Invalid treeIndex=${treeIndex} chunkIndex=${chunkIndex} for verkle tree access`, ) } } } export class VerkleAccessWitness implements VerkleAccessWitnessInterface { stems: Map<PrefixedHexString, StemAccessEvent & StemMeta> chunks: Map<PrefixedHexString, ChunkAccessEvent> stemCache: StemCache = new StemCache() chunkCache: ChunkCache = new ChunkCache() verkleCrypto: VerkleCrypto constructor(opts: { verkleCrypto: VerkleCrypto stems?: Map<PrefixedHexString, StemAccessEvent & StemMeta> chunks?: Map<PrefixedHexString, ChunkAccessEvent> }) { if (opts.verkleCrypto === undefined) { throw EthereumJSErrorWithoutCode('verkle crypto required') } this.verkleCrypto = opts.verkleCrypto this.stems = opts.stems ?? new Map<PrefixedHexString, StemAccessEvent & StemMeta>() this.chunks = opts.chunks ?? new Map<PrefixedHexString, ChunkAccessEvent>() } readAccountBasicData(address: Address): bigint { return this.touchAddressOnReadAndComputeGas(address, 0, VERKLE_BASIC_DATA_LEAF_KEY) } writeAccountBasicData(address: Address): bigint { return this.touchAddressOnWriteAndComputeGas(address, 0, VERKLE_BASIC_DATA_LEAF_KEY) } readAccountCodeHash(address: Address): bigint { return this.touchAddressOnReadAndComputeGas(address, 0, VERKLE_CODE_HASH_LEAF_KEY) } writeAccountCodeHash(address: Address): bigint { return this.touchAddressOnWriteAndComputeGas(address, 0, VERKLE_CODE_HASH_LEAF_KEY) } readAccountHeader(address: Address): bigint { let gas = BIGINT_0 gas += this.readAccountBasicData(address) gas += this.readAccountCodeHash(address) return gas } writeAccountHeader(address: Address): bigint { let gas = BIGINT_0 gas += this.writeAccountBasicData(address) gas += this.writeAccountCodeHash(address) return gas } readAccountCodeChunks(contract: Address, startPc: number, endPc: number): bigint { let gas = BIGINT_0 for (let chunkNum = Math.floor(startPc / 31); chunkNum <= Math.floor(endPc / 31); chunkNum++) { const { treeIndex, subIndex } = getVerkleTreeIndicesForCodeChunk(chunkNum) gas += this.touchAddressOnReadAndComputeGas(contract, treeIndex, subIndex) } return gas } writeAccountCodeChunks(contract: Address, startPc: number, endPc: number): bigint { let gas = BIGINT_0 for (let chunkNum = Math.floor(startPc / 31); chunkNum <= Math.floor(endPc / 31); chunkNum++) { const { treeIndex, subIndex } = getVerkleTreeIndicesForCodeChunk(chunkNum) gas += this.touchAddressOnWriteAndComputeGas(contract, treeIndex, subIndex) } return gas } readAccountStorage(address: Address, storageSlot: bigint): bigint { const { treeIndex, subIndex } = getVerkleTreeIndicesForStorageSlot(storageSlot) return this.touchAddressOnReadAndComputeGas(address, treeIndex, subIndex) } writeAccountStorage(address: Address, storageSlot: bigint): bigint { const { treeIndex, subIndex } = getVerkleTreeIndicesForStorageSlot(storageSlot) return this.touchAddressOnWriteAndComputeGas(address, treeIndex, subIndex) } touchAddressOnWriteAndComputeGas( address: Address, treeIndex: number | bigint, subIndex: number | Uint8Array, ): bigint { return this.touchAddressAndComputeGas(address, treeIndex, subIndex, { isWrite: true, }) } touchAddressOnReadAndComputeGas( address: Address, treeIndex: number | bigint, subIndex: number | Uint8Array, ): bigint { return this.touchAddressAndComputeGas(address, treeIndex, subIndex, { isWrite: false, }) } touchAddressAndComputeGas( address: Address, treeIndex: number | bigint, subIndex: number | Uint8Array, { isWrite }: { isWrite?: boolean }, ): bigint { let gas = BIGINT_0 const { stemRead, stemWrite, chunkRead, chunkWrite, chunkFill } = this.touchAddress( address, treeIndex, subIndex, { isWrite }, ) if (stemRead === true) { gas += WitnessBranchReadCost } if (stemWrite === true) { gas += WitnessBranchWriteCost } if (chunkRead === true) { gas += WitnessChunkReadCost } if (chunkWrite === true) { gas += WitnessChunkWriteCost } if (chunkFill === true) { gas += WitnessChunkFillCost } debug( `touchAddressAndComputeGas=${gas} address=${address} treeIndex=${treeIndex} subIndex=${subIndex}`, ) return gas } touchAddress( address: Address, treeIndex: number | bigint, subIndex: number | Uint8Array, { isWrite }: { isWrite?: boolean } = {}, ): AccessEventFlags { let stemRead = false, stemWrite = false, chunkRead = false, chunkWrite = false // currently there are no gas charges for setting the chunk for the first time // i.e. no fill cost is charged right now const chunkFill = false const accessedStemKey = getVerkleStem(this.verkleCrypto, address, treeIndex) const accessedStemHex = bytesToHex(accessedStemKey) let accessedStem = this.stemCache.get(accessedStemHex) ?? this.stems.get(accessedStemHex) if (accessedStem === undefined) { stemRead = true accessedStem = { address, treeIndex } this.stemCache.set(accessedStemHex, accessedStem) } const accessedChunkKey = getVerkleKey( accessedStemKey, typeof subIndex === 'number' ? intToBytes(subIndex) : subIndex, ) const accessedChunkKeyHex = bytesToHex(accessedChunkKey) let accessedChunk = this.chunkCache.get(accessedChunkKeyHex) ?? this.chunks.get(accessedChunkKeyHex) if (accessedChunk === undefined) { chunkRead = true accessedChunk = {} this.chunkCache.set(accessedChunkKeyHex, accessedChunk) } if (isWrite === true) { if (accessedStem.write !== true) { stemWrite = true // this would also directly modify in the map accessedStem.write = true } if (accessedChunk.write !== true) { chunkWrite = true // this would also directly modify in the map accessedChunk.write = true } } debug( `${accessedChunkKeyHex}: isWrite=${isWrite} for stemRead=${stemRead} stemWrite=${stemWrite} chunkRead=${chunkRead} chunkWrite=${chunkWrite} chunkFill=${chunkFill}`, ) return { stemRead, stemWrite, chunkRead, chunkWrite, chunkFill } } merge(accessWitness: VerkleAccessWitness): void { for (const [chunkKey, chunkValue] of accessWitness.chunks.entries()) { const stemKey = chunkKey.slice(0, chunkKey.length - 2) as PrefixedHexString const stem = accessWitness.stems.get(stemKey) if (stem === undefined) { throw Error(`Internal error: missing stem for the chunkKey=${chunkKey}`) } const thisStem = this.stems.get(stemKey) if (thisStem === undefined) { this.stems.set(stemKey, stem) } else { thisStem.write = thisStem.write !== true ? stem.write : true } const thisChunk = this.chunks.get(chunkKey) if (thisChunk === undefined) { this.chunks.set(chunkKey, chunkValue) } else { thisChunk.write = thisChunk.write !== true ? chunkValue.write : true thisChunk.fill = thisChunk.fill !== true ? thisChunk.fill : true } } } commit(): void { const cachedStems = this.stemCache.commit() for (const [stemKey, stemValue] of cachedStems) { this.stems.set(stemKey, stemValue) } const cachedChunks = this.chunkCache.commit() for (const [chunkKey, chunkValue] of cachedChunks) { this.chunks.set(chunkKey, chunkValue) } } revert(): void { this.stemCache.clear() this.chunkCache.clear() } debugWitnessCost(): void { // Calculate the aggregate gas cost for verkle access witness per type let stemReads = 0, stemWrites = 0, chunkReads = 0, chunkWrites = 0 for (const [_, { write }] of this.stems.entries()) { stemReads++ if (write === true) { stemWrites++ } } for (const [_, { write }] of this.chunks.entries()) { chunkReads++ if (write === true) { chunkWrites++ } } debug( `${stemReads} stem reads, totalling ${BigInt(stemReads) * WitnessBranchReadCost} gas units`, ) debug( `${stemWrites} stem writes, totalling ${BigInt(stemWrites) * WitnessBranchWriteCost} gas units`, ) debug( `${chunkReads} chunk reads, totalling ${BigInt(chunkReads) * WitnessChunkReadCost} gas units`, ) debug( `${chunkWrites} chunk writes, totalling ${BigInt(chunkWrites) * WitnessChunkWriteCost} gas units`, ) } *rawAccesses(): Generator<RawVerkleAccessedState> { for (const chunkKey of this.chunks.keys()) { // drop the last byte const stemKey = chunkKey.slice(0, chunkKey.length - 2) as PrefixedHexString const stem = this.stems.get(stemKey) if (stem === undefined) { throw Error(`Internal error: missing stem for the chunkKey=${chunkKey}`) } const { address, treeIndex } = stem const chunkIndex = Number(`0x${chunkKey.slice(chunkKey.length - 2)}`) const accessedState = { address, treeIndex, chunkIndex, chunkKey } yield accessedState } } *accesses(): Generator<VerkleAccessedStateWithAddress> { for (const rawAccess of this.rawAccesses()) { const { address, treeIndex, chunkIndex, chunkKey } = rawAccess const accessedState = decodeAccessedState(treeIndex, chunkIndex) yield { ...accessedState, address, chunkKey } } } } /** * Generate a {@link VerkleExecutionWitness} from a state manager and an access witness. * @param stateManager - The state manager containing the state to generate the witness for. * @param accessWitness - The access witness containing the accessed states. * @param parentStateRoot - The parent state root (i.e. prestate root) to generate the witness for. * @returns The generated verkle execution witness * * Note: This does not provide the verkle proof, which is not implemented */ export const generateExecutionWitness = async ( stateManager: StatefulVerkleStateManager, accessWitness: VerkleAccessWitness, parentStateRoot: Uint8Array, ): Promise<VerkleExecutionWitness> => { const trie = stateManager['_trie'] as VerkleTree await trie['_lock'].acquire() const postStateRoot = await stateManager.getStateRoot() const ew: VerkleExecutionWitness = { stateDiff: [], parentStateRoot: bytesToHex(parentStateRoot), /// @ts-expect-error -- Verkle proofs are unimplemented verkleProof: undefined, } // Generate a map of all stems with their accessed suffixes const accessedSuffixes = new Map<PrefixedHexString, number[]>() for (const chunkKey of accessWitness['chunks'].keys()) { const stem = chunkKey.slice(0, 64) as PrefixedHexString if (accessedSuffixes.has(stem)) { const suffixes = accessedSuffixes.get(stem) suffixes!.push(parseInt(chunkKey.slice(64), 16)) accessedSuffixes.set(stem, suffixes!) } else { accessedSuffixes.set(stem, [parseInt(chunkKey.slice(64), 16)]) } } // Get values from the trie for each stem and suffix for (const stem of accessedSuffixes.keys()) { trie.root(parentStateRoot) const suffixes = accessedSuffixes.get(stem) if (suffixes === undefined || suffixes.length === 0) continue const currentValues = await trie.get(hexToBytes(stem), suffixes) trie.root(postStateRoot) const newValues = await trie.get(hexToBytes(stem), suffixes) const stemStateDiff = [] for (let x = 0; x < suffixes.length; x++) { // skip if both are the same const currentValue = currentValues[x] const newValue = newValues[x] if ( currentValue instanceof Uint8Array && newValue instanceof Uint8Array && equalsBytes(currentValue, newValue) ) continue stemStateDiff.push({ suffix: suffixes[x], currentValue: currentValue ? bytesToHex(currentValue) : null, newValue: newValue ? bytesToHex(newValue) : null, }) } ew.stateDiff.push({ stem, suffixDiffs: stemStateDiff }) } trie['_lock'].release() return ew }