UNPKG

@tokamak-zk-evm/synthesizer

Version:

Tokamak zk-EVM Synthesizer - Processes Ethereum transactions into wire maps for Tokamak zk-SNARK proof generation

341 lines (289 loc) 11.2 kB
import { VerkleAccessedStateType } from '@synthesizer-libs/common' import { BIGINT_0, 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, getVerkleKey, getVerkleStem, getVerkleTreeIndicesForCodeChunk, intToBytes, } from "@synthesizer-libs/util" import debugDefault from 'debug' import type { AccessEventFlags, RawVerkleAccessedState, VerkleAccessWitnessInterface, VerkleAccessedState, VerkleAccessedStateWithAddress, } from '@synthesizer-libs/common' import type { Address, PrefixedHexString, VerkleCrypto } from '@synthesizer-libs/util' const debug = debugDefault('statemanager: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 type StemAccessEvent = { write?: boolean } // chunk fill access event is not being charged right now in kaustinen but will be rectified // in upcoming iterations type ChunkAccessEvent = StemAccessEvent & { fill?: boolean } // Since stem is pedersen hashed, it is useful to maintain the reverse relationship type StemMeta = { address: Address; treeIndex: number | bigint } export class VerkleAccessWitness implements VerkleAccessWitnessInterface { stems: Map<PrefixedHexString, StemAccessEvent & StemMeta> chunks: Map<PrefixedHexString, ChunkAccessEvent> verkleCrypto: VerkleCrypto constructor(opts: { verkleCrypto: VerkleCrypto stems?: Map<PrefixedHexString, StemAccessEvent & StemMeta> chunks?: Map<PrefixedHexString, ChunkAccessEvent> }) { if (opts.verkleCrypto === undefined) { throw new Error('verkle crypto required') } this.verkleCrypto = opts.verkleCrypto this.stems = opts.stems ?? new Map<PrefixedHexString, StemAccessEvent & StemMeta>() this.chunks = opts.chunks ?? new Map<PrefixedHexString, ChunkAccessEvent>() } touchAndChargeProofOfAbsence(address: Address): bigint { let gas = BIGINT_0 gas += this.touchAddressOnReadAndComputeGas(address, 0, VERKLE_BASIC_DATA_LEAF_KEY) gas += this.touchAddressOnReadAndComputeGas(address, 0, VERKLE_CODE_HASH_LEAF_KEY) return gas } touchAndChargeMessageCall(address: Address): bigint { let gas = BIGINT_0 gas += this.touchAddressOnReadAndComputeGas(address, 0, VERKLE_BASIC_DATA_LEAF_KEY) return gas } touchAndChargeValueTransfer(target: Address): bigint { let gas = BIGINT_0 gas += this.touchAddressOnWriteAndComputeGas(target, 0, VERKLE_BASIC_DATA_LEAF_KEY) return gas } touchAndChargeContractCreateInit(address: Address): bigint { let gas = BIGINT_0 gas += this.touchAddressOnWriteAndComputeGas(address, 0, VERKLE_BASIC_DATA_LEAF_KEY) return gas } touchAndChargeContractCreateCompleted(address: Address): bigint { let gas = BIGINT_0 gas += this.touchAddressOnWriteAndComputeGas(address, 0, VERKLE_BASIC_DATA_LEAF_KEY) gas += this.touchAddressOnWriteAndComputeGas(address, 0, VERKLE_CODE_HASH_LEAF_KEY) return gas } touchTxOriginAndComputeGas(origin: Address): bigint { let gas = BIGINT_0 gas += this.touchAddressOnReadAndComputeGas(origin, 0, VERKLE_BASIC_DATA_LEAF_KEY) gas += this.touchAddressOnReadAndComputeGas(origin, 0, VERKLE_CODE_HASH_LEAF_KEY) return gas } touchTxTargetAndComputeGas(target: Address, { sendsValue }: { sendsValue?: boolean } = {}) { let gas = BIGINT_0 gas += this.touchAddressOnReadAndComputeGas(target, 0, VERKLE_CODE_HASH_LEAF_KEY) if (sendsValue === true) { gas += this.touchAddressOnWriteAndComputeGas(target, 0, VERKLE_BASIC_DATA_LEAF_KEY) } else { gas += this.touchAddressOnReadAndComputeGas(target, 0, VERKLE_BASIC_DATA_LEAF_KEY) } return gas } touchCodeChunksRangeOnReadAndChargeGas(contact: 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(contact, treeIndex, subIndex) } return gas } touchCodeChunksRangeOnWriteAndChargeGas( contact: 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(contact, treeIndex, subIndex) } return gas } touchAddressOnWriteAndComputeGas( address: Address, treeIndex: number | bigint, subIndex: number | Uint8Array, ): bigint { return this.touchAddressAndChargeGas(address, treeIndex, subIndex, { isWrite: true }) } touchAddressOnReadAndComputeGas( address: Address, treeIndex: number | bigint, subIndex: number | Uint8Array, ): bigint { return this.touchAddressAndChargeGas(address, treeIndex, subIndex, { isWrite: false }) } touchAddressAndChargeGas( 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( `touchAddressAndChargeGas=${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.stems.get(accessedStemHex) if (accessedStem === undefined) { stemRead = true accessedStem = { address, treeIndex } this.stems.set(accessedStemHex, accessedStem) } const accessedChunkKey = getVerkleKey( accessedStemKey, typeof subIndex === 'number' ? intToBytes(subIndex) : subIndex, ) const accessedChunkKeyHex = bytesToHex(accessedChunkKey) let accessedChunk = this.chunks.get(accessedChunkKeyHex) if (accessedChunk === undefined) { chunkRead = true accessedChunk = {} this.chunks.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 steamRead=${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 } } } *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 } } } } 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`, ) } } }