UNPKG

@tokamak-zk-evm/synthesizer

Version:

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

439 lines 22.1 kB
import { EOFError, validationError } from './errors.js'; import { stackDelta } from './stackDelta.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 var ContainerSectionType; (function (ContainerSectionType) { ContainerSectionType[ContainerSectionType["InitCode"] = 0] = "InitCode"; ContainerSectionType[ContainerSectionType["DeploymentCode"] = 1] = "DeploymentCode"; ContainerSectionType[ContainerSectionType["RuntimeCode"] = 2] = "RuntimeCode"; })(ContainerSectionType = ContainerSectionType || (ContainerSectionType = {})); /** * 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, evm, mode = ContainerSectionType.RuntimeCode) { return validateOpcodes(container, evm, mode); } // Helper methods to read Int16s / Uint16s function readInt16(code, start) { return new DataView(code.buffer).getInt16(start); } function readUint16(code, start) { return new DataView(code.buffer).getUint16(start); } function validateOpcodes(container, evm, mode = ContainerSectionType.RuntimeCode) { // Track the intermediate bytes const intermediateBytes = new Set(); // Track the jump locations (for forward jumps it is unknown at the first pass if the byte is intermediate) const jumpLocations = new Set(); // Track the type of the container targets // Should at the end of the analysis have all the containers const containerTypeMap = new Map(); function addJump(location) { if (intermediateBytes.has(location)) { // When trying to JUMP into an intermediate byte: this is invalid validationError(EOFError.InvalidRJUMP); } jumpLocations.add(location); } function addIntermediate(location) { 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(); 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(); 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(); // Add all reachable code sections const reachableSections = {}; 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(); reachableOpcodes.add(0); // Validate that each opcode is defined let ptr = 0; let lastOpcode = 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 = [inputs]; const stackHeightMax = [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(); // 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(); 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; } //# sourceMappingURL=verify.js.map