UNPKG

@tokamak-zk-evm/synthesizer

Version:

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

462 lines (421 loc) 16 kB
import { CODE_MIN, CODE_SIZE_MIN, CONTAINER_MAX, CONTAINER_MIN, CONTAINER_SIZE_MIN, FORMAT, INPUTS_MAX, KIND_CODE, KIND_CONTAINER, KIND_DATA, KIND_TYPE, MAGIC, MAX_HEADER_SIZE, MAX_STACK_HEIGHT, OUTPUTS_MAX, TERMINATOR, TYPE_DIVISOR, TYPE_MAX, TYPE_MIN, VERSION, } from './constants.js' import { EOFError, validationError } from './errors.js' import { ContainerSectionType, verifyCode } from './verify.js' import type { EVM } from '../evm.js' /* This file creates EOF Containers EOF Containers are described in EIP-3540. A container consists of a header and a body. The header describes the layout of the body. The body has the actual "interesting" contents, such as the bytecode to run, the data section, and possibly yet-to-be-deployed containers (via EOFCREATE, to create new EOF contracts from an existing one) */ // This enum marks the "mode" of a container // Depending on this mode, certain extra checks for validity have to be done, or some checks can be skipped export enum EOFContainerMode { Default, // Default container validation Initmode, // Initmode container validation (for subcontainers pointed to by EOFCreate) TxInitmode, // Tx initmode container validation (for txs deploying EOF contracts) } // The StreamReader is a helper class to help reading byte arrays class StreamReader { private data: Uint8Array // Stream to read private ptr: number // Current pointer to where the stream is being read constructor(stream: Uint8Array) { this.data = stream this.ptr = 0 } /** * Read `amount` bytes from the stream. Throws when trying to read out of bounds with an optional error string. * This also updates the internal pointer * @param amount Bytes to read * @param errorStr Optional error string to throw when trying to read out-of-bounds * @returns The byte array with length `amount` */ readBytes(amount: number, errorStr?: string) { const end = this.ptr + amount if (end > this.data.length) { validationError(EOFError.OutOfBounds, this.ptr, errorStr) } const ptr = this.ptr this.ptr += amount return this.data.slice(ptr, end) } /** * Reads an Uint8. Also updates the pointer. * @param errorStr Optional error string * @returns The uint8 */ readUint(errorStr?: string) { if (this.ptr >= this.data.length) { validationError(EOFError.OutOfBounds, this.ptr, errorStr) } return this.data[this.ptr++] } /** * Verify that the current uint8 pointed to by the pointer is the expected uint8 * Also updates the pointer * @param expect The uint to expect * @param errorStr Optional error string when the read uint is not the expected uint */ verifyUint(expect: number, errorStr?: string) { if (this.readUint() !== expect) { validationError(EOFError.VerifyUint, this.ptr - 1, errorStr) } } /** * Same as readUint, except this reads an uint16 * @param errorStr * @returns */ readUint16(errorStr?: string) { const end = this.ptr + 2 if (end > this.data.length) { validationError(EOFError.OutOfBounds, this.ptr, errorStr) } const ptr = this.ptr this.ptr += 2 return new DataView(this.data.buffer).getUint16(ptr) } /** * Get the current pointer of the stream * @returns The pointer */ getPtr() { return this.ptr } // Get the remainder bytes of the current stream readRemainder() { return this.data.slice(this.ptr) } // Returns `true` if the stream is fully read, or false if there are dangling bytes isAtEnd() { return this.ptr === this.data.length } } // TODO add initcode flags (isEOFContract) // TODO validation: mark sections as either initcode or runtime code to validate /** * The EOFHeader, describing the header of the EOF container */ class EOFHeader { typeSize: number // Size of types section codeSizes: number[] // Sizes of code sections containerSizes: number[] // Sizes of containers dataSize: number // Size of data section dataSizePtr: number // Used to edit the dataSize in RETURNCONTRACT buffer: Uint8Array // The raw buffer of the entire header private codeStartPos: number[] // Internal array to track at which byte of the container the code starts (per section) /** * Create an EOF header. Performs various validation checks inside the constructor * @param input either a raw header or a complete container */ constructor(input: Uint8Array) { if (input.length > MAX_HEADER_SIZE) { throw new Error('err: container size more than maximum valid size') } const stream = new StreamReader(input) // Verify that the header starts with 0xEF0001 stream.verifyUint(FORMAT, EOFError.FORMAT) stream.verifyUint(MAGIC, EOFError.MAGIC) stream.verifyUint(VERSION, EOFError.VERSION) if (input.length < 15) { throw new Error('err: container size less than minimum valid size') } // Verify that the types section is present and its length is valid stream.verifyUint(KIND_TYPE, EOFError.KIND_TYPE) const typeSize = stream.readUint16(EOFError.TypeSize) if (typeSize < TYPE_MIN) { validationError(EOFError.InvalidTypeSize, typeSize) } if (typeSize % TYPE_DIVISOR !== 0) { validationError(EOFError.InvalidTypeSize, typeSize) } if (typeSize > TYPE_MAX) { throw new Error(`err: number of code sections must not exceed 1024 (got ${typeSize})`) } // Verify that the code section is present and its size is valid stream.verifyUint(KIND_CODE, EOFError.KIND_CODE) const codeSize = stream.readUint16(EOFError.CodeSize) if (codeSize < CODE_MIN) { validationError(EOFError.MinCodeSections) } if (codeSize !== typeSize / TYPE_DIVISOR) { validationError(EOFError.TypeSections, typeSize / TYPE_DIVISOR, codeSize) } // Read the actual code sizes in the code section and verify that each section has the minimum size const codeSizes = [] for (let i = 0; i < codeSize; i++) { const codeSectionSize = stream.readUint16(EOFError.CodeSection) if (codeSectionSize < CODE_SIZE_MIN) { validationError(EOFError.CodeSectionSize) } codeSizes.push(codeSectionSize) } // Check if there are container sections let nextSection = stream.readUint() const containerSizes: number[] = [] if (nextSection === KIND_CONTAINER) { // The optional container section is present, validate that the size is within bounds const containerSectionSize = stream.readUint16(EOFError.ContainerSize) if (containerSectionSize < CONTAINER_MIN) { validationError(EOFError.ContainerSectionSize) } if (containerSectionSize > CONTAINER_MAX) { validationError(EOFError.ContainerSectionSize) } // Read the actual container sections and validate that each section has the minimum size for (let i = 0; i < containerSectionSize; i++) { const containerSize = stream.readUint16(EOFError.ContainerSection) if (containerSize < CONTAINER_SIZE_MIN) { validationError(EOFError.ContainerSectionMin) } containerSizes.push(containerSize) } nextSection = stream.readUint() } // Verify that the next section is of the data type if (nextSection !== KIND_DATA) { validationError(EOFError.KIND_DATA) } this.dataSizePtr = stream.getPtr() const dataSize = stream.readUint16(EOFError.DataSize) // Verify that the header ends with the TERMINATOR byte stream.verifyUint(TERMINATOR, EOFError.TERMINATOR) // Write all values to the header object this.typeSize = typeSize this.codeSizes = codeSizes this.containerSizes = containerSizes this.dataSize = dataSize // Slice the input such that `this.buffer` is now the complete header // If there are dangling bytes in the stream, this is OK: this is the body section of the container this.buffer = input.slice(0, stream.getPtr()) const relativeOffset = this.buffer.length + this.typeSize // Write the start of the first code section into `codeStartPos` // Note: in EVM, if one would set the Program Counter to this byte, it would start executing the bytecode of the first code section this.codeStartPos = [relativeOffset] } sections() { return [this.typeSize, this.codeSizes, this.containerSizes, this.dataSize] } sectionSizes() { return [1, this.codeSizes.length, this.containerSizes.length, 1] } // Returns the code position in the container for the requested section // Setting the Program Counter in the EVM to a number of this array would start executing the bytecode of the indexed section getCodePosition(section: number) { if (this.codeStartPos[section]) { return this.codeStartPos[section] } const start = this.codeStartPos.length let offset = this.codeStartPos[start - 1] for (let i = start; i <= section; i++) { offset += this.codeSizes[i - 1] this.codeStartPos[i] = offset } return offset } } export interface TypeSection { inputs: number outputs: number maxStackHeight: number } /** * The EOF body holds the contents of the EOF container, such as the code sections (bytecode), * the subcontainers (EOF containers to be deployed via EOFCREATE) and the data section */ class EOFBody { typeSections: TypeSection[] // Array of type sections, used to index the inputs/outputs/max stack height of each section codeSections: Uint8Array[] // Bytecode of each code section containerSections: Uint8Array[] // Raw container bytes of each subcontainer entireCode: Uint8Array // The `entireCode` are all code sections concatenated dataSection: Uint8Array // Bytes of the data section buffer: Uint8Array // Raw bytes of the body txCallData?: Uint8Array // Only available in TxInitmode. The `txCallData` are the dangling bytes after parsing the container, // and these are used for the CALLDATA in the EVM when trying to create a contract via a transaction, and the deployment code is an EOF container constructor( buf: Uint8Array, // Buffer of the body. This should be the entire body. It is not valid to pass an entire EOF container in here header: EOFHeader, // EOFHeader corresponding to this body eofMode: EOFContainerMode = EOFContainerMode.Default, // Container mode of EOF dataSectionAllowedSmaller = false, // Only for validation: Deployment containers are allowed to have smaller data section size ) { const stream = new StreamReader(buf) const typeSections: TypeSection[] = [] // Read and parse each type section, and validate that the type section values are within valid bounds for (let i = 0; i < header.typeSize / 4; i++) { const inputs = stream.readUint(EOFError.Inputs) const outputs = stream.readUint(EOFError.Outputs) const maxStackHeight = stream.readUint16(EOFError.MaxStackHeight) if (i === 0) { if (inputs !== 0) { validationError(EOFError.Code0Inputs) } if (outputs !== 0x80) { validationError(EOFError.Code0Outputs) } } if (inputs > INPUTS_MAX) { validationError(EOFError.MaxInputs, i, inputs) } if (outputs > OUTPUTS_MAX) { validationError(EOFError.MaxOutputs, i, outputs) } if (maxStackHeight > MAX_STACK_HEIGHT) { validationError(EOFError.MaxStackHeightLimit, i, maxStackHeight) } typeSections.push({ inputs, outputs, maxStackHeight, }) } // Read each code section const codeStartPtr = stream.getPtr() const codes = [] for (const [i, codeSize] of header.codeSizes.entries()) { try { const code = stream.readBytes(codeSize) codes.push(code) } catch { validationError(EOFError.CodeSection, i) } } // Write the entire code section to the entireCodeSection const entireCodeSection = buf.slice(codeStartPtr, stream.getPtr()) // Read all raw subcontainers and push those to the containers array const containers = [] for (const [i, containerSize] of header.containerSizes.entries()) { try { const container = stream.readBytes(containerSize) containers.push(container) } catch { validationError(EOFError.ContainerSection, i) } } // Data section of the body // Note: for EOF containers in Initmode (these are Subcontainers) it is allowed // to have a data section of size lower than what is written in the header // For details, see "Data section lifecycle" of EIP 7620 let dataSection: Uint8Array // Edge case: deployment code validation if (eofMode !== EOFContainerMode.Initmode && !dataSectionAllowedSmaller) { dataSection = stream.readBytes(header.dataSize, EOFError.DataSection) if (eofMode === EOFContainerMode.Default) { if (!stream.isAtEnd()) { // If there are dangling bytes in default container mode, this is invalid validationError(EOFError.DanglingBytes) } } else { // Tx init mode: the remaining bytes (if any) are used as CALLDATA in the EVM, in case of a Tx init this.txCallData = stream.readRemainder() } } else { dataSection = stream.readRemainder() } // Write all data to the object this.typeSections = typeSections this.codeSections = codes this.containerSections = containers this.entireCode = entireCodeSection this.dataSection = dataSection this.buffer = buf } sections() { return [this.typeSections, this.codeSections, this.dataSection] } size() { return { typeSize: this.typeSections.length, codeSize: this.codeSections.length, dataSize: this.dataSection.length, } } sectionSizes() { return [ this.typeSections.map(() => 4), this.codeSections.map((b) => b.length), this.dataSection.length, ] } } /** * Main constructor for the EOFContainer */ export class EOFContainer { header: EOFHeader body: EOFBody buffer: Uint8Array eofMode: EOFContainerMode /** * * @param buf Entire container buffer * @param eofMode Container mode to validate the container on * @param dataSectionAllowedSmaller `true` if the data section is allowed to be smaller than the data section size in the header */ constructor( buf: Uint8Array, eofMode: EOFContainerMode = EOFContainerMode.Default, dataSectionAllowedSmaller = false, ) { this.eofMode = eofMode this.header = new EOFHeader(buf) this.body = new EOFBody( buf.slice(this.header.buffer.length), this.header, eofMode, dataSectionAllowedSmaller, ) this.buffer = buf } } /** * This method validates the EOF. It also performs deeper validation of the body, such as stack/opcode validation * This is ONLY necessary when trying to deploy contracts from a transaction: these can submit containers which are invalid * Since all deployed EOF containers are valid by definition, `validateEOF` does not need to be called each time an EOF contract is called * @param input Full container buffer * @param evm EVM, to read opcodes from * @param containerMode Container mode to validate on * @param eofMode EOF mode to run in * @returns */ export function validateEOF( input: Uint8Array, evm: EVM, containerMode: ContainerSectionType = ContainerSectionType.RuntimeCode, eofMode: EOFContainerMode = EOFContainerMode.Default, ) { const container = new EOFContainer( input, eofMode, containerMode === ContainerSectionType.DeploymentCode, ) const containerMap = verifyCode(container, evm, containerMode) // Recursively validate the containerSections for (let i = 0; i < container.body.containerSections.length; i++) { const subContainer = container.body.containerSections[i] const mode = containerMap.get(i)! validateEOF(subContainer, evm, mode) } return container }