UNPKG

@tokamak-zk-evm/synthesizer

Version:

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

519 lines (439 loc) 19.5 kB
import { EOFError, validationError } from './errors.js' import { stackDelta } from './stackDelta.js' import type { EVM } from '../evm.js' import type { EOFContainer } from './container.js' /** * Note for reviewers regarding these flags: these only reside inside `verify.ts` (this file) * and `container.ts`. For `container.ts`, the only behavior which ever changes is in the `DeploymentCode` mode * This `DeploymentCode` mode means that the subcontainer is flagged in such way that this container is launched * in a "deployment" mode. This means, that the data section of the body is actually allowed to contain * less data than is written in the header. However, once the target container (by the container in deployment) * mode is returned by RETURNCONTRACT it should have at least the header amount of data. * See also "data section lifecycle" * Note: the subcontainers of a container can be marked "InitCode" or "DeploymentCode". * InitCode cannot contain the instructions RETURN / STOP * InitCode is the only container type which can contain RETURNCONTRACT * A container can also be marked DeploymentCode, this is a subcontainer targeted by RETURNCONTRACT * A container cannot be marked both InitCode and DeploymentCode * This flag is thus to distinguish between subcontainers, and also thus also allows for data section sizes * lower than the size in the header in case of `InitCode` */ export enum ContainerSectionType { InitCode, // Targeted by EOFCreate DeploymentCode, // Targeted by RETURNCONTRACT RuntimeCode, // "Default" runtime code } /** * This method validates an EOF container deeply. It will validate the opcodes, validate the stack, and performs * various checks such as checking for forbidden opcodes in certain modes, jumps to invalid places, etc. * For more information, see "Code validation" of https://github.com/ipsilon/eof/blob/main/spec/eof.md * This is a compilation of all the extra validation rules introduced by the various EIPs * In particular, the stack validation EIP https://eips.ethereum.org/EIPS/eip-5450 is a big part here * @param container EOFContainer to verify * @param evm The EVM to run in (pulls opcodes from here) * @param mode The validation mode to run in * @returns Returns a Map which marks what ContainerSectionType each container is * NOTE: this should likely not be a map, since a container section can only be of a single type, not multiple */ export function verifyCode( container: EOFContainer, evm: EVM, mode: ContainerSectionType = ContainerSectionType.RuntimeCode, ) { return validateOpcodes(container, evm, mode) } // Helper methods to read Int16s / Uint16s function readInt16(code: Uint8Array, start: number) { return new DataView(code.buffer).getInt16(start) } function readUint16(code: Uint8Array, start: number) { return new DataView(code.buffer).getUint16(start) } function validateOpcodes( container: EOFContainer, evm: EVM, mode: ContainerSectionType = ContainerSectionType.RuntimeCode, ) { // Track the intermediate bytes const intermediateBytes = new Set<number>() // Track the jump locations (for forward jumps it is unknown at the first pass if the byte is intermediate) const jumpLocations = new Set<number>() // Track the type of the container targets // Should at the end of the analysis have all the containers const containerTypeMap = new Map<number, ContainerSectionType>() function addJump(location: number) { if (intermediateBytes.has(location)) { // When trying to JUMP into an intermediate byte: this is invalid validationError(EOFError.InvalidRJUMP) } jumpLocations.add(location) } function addIntermediate(location: number) { if (jumpLocations.has(location)) { // When trying to add an intermediate to a location already JUMPed to: this is invalid validationError(EOFError.InvalidRJUMP) } intermediateBytes.add(location) } // TODO (?) -> stackDelta currently only has active EOF opcodes, can use it directly (?) // (so no need to generate the valid opcodeNumbers) // Validate each code section const opcodes = evm.getActiveOpcodes() const opcodeNumbers = new Set<number>() for (const [key] of opcodes) { opcodeNumbers.add(key) } // Add INVALID as valid opcodeNumbers.add(0xfe) // Remove CODESIZE, CODECOPY, EXTCODESIZE, EXTCODECOPY, EXTCODEHASH, GAS opcodeNumbers.delete(0x38) opcodeNumbers.delete(0x39) opcodeNumbers.delete(0x5a) opcodeNumbers.delete(0x3b) opcodeNumbers.delete(0x3c) opcodeNumbers.delete(0x3f) // Remove CALLCODE and SELFDESTRUCT opcodeNumbers.delete(0xf2) opcodeNumbers.delete(0xff) // TODO omnibus https://github.com/ipsilon/eof/blob/main/spec/eof.md states // JUMP / JUMPI / PC / CREATE / CREATE2 also banned // This is not in the EIPs yet // Add these opcodes here opcodeNumbers.delete(0x56) // JUMP opcodeNumbers.delete(0x57) // JUMPI opcodeNumbers.delete(0x58) // PC opcodeNumbers.delete(0xf0) // CREATE opcodeNumbers.delete(0xf5) // CREATE2 // Note: Name might be misleading since this is the list of opcodes which are OK as final opcodes in a code section // TODO if using stackDelta for EOF it is possible to add a "termination" boolean for the opcode to mark it as terminating // (so no need to generate this set here) const terminatingOpcodes = new Set<number>() terminatingOpcodes.add(0x00) // STOP terminatingOpcodes.add(0xf3) // RETURN terminatingOpcodes.add(0xfd) // REVERT terminatingOpcodes.add(0xfe) // INVALID terminatingOpcodes.add(0xee) // RETURNCONTRACT terminatingOpcodes.add(0xe4) // RETF terminatingOpcodes.add(0xe5) // JUMPF terminatingOpcodes.add(0xe0) // RJUMPing back into code section is OK for (const opcode of terminatingOpcodes) { if (!opcodeNumbers.has(opcode)) { terminatingOpcodes.delete(opcode) } } const validJumps = new Set<number>() // Add all reachable code sections const reachableSections: { [key: number]: Set<number> } = {} let codeSection = -1 for (const code of container.body.codeSections) { codeSection++ reachableSections[codeSection] = new Set() const returningFunction = container.body.typeSections[codeSection].outputs === 0x80 // Tracking set of reachable opcodes const reachableOpcodes = new Set<number>() reachableOpcodes.add(0) // Validate that each opcode is defined let ptr = 0 let lastOpcode: number = 0 // Note: code sections cannot be empty, so this number will always be set // Implement the EIP 5450 stack validation algorithm const inputs = container.body.typeSections[codeSection].inputs let maxStackHeight = inputs // These arrays track the min/max stack height **before** executing the instruction const stackHeightMin: number[] = [inputs] const stackHeightMax: number[] = [inputs] // Loop over the entire code section and validate various rules // For (most) validation rules, see https://github.com/ipsilon/eof/blob/main/spec/eof.md // For all validation rules per opcode, find the corresponding EIP, the rules are there while (ptr < code.length) { // Tracks the successor opcodes of this opcode (for stack purposes) const successorSet = new Set<number>() // ReachableOpcodes: this can likely be deleted after implementing the 5450 algorithm if (!reachableOpcodes.has(ptr)) { validationError(EOFError.UnreachableCode) } if (stackHeightMin[ptr] === undefined || stackHeightMax[ptr] === undefined) { // Code is either unreachable or only reachable via a backwards jump validationError(EOFError.UnreachableCode) } validJumps.add(ptr) const opcode = code[ptr] const minStackCurrent = stackHeightMin[ptr] const maxStackCurrent = stackHeightMax[ptr] const opcodeInputs = stackDelta[opcode].inputs const opcodeOutputs = stackDelta[opcode].outputs if (minStackCurrent - opcodeInputs < 0) { validationError(EOFError.StackUnderflow) } const delta = opcodeOutputs - opcodeInputs let minStackNext = minStackCurrent + delta let maxStackNext = maxStackCurrent + delta if (maxStackNext > 1023) { // TODO verify if 1023 or 1024 is the right constant validationError(EOFError.StackOverflow) } if (returningFunction && opcode === 0xe4) { validationError(EOFError.InvalidReturningSection) } lastOpcode = opcode if (!opcodeNumbers.has(opcode)) { validationError(EOFError.InvalidOpcode) } if (opcode === 0xe0 || opcode === 0xe1) { // RJUMP / RJUMPI const target = readInt16(code, ptr + 1) + ptr + 3 if (target < 0 || target >= code.length) { validationError(EOFError.InvalidRJUMP) } successorSet.add(target) addJump(target) reachableOpcodes.add(target) if (opcode === 0xe0) { // For RJUMP check that the instruction after RJUMP is reachable // If not the case then it is not yet targeted by a forward jump // and hence violates the spec if (!reachableOpcodes.has(ptr + 3) && ptr + 3 < code.length) { // Note: the final condition above ensures that the bytes after ptr are there // This is an edge case, if the container ends with RJUMP (which is valid) validationError(EOFError.UnreachableCode) } } } else if (opcode === 0xe2) { // RJUMPV const tableSize = code[ptr + 1] + 1 if (tableSize === undefined) { validationError(EOFError.OpcodeIntermediatesOOB) } else if (tableSize === 0) { validationError(EOFError.RJUMPVTableSize0) } if (ptr + tableSize * 2 + 2 >= code.length) { // Fall-through case validationError(EOFError.OpcodeIntermediatesOOB) } const newPc = ptr + 2 + tableSize * 2 for (let i = 0; i < tableSize; i++) { const newPtr = ptr + 2 + i * 2 // Add the table bytes to intermediates addIntermediate(newPtr) addIntermediate(newPtr + 1) const target = readInt16(code, newPtr) + newPc if (target < 0 || target >= code.length) { validationError(EOFError.OpcodeIntermediatesOOB) } successorSet.add(target) addJump(target) reachableOpcodes.add(target) } // Special case for RJUMPV: move ptr over the table (the immediate starting byte will be added later) // In this special case, add the immediate starting byte addIntermediate(ptr + 1) ptr += 2 * tableSize + 1 } else if (opcode === 0xe3 || opcode === 0xe5) { // CALLF / JUMPF const target = readUint16(code, ptr + 1) reachableSections[codeSection].add(target) if (target >= container.header.codeSizes.length) { validationError(EOFError.InvalidCallTarget) } if (opcode === 0xe3) { // CALLF const targetOutputs = container.body.typeSections[target].outputs const targetInputs = container.body.typeSections[target].inputs if (targetOutputs === 0x80) { // CALLF points to non-returning function which is not allowed validationError(EOFError.InvalidCALLFReturning) } if (minStackCurrent < targetInputs) { validationError(EOFError.StackUnderflow) } if ( maxStackCurrent + container.body.typeSections[target].maxStackHeight - targetInputs > 1024 ) { validationError(EOFError.StackOverflow) } minStackNext += targetOutputs - targetInputs maxStackNext += targetOutputs - targetInputs } else { // JUMPF const currentOutputs = container.body.typeSections[codeSection].outputs const targetOutputs = container.body.typeSections[target].outputs const targetInputs = container.body.typeSections[target].inputs const targetNonReturning = targetOutputs === 0x80 if (targetOutputs > currentOutputs && !targetNonReturning) { // Spec rule: // JUMPF operand must point to a code section with equal or fewer number of outputs as // the section in which it resides, or to a section with 0x80 as outputs (non-returning) validationError(EOFError.InvalidJUMPF) } if (returningFunction && targetOutputs <= 0x7f) { // Current function is returning, but target is not, cannot jump into this validationError(EOFError.InvalidReturningSection) } if (targetNonReturning) { // Target is returning if (minStackCurrent < targetInputs) { validationError(EOFError.StackUnderflow) } } else { // Target is returning const expectedStack = currentOutputs + targetInputs - targetOutputs if (!(minStackCurrent === maxStackCurrent && maxStackCurrent === expectedStack)) { validationError(EOFError.InvalidStackHeight) } } if ( maxStackCurrent + container.body.typeSections[target].maxStackHeight - targetInputs > 1024 ) { //console.log(maxStackCurrent, targetOutputs, targetInputs, targetNonReturning) validationError(EOFError.StackOverflow) } } } else if (opcode === 0xe4) { // RETF // Stack height must match the outputs of current code section const outputs = container.body.typeSections[codeSection].outputs if (!(minStackCurrent === maxStackCurrent && maxStackCurrent === outputs)) { validationError(EOFError.InvalidStackHeight) } } else if (opcode === 0xe6) { // DUPN const toDup = code[ptr + 1] if (toDup + 1 > minStackCurrent) { validationError(EOFError.StackUnderflow) } } else if (opcode === 0xe7) { // SWAPN const toSwap = code[ptr + 1] // TODO: EVMONEs test wants this to be `toSwap + 2`, but that seems to be incorrect // Will keep `toSwap + 1` for now if (toSwap + 1 > minStackCurrent) { validationError(EOFError.StackUnderflow) } } else if (opcode === 0xe8) { // EXCHANGE const exchangeRaw = code[ptr + 1] const n = (exchangeRaw >> 4) + 1 const m = (exchangeRaw & 0x0f) + 1 if (n + m + 1 > minStackCurrent) { validationError(EOFError.StackUnderflow) } } else if (opcode === 0xec) { // EOFCREATE const target = code[ptr + 1] if (target >= container.header.containerSizes.length) { validationError(EOFError.InvalidEOFCreateTarget) } if (containerTypeMap.has(target)) { if (containerTypeMap.get(target) !== ContainerSectionType.InitCode) { validationError(EOFError.ContainerDoubleType) } } containerTypeMap.set(target, ContainerSectionType.InitCode) } else if (opcode === 0xee) { // RETURNCONTRACT if (mode !== ContainerSectionType.InitCode) { validationError(EOFError.ContainerTypeError) } const target = code[ptr + 1] if (target >= container.header.containerSizes.length) { validationError(EOFError.InvalidRETURNContractTarget) } if (containerTypeMap.has(target)) { if (containerTypeMap.get(target) !== ContainerSectionType.DeploymentCode) { validationError(EOFError.ContainerDoubleType) } } containerTypeMap.set(target, ContainerSectionType.DeploymentCode) } else if (opcode === 0xd1) { // DATALOADN const dataTarget = readUint16(code, ptr + 1) const endOfSlice = dataTarget + 32 if (container.header.dataSize < endOfSlice) { validationError(EOFError.DataLoadNOutOfBounds) } } else if (opcode === 0x00 || opcode === 0xf3) { // STOP / RETURN if (mode === ContainerSectionType.InitCode) { validationError(EOFError.ContainerTypeError) } } // Move ptr forward over any intermediates (if any) // Note: for EOF this stackDelta is guaranteed to exist const intermediates = stackDelta[opcode].intermediates if (intermediates > 0) { for (let i = 1; i <= intermediates; i++) { addIntermediate(ptr + i) } ptr += intermediates // If the opcode has any intermediates, jump over it } if (ptr >= code.length) { validationError(EOFError.OpcodeIntermediatesOOB) } ptr++ // Move to next opcode if (stackDelta[opcode].terminating === undefined) { // If the opcode is not terminating we can add the next opcode to the reachable opcodes // It can be reached by sequential instruction flow reachableOpcodes.add(ptr) // Add next opcode to successorSet // NOTE: these are all opcodes except RJUMP if (opcode !== 0xe0) { successorSet.add(ptr) } } // TODO here validate stack / reachability and stack overflow check for (const successor of successorSet) { if (successor < ptr) { // Reached via backwards jump if ( stackHeightMin[successor] !== minStackNext || stackHeightMax[successor] !== maxStackNext ) { validationError(EOFError.UnstableStack) } } if (stackHeightMax[successor] === undefined) { // Target is seen for first time stackHeightMin[successor] = minStackNext stackHeightMax[successor] = maxStackNext } else { stackHeightMin[successor] = Math.min(stackHeightMin[successor], minStackNext) stackHeightMax[successor] = Math.max(stackHeightMax[successor], maxStackNext) } } maxStackHeight = Math.max(maxStackNext, maxStackHeight) } // Validate that the final opcode terminates if (!terminatingOpcodes.has(lastOpcode)) { validationError(EOFError.InvalidTerminator) } if (container.body.typeSections[codeSection].maxStackHeight !== maxStackHeight) { validationError(EOFError.MaxStackHeightViolation) } if (maxStackHeight > 1023) { // TODO verify if 1023 or 1024 is the right constant validationError(EOFError.MaxStackHeightLimit) } } // Verify that each code section can be reached from code section 0 const sectionAccumulator = new Set<number>() sectionAccumulator.add(0) // 0 is always reachable const toCheck = [0] while (toCheck.length > 0) { const checkArray = reachableSections[toCheck.pop()!] for (const checkSection of checkArray) { if (!sectionAccumulator.has(checkSection)) { // Only check the reachable section if sectionAccumulator.add(checkSection) toCheck.push(checkSection) } } } if (sectionAccumulator.size !== container.header.codeSizes.length) { validationError(EOFError.UnreachableCodeSections) } if (containerTypeMap.size !== container.header.containerSizes.length) { validationError(EOFError.UnreachableContainerSections) } return containerTypeMap }