UNPKG

@ethereumjs/evm

Version:
308 lines 14.2 kB
import { BinaryTreeAccessedStateType } from '@ethereumjs/common'; import { BIGINT_0, BINARY_TREE_BASIC_DATA_LEAF_KEY, BINARY_TREE_CODE_HASH_LEAF_KEY, BINARY_TREE_CODE_OFFSET, BINARY_TREE_HEADER_STORAGE_OFFSET, BINARY_TREE_MAIN_STORAGE_OFFSET, BINARY_TREE_NODE_WIDTH, bytesToHex, equalsBytes, getBinaryTreeIndicesForCodeChunk, getBinaryTreeIndicesForStorageSlot, getBinaryTreeKey, getBinaryTreeStem, hexToBytes, intToBytes, } from '@ethereumjs/util'; import debugDefault from 'debug'; import { ChunkCache } from "./chunkCache.js"; import { StemCache } from "./stemCache.js"; const debug = debugDefault('evm:binaryTree:aw'); /** * Tree key constants. */ const WitnessBranchReadCost = BigInt(1900); const WitnessChunkReadCost = BigInt(200); const WitnessBranchWriteCost = BigInt(3000); const WitnessChunkWriteCost = BigInt(500); const WitnessChunkFillCost = BigInt(6200); export function decodeBinaryAccessState(treeIndex, chunkIndex) { const position = BigInt(treeIndex) * BigInt(BINARY_TREE_NODE_WIDTH) + BigInt(chunkIndex); switch (position) { case BigInt(0): return { type: BinaryTreeAccessedStateType.BasicData }; case BigInt(1): return { type: BinaryTreeAccessedStateType.CodeHash }; default: if (position < BINARY_TREE_HEADER_STORAGE_OFFSET) { throw Error(`No attribute yet stored >=2 and <${BINARY_TREE_HEADER_STORAGE_OFFSET}`); } if (position >= BINARY_TREE_HEADER_STORAGE_OFFSET && position < BINARY_TREE_CODE_OFFSET) { const slot = position - BigInt(BINARY_TREE_HEADER_STORAGE_OFFSET); return { type: BinaryTreeAccessedStateType.Storage, slot }; } else if (position >= BINARY_TREE_CODE_OFFSET && position < BINARY_TREE_MAIN_STORAGE_OFFSET) { const codeChunkIdx = Number(position) - BINARY_TREE_CODE_OFFSET; return { type: BinaryTreeAccessedStateType.Code, codeOffset: codeChunkIdx * 31, }; } else if (position >= BINARY_TREE_MAIN_STORAGE_OFFSET) { const slot = BigInt(position - BINARY_TREE_MAIN_STORAGE_OFFSET); return { type: BinaryTreeAccessedStateType.Storage, slot }; } else { throw Error(`Invalid treeIndex=${treeIndex} chunkIndex=${chunkIndex} for binary tree access`); } } } export class BinaryTreeAccessWitness { constructor(opts) { this.stemCache = new StemCache(); this.chunkCache = new ChunkCache(); this.hashFunction = opts.hashFunction; this.stems = opts.stems ?? new Map(); this.chunks = opts.chunks ?? new Map(); } readAccountBasicData(address) { return this.touchAddressOnReadAndComputeGas(address, 0, BINARY_TREE_BASIC_DATA_LEAF_KEY); } writeAccountBasicData(address) { return this.touchAddressOnWriteAndComputeGas(address, 0, BINARY_TREE_BASIC_DATA_LEAF_KEY); } readAccountCodeHash(address) { return this.touchAddressOnReadAndComputeGas(address, 0, BINARY_TREE_CODE_HASH_LEAF_KEY); } writeAccountCodeHash(address) { return this.touchAddressOnWriteAndComputeGas(address, 0, BINARY_TREE_CODE_HASH_LEAF_KEY); } readAccountHeader(address) { let gas = BIGINT_0; gas += this.readAccountBasicData(address); gas += this.readAccountCodeHash(address); return gas; } writeAccountHeader(address) { let gas = BIGINT_0; gas += this.writeAccountBasicData(address); gas += this.writeAccountCodeHash(address); return gas; } readAccountCodeChunks(contract, startPc, endPc) { let gas = BIGINT_0; for (let chunkNum = Math.floor(startPc / 31); chunkNum <= Math.floor(endPc / 31); chunkNum++) { const { treeIndex, subIndex } = getBinaryTreeIndicesForCodeChunk(chunkNum); gas += this.touchAddressOnReadAndComputeGas(contract, treeIndex, subIndex); } return gas; } writeAccountCodeChunks(contract, startPc, endPc) { let gas = BIGINT_0; for (let chunkNum = Math.floor(startPc / 31); chunkNum <= Math.floor(endPc / 31); chunkNum++) { const { treeIndex, subIndex } = getBinaryTreeIndicesForCodeChunk(chunkNum); gas += this.touchAddressOnWriteAndComputeGas(contract, treeIndex, subIndex); } return gas; } readAccountStorage(address, storageSlot) { const { treeIndex, subIndex } = getBinaryTreeIndicesForStorageSlot(storageSlot); return this.touchAddressOnReadAndComputeGas(address, treeIndex, subIndex); } writeAccountStorage(address, storageSlot) { const { treeIndex, subIndex } = getBinaryTreeIndicesForStorageSlot(storageSlot); return this.touchAddressOnWriteAndComputeGas(address, treeIndex, subIndex); } touchAddressOnWriteAndComputeGas(address, treeIndex, subIndex) { return this.touchAddressAndComputeGas(address, treeIndex, subIndex, { isWrite: true, }); } touchAddressOnReadAndComputeGas(address, treeIndex, subIndex) { return this.touchAddressAndComputeGas(address, treeIndex, subIndex, { isWrite: false, }); } touchAddressAndComputeGas(address, treeIndex, subIndex, { isWrite }) { 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, treeIndex, subIndex, { isWrite } = {}) { 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 = getBinaryTreeStem(this.hashFunction, 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 = getBinaryTreeKey(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) { for (const [chunkKey, chunkValue] of accessWitness.chunks.entries()) { const stemKey = chunkKey.slice(0, chunkKey.length - 2); 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() { 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() { this.stemCache.clear(); this.chunkCache.clear(); } debugWitnessCost() { // Calculate the aggregate gas cost for binary 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() { for (const chunkKey of this.chunks.keys()) { // drop the last byte const stemKey = chunkKey.slice(0, chunkKey.length - 2); 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() { for (const rawAccess of this.rawAccesses()) { const { address, treeIndex, chunkIndex, chunkKey } = rawAccess; const accessedState = decodeBinaryAccessState(treeIndex, chunkIndex); yield { ...accessedState, address, chunkKey }; } } } /** * Generate a {@link BinaryTreeExecutionWitness} 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 binary tree execution witness */ export const generateBinaryExecutionWitness = async (stateManager, accessWitness, parentStateRoot) => { const tree = stateManager['_tree']; await tree['_lock'].acquire(); const postStateRoot = await stateManager.getStateRoot(); const ew = { stateDiff: [], parentStateRoot: bytesToHex(parentStateRoot), proof: undefined, // Binary proofs are not implemented }; // Generate a map of all stems with their accessed suffixes const accessedSuffixes = new Map(); for (const chunkKey of accessWitness['chunks'].keys()) { const stem = chunkKey.slice(0, 64); 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 tree for each stem and suffix for (const stem of accessedSuffixes.keys()) { tree.root(parentStateRoot); const suffixes = accessedSuffixes.get(stem); if (suffixes === undefined || suffixes.length === 0) continue; const currentValues = await tree.get(hexToBytes(stem), suffixes); tree.root(postStateRoot); const newValues = await tree.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 }); } tree['_lock'].release(); return ew; }; //# sourceMappingURL=binaryTreeAccessWitness.js.map