@ethereumjs/evm
Version:
JavaScript Ethereum Virtual Machine (EVM) implementation
380 lines • 18.2 kB
JavaScript
"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