@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
JavaScript
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