UNPKG

@ethereumjs/evm

Version:
380 lines 18.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EOFContainer = exports.EOFContainerMode = void 0; exports.validateEOF = validateEOF; const util_1 = require("@ethereumjs/util"); const constants_ts_1 = require("./constants.js"); const errors_ts_1 = require("./errors.js"); const verify_ts_1 = require("./verify.js"); exports.EOFContainerMode = { Default: 'default', // Default container validation Initmode: 'initMode', // Initmode container validation (for subcontainers pointed to by EOFCreate) TxInitmode: 'txInitMode', // Tx initmode container validation (for txs deploying EOF contracts) }; // The StreamReader is a helper class to help reading byte arrays class StreamReader { constructor(stream) { 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, errorStr) { const end = this.ptr + amount; if (end > this.data.length) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.OUT_OF_BOUNDS, 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) { if (this.ptr >= this.data.length) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.OUT_OF_BOUNDS, 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, errorStr) { if (this.readUint() !== expect) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.VERIFY_UINT, this.ptr - 1, errorStr); } } /** * Same as readUint, except this reads an uint16 * @param errorStr - Optional error message to include in thrown validation errors * @returns Unsigned 16-bit integer read from the stream */ readUint16(errorStr) { const end = this.ptr + 2; if (end > this.data.length) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.OUT_OF_BOUNDS, 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 { /** * Create an EOF header. Performs various validation checks inside the constructor * @param input either a raw header or a complete container */ constructor(input) { if (input.length > constants_ts_1.MAX_HEADER_SIZE) { throw (0, util_1.EthereumJSErrorWithoutCode)('err: container size more than maximum valid size'); } const stream = new StreamReader(input); // Verify that the header starts with 0xEF0001 stream.verifyUint(constants_ts_1.FORMAT, errors_ts_1.EOFErrorMessage.FORMAT); stream.verifyUint(constants_ts_1.MAGIC, errors_ts_1.EOFErrorMessage.MAGIC); stream.verifyUint(constants_ts_1.VERSION, errors_ts_1.EOFErrorMessage.VERSION); if (input.length < 15) { throw (0, util_1.EthereumJSErrorWithoutCode)('err: container size less than minimum valid size'); } // Verify that the types section is present and its length is valid stream.verifyUint(constants_ts_1.KIND_TYPE, errors_ts_1.EOFErrorMessage.KIND_TYPE); const typeSize = stream.readUint16(errors_ts_1.EOFErrorMessage.TYPE_SIZE); if (typeSize < constants_ts_1.TYPE_MIN) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.INVALID_TYPE_SIZE, typeSize); } if (typeSize % constants_ts_1.TYPE_DIVISOR !== 0) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.INVALID_TYPE_SIZE, typeSize); } if (typeSize > constants_ts_1.TYPE_MAX) { throw (0, util_1.EthereumJSErrorWithoutCode)(`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(constants_ts_1.KIND_CODE, errors_ts_1.EOFErrorMessage.KIND_CODE); const codeSize = stream.readUint16(errors_ts_1.EOFErrorMessage.CODE_SIZE); if (codeSize < constants_ts_1.CODE_MIN) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.MIN_CODE_SECTIONS); } if (codeSize !== typeSize / constants_ts_1.TYPE_DIVISOR) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.TYPE_SECTIONS, typeSize / constants_ts_1.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(errors_ts_1.EOFErrorMessage.CODE_SECTION); if (codeSectionSize < constants_ts_1.CODE_SIZE_MIN) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.CODE_SECTION_SIZE); } codeSizes.push(codeSectionSize); } // Check if there are container sections let nextSection = stream.readUint(); const containerSizes = []; if (nextSection === constants_ts_1.KIND_CONTAINER) { // The optional container section is present, validate that the size is within bounds const containerSectionSize = stream.readUint16(errors_ts_1.EOFErrorMessage.CONTAINER_SIZE); if (containerSectionSize < constants_ts_1.CONTAINER_MIN) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.CONTAINER_SECTION_SIZE); } if (containerSectionSize > constants_ts_1.CONTAINER_MAX) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.CONTAINER_SECTION_SIZE); } // 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(errors_ts_1.EOFErrorMessage.CONTAINER_SECTION); if (containerSize < constants_ts_1.CONTAINER_SIZE_MIN) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.CONTAINER_SECTION_MIN); } containerSizes.push(containerSize); } nextSection = stream.readUint(); } // Verify that the next section is of the data type if (nextSection !== constants_ts_1.KIND_DATA) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.KIND_DATA); } this.dataSizePtr = stream.getPtr(); const dataSize = stream.readUint16(errors_ts_1.EOFErrorMessage.DATA_SIZE); // Verify that the header ends with the TERMINATOR byte stream.verifyUint(constants_ts_1.TERMINATOR, errors_ts_1.EOFErrorMessage.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) { 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; } // Returns the code section for a given program counter position getSectionFromProgramCounter(programCounter) { const lastIndex = this.codeSizes.length - 1; const lastStart = this.getCodePosition(lastIndex); const lastEnd = lastStart + this.codeSizes[lastIndex]; if (programCounter < 0 || programCounter > lastEnd) { // If code position is outside the beginning or end of the code sections, return 0 throw (0, util_1.EthereumJSErrorWithoutCode)('program counter out of bounds'); } if (this.codeStartPos.length < this.codeSizes.length) { this.getCodePosition(this.codeSizes.length - 1); // initialize code positions if uninitialized } for (let i = 0; i < this.codeSizes.length; i++) { if (programCounter < this.codeStartPos[i] + this.codeSizes[i]) { // We've found our section if the code position is less than the end of the current code section return i; } } // This shouldn't happen so just error throw (0, util_1.EthereumJSErrorWithoutCode)(`Invalid program counter value: ${programCounter}`); } } /** * 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 { // 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, // Buffer of the body. This should be the entire body. It is not valid to pass an entire EOF container in here header, // EOFHeader corresponding to this body eofMode = exports.EOFContainerMode.Default, // Container mode of EOF dataSectionAllowedSmaller = false) { const stream = new StreamReader(buf); const typeSections = []; // 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(errors_ts_1.EOFErrorMessage.INPUTS); const outputs = stream.readUint(errors_ts_1.EOFErrorMessage.OUTPUTS); const maxStackHeight = stream.readUint16(errors_ts_1.EOFErrorMessage.MAX_STACK_HEIGHT); if (i === 0) { if (inputs !== 0) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.CODE0_INPUTS); } if (outputs !== 0x80) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.CODE0_OUTPUTS); } } if (inputs > constants_ts_1.INPUTS_MAX) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.MAX_INPUTS, i, inputs); } if (outputs > constants_ts_1.OUTPUTS_MAX) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.MAX_OUTPUTS, i, outputs); } if (maxStackHeight > constants_ts_1.MAX_STACK_HEIGHT) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.MAX_STACK_HEIGHT_LIMIT, 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 { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.CODE_SECTION, 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 { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.CONTAINER_SECTION, 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; // Edge case: deployment code validation if (eofMode !== exports.EOFContainerMode.Initmode && !dataSectionAllowedSmaller) { dataSection = stream.readBytes(header.dataSize, errors_ts_1.EOFErrorMessage.DATA_SECTION); if (eofMode === exports.EOFContainerMode.Default) { if (!stream.isAtEnd()) { // If there are dangling bytes in default container mode, this is invalid (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.DANGLING_BYTES); } } 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(); if (dataSection.length > header.dataSize) { (0, errors_ts_1.validationError)(errors_ts_1.EOFErrorMessage.DANGLING_BYTES); } } // 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 */ class EOFContainer { /** * * @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, eofMode = exports.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; } } exports.EOFContainer = EOFContainer; /** * 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 The decoded EOF container */ function validateEOF(input, evm, containerMode = verify_ts_1.ContainerSectionType.RuntimeCode, eofMode = exports.EOFContainerMode.Default) { const container = new EOFContainer(input, eofMode, containerMode === verify_ts_1.ContainerSectionType.DeploymentCode); const containerMap = (0, verify_ts_1.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; } //# sourceMappingURL=container.js.map