@tokamak-zk-evm/synthesizer
Version:
Tokamak zk-EVM Synthesizer - Processes Ethereum transactions into wire maps for Tokamak zk-SNARK proof generation
360 lines • 15.8 kB
JavaScript
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';
/*
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 var EOFContainerMode;
(function (EOFContainerMode) {
EOFContainerMode[EOFContainerMode["Default"] = 0] = "Default";
EOFContainerMode[EOFContainerMode["Initmode"] = 1] = "Initmode";
EOFContainerMode[EOFContainerMode["TxInitmode"] = 2] = "TxInitmode";
})(EOFContainerMode = EOFContainerMode || (EOFContainerMode = {}));
// 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) {
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) {
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, errorStr) {
if (this.readUint() !== expect) {
validationError(EOFError.VerifyUint, this.ptr - 1, errorStr);
}
}
/**
* Same as readUint, except this reads an uint16
* @param errorStr
* @returns
*/
readUint16(errorStr) {
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 {
/**
* 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 > 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 = [];
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) {
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;
}
}
/**
* 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 = 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(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;
// 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 {
/**
*
* @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 = 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, evm, containerMode = ContainerSectionType.RuntimeCode, eofMode = 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;
}
//# sourceMappingURL=container.js.map