diff --git a/packages/common/src/chains.ts b/packages/common/src/chains.ts index b463c81d1b..eb1cb3b21f 100644 --- a/packages/common/src/chains.ts +++ b/packages/common/src/chains.ts @@ -117,6 +117,10 @@ export const Mainnet: ChainConfig = { name: 'prague', block: null, }, + { + name: 'osaka', + block: null, + }, ], bootstrapNodes: [ { diff --git a/packages/common/src/enums.ts b/packages/common/src/enums.ts index 3ef732f922..8f4f673d57 100644 --- a/packages/common/src/enums.ts +++ b/packages/common/src/enums.ts @@ -71,6 +71,7 @@ export enum Hardfork { Shanghai = 'shanghai', Cancun = 'cancun', Prague = 'prague', + Osaka = 'osaka', Verkle = 'verkle', } diff --git a/packages/common/src/hardforks.ts b/packages/common/src/hardforks.ts index 1f3a8fbf8e..6f7c498396 100644 --- a/packages/common/src/hardforks.ts +++ b/packages/common/src/hardforks.ts @@ -158,10 +158,11 @@ export const hardforksDict: HardforksDict = { * Status : Final */ prague: { - // TODO update this accordingly to the right devnet setup - //eips: [663, 3540, 3670, 4200, 4750, 5450, 6206, 7069, 7480, 7620, 7692, 7698], // This is EOF-only eips: [2537, 2935, 6110, 7002, 7251, 7685, 7702], // This is current prague without EOF }, + osaka: { + eips: [663, 3540, 3670, 4200, 4750, 5450, 6206, 7069, 7480, 7620, 7692, 7698], // These are the EOF EIPs + }, /** * Description: Next feature hardfork after prague, internally used for verkle testing/implementation (incomplete/experimental) * URL : https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/osaka.md diff --git a/packages/evm/src/eof/container.ts b/packages/evm/src/eof/container.ts index fe4a08e83d..3f8a1cddeb 100644 --- a/packages/evm/src/eof/container.ts +++ b/packages/evm/src/eof/container.ts @@ -368,6 +368,10 @@ class EOFBody { } } else { dataSection = stream.readRemainder() + + if (dataSection.length > header.dataSize) { + validationError(EOFError.DanglingBytes) + } } // Write all data to the object diff --git a/packages/evm/src/eof/errors.ts b/packages/evm/src/eof/errors.ts index e3182f1c97..7b7cf9798a 100644 --- a/packages/evm/src/eof/errors.ts +++ b/packages/evm/src/eof/errors.ts @@ -63,6 +63,7 @@ export enum EOFError { InvalidStackHeight = 'invalid stack height', InvalidJUMPF = 'invalid jumpf target (output count)', InvalidReturningSection = 'invalid returning code section: section is not returning', + ReturningNoReturn = 'invalid section: section should return but has no RETF/JUMP to return', RJUMPVTableSize0 = 'invalid RJUMPV: table size 0', UnreachableCodeSections = 'unreachable code sections', UnreachableCode = 'unreachable code (by forward jumps)', diff --git a/packages/evm/src/eof/verify.ts b/packages/evm/src/eof/verify.ts index ac0e632050..e6c4ecaa15 100644 --- a/packages/evm/src/eof/verify.ts +++ b/packages/evm/src/eof/verify.ts @@ -61,31 +61,9 @@ function validateOpcodes( evm: EVM, mode: ContainerSectionType = 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: number) { - if (intermediateBytes.has(location)) { - // When trying to JUMP into an intermediate byte: this is invalid - validationError(EOFError.InvalidRJUMP) - } - jumpLocations.add(location) - } - - function addIntermediate(location: number) { - 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) @@ -156,11 +134,42 @@ function validateOpcodes( let codeSection = -1 for (const code of container.body.codeSections) { + // 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() + + // eslint-disable-next-line no-inner-declarations + function addJump(location: number) { + if (intermediateBytes.has(location)) { + // When trying to JUMP into an intermediate byte: this is invalid + validationError(EOFError.InvalidRJUMP) + } + jumpLocations.add(location) + } + + // eslint-disable-next-line no-inner-declarations + function addIntermediate(location: number) { + 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) + } + codeSection++ reachableSections[codeSection] = new Set() - const returningFunction = container.body.typeSections[codeSection].outputs === 0x80 + // Section is marked as "non-returning": it does never "return" to another code section + // it rather exits the current EVM call frame + const nonReturningFunction = container.body.typeSections[codeSection].outputs === 0x80 + + // Boolean flag to mark if this section has a returning opcode: + // RETF + // Or JUMPF into a returning section + // Each returning section should contain a returning opcode + let sectionHasReturningOpcode = false // Tracking set of reachable opcodes const reachableOpcodes = new Set() @@ -212,12 +221,12 @@ function validateOpcodes( let minStackNext = minStackCurrent + delta let maxStackNext = maxStackCurrent + delta - if (maxStackNext > 1023) { + if (maxStackNext > 1024) { // TODO verify if 1023 or 1024 is the right constant validationError(EOFError.StackOverflow) } - if (returningFunction && opcode === 0xe4) { + if (nonReturningFunction && opcode === 0xe4) { validationError(EOFError.InvalidReturningSection) } @@ -328,7 +337,7 @@ function validateOpcodes( validationError(EOFError.InvalidJUMPF) } - if (returningFunction && targetOutputs <= 0x7f) { + if (nonReturningFunction && targetOutputs <= 0x7f) { // Current function is returning, but target is not, cannot jump into this validationError(EOFError.InvalidReturningSection) } @@ -344,12 +353,12 @@ function validateOpcodes( if (!(minStackCurrent === maxStackCurrent && maxStackCurrent === expectedStack)) { validationError(EOFError.InvalidStackHeight) } + sectionHasReturningOpcode = true } if ( maxStackCurrent + container.body.typeSections[target].maxStackHeight - targetInputs > 1024 ) { - //console.log(maxStackCurrent, targetOutputs, targetInputs, targetNonReturning) validationError(EOFError.StackOverflow) } } @@ -360,6 +369,7 @@ function validateOpcodes( if (!(minStackCurrent === maxStackCurrent && maxStackCurrent === outputs)) { validationError(EOFError.InvalidStackHeight) } + sectionHasReturningOpcode = true } else if (opcode === 0xe6) { // DUPN const toDup = code[ptr + 1] @@ -369,9 +379,7 @@ function validateOpcodes( } 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) { + if (toSwap + 2 > minStackCurrent) { validationError(EOFError.StackUnderflow) } } else if (opcode === 0xe8) { @@ -485,10 +493,15 @@ function validateOpcodes( if (container.body.typeSections[codeSection].maxStackHeight !== maxStackHeight) { validationError(EOFError.MaxStackHeightViolation) } - if (maxStackHeight > 1023) { + if (maxStackHeight > 1024) { // TODO verify if 1023 or 1024 is the right constant validationError(EOFError.MaxStackHeightLimit) } + + // Validate that if the section is returning, there is a returning opcode + if (!sectionHasReturningOpcode && !nonReturningFunction) { + validationError(EOFError.ReturningNoReturn) + } } // Verify that each code section can be reached from code section 0 diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index 5e86d49d60..c37000d4a5 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -86,6 +86,7 @@ export class EVM implements EVMInterface { Hardfork.Shanghai, Hardfork.Cancun, Hardfork.Prague, + Hardfork.Osaka, Hardfork.Verkle, ] protected _tx?: { diff --git a/packages/evm/src/opcodes/functions.ts b/packages/evm/src/opcodes/functions.ts index 6b7ef88852..1a55a62f92 100644 --- a/packages/evm/src/opcodes/functions.ts +++ b/packages/evm/src/opcodes/functions.ts @@ -595,9 +595,6 @@ export const handlers: Map = new Map([ runState.stack.push(BigInt(bytesToHex(account.codeHash))) return - } else { - runState.stack.push(bytesToBigInt(keccak256(code))) - return } } @@ -957,12 +954,6 @@ export const handlers: Map = new Map([ 0x60, function (runState, common) { const numToPush = runState.opCode - 0x5f - if ( - runState.programCounter + numToPush > runState.code.length && - common.isActivatedEIP(3540) - ) { - trap(ERROR.OUT_OF_RANGE) - } if (common.isActivatedEIP(6800) && runState.env.chargeCodeAccesses === true) { const contract = runState.interpreter.getAddress() @@ -1028,7 +1019,7 @@ export const handlers: Map = new Map([ // 0xd0: DATALOAD [ 0xd0, - function (runState, _common) { + function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) @@ -1053,7 +1044,7 @@ export const handlers: Map = new Map([ // 0xd1: DATALOADN [ 0xd1, - function (runState, _common) { + function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) @@ -1071,7 +1062,7 @@ export const handlers: Map = new Map([ // 0xd2: DATASIZE [ 0xd2, - function (runState, _common) { + function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) @@ -1082,7 +1073,7 @@ export const handlers: Map = new Map([ // 0xd3: DATACOPY [ 0xd3, - function (runState, _common) { + function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) @@ -1099,7 +1090,7 @@ export const handlers: Map = new Map([ // 0xe0: RJUMP [ 0xe0, - function (runState, _common) { + function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) @@ -1113,7 +1104,7 @@ export const handlers: Map = new Map([ // 0xe1: RJUMPI [ 0xe1, - function (runState, _common) { + function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) @@ -1133,7 +1124,7 @@ export const handlers: Map = new Map([ // 0xe2: RJUMPV [ 0xe2, - function (runState, _common) { + function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) @@ -1160,7 +1151,7 @@ export const handlers: Map = new Map([ // 0xe3: CALLF [ 0xe3, - function (runState, _common) { + function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) @@ -1170,7 +1161,7 @@ export const handlers: Map = new Map([ ) const stackItems = runState.stack.length const typeSection = runState.env.eof!.container.body.typeSections[sectionTarget] - if (1024 < stackItems + typeSection?.inputs - typeSection?.maxStackHeight) { + if (stackItems > 1024 - typeSection.maxStackHeight + typeSection.inputs) { trap(EOFError.StackOverflow) } if (runState.env.eof!.eofRunState.returnStack.length >= 1024) { @@ -1185,7 +1176,7 @@ export const handlers: Map = new Map([ // 0xe4: RETF [ 0xe4, - function (runState, _common) { + function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) @@ -1201,7 +1192,7 @@ export const handlers: Map = new Map([ // 0xe5: JUMPF [ 0xe5, - function (runState, _common) { + function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) @@ -1214,7 +1205,7 @@ export const handlers: Map = new Map([ ) const stackItems = runState.stack.length const typeSection = runState.env.eof!.container.body.typeSections[sectionTarget] - if (1024 < stackItems + typeSection?.inputs - typeSection?.maxStackHeight) { + if (stackItems > 1024 - typeSection.maxStackHeight + typeSection.inputs) { trap(EOFError.StackOverflow) } /*if (runState.env.eof!.eofRunState.returnStack.length >= 1024) { @@ -1229,7 +1220,7 @@ export const handlers: Map = new Map([ // 0xe6: DUPN [ 0xe6, - function (runState, _common) { + function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) @@ -1247,7 +1238,7 @@ export const handlers: Map = new Map([ // 0xe7: SWAPN [ 0xe7, - function (runState, _common) { + function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) @@ -1265,7 +1256,7 @@ export const handlers: Map = new Map([ // 0xe8: EXCHANGE [ 0xe8, - function (runState, _common) { + function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) @@ -1282,11 +1273,14 @@ export const handlers: Map = new Map([ // 0xec: EOFCREATE [ 0xec, - async function (runState, _common) { + async function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) } else { + if (runState.interpreter.isStatic()) { + trap(ERROR.STATIC_STATE_CHANGE) + } // Read container index const containerIndex = runState.env.code[runState.programCounter] const containerCode = runState.env.eof!.container.body.containerSections[containerIndex] @@ -1318,7 +1312,7 @@ export const handlers: Map = new Map([ // 0xee: RETURNCONTRACT [ 0xee, - async function (runState, _common) { + async function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) @@ -1518,10 +1512,34 @@ export const handlers: Map = new Map([ runState.stack.push(ret) }, ], + // 0xf7: RETURNDATALOAD + [ + 0xf7, + function (runState) { + if (runState.env.eof === undefined) { + // Opcode not available in legacy contracts + trap(ERROR.INVALID_OPCODE) + } + const pos = runState.stack.pop() + if (pos > runState.interpreter.getReturnDataSize()) { + runState.stack.push(BIGINT_0) + return + } + + const i = Number(pos) + let loaded = runState.interpreter.getReturnData().subarray(i, i + 32) + loaded = loaded.length ? loaded : Uint8Array.from([0]) + let r = bytesToBigInt(loaded) + if (loaded.length < 32) { + r = r << (BIGINT_8 * BigInt(32 - loaded.length)) + } + runState.stack.push(r) + }, + ], // 0xf8: EXTCALL [ 0xf8, - async function (runState, _common) { + async function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) @@ -1555,7 +1573,7 @@ export const handlers: Map = new Map([ // 0xf9: EXTDELEGATECALL [ 0xf9, - async function (runState, _common) { + async function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) @@ -1580,6 +1598,7 @@ export const handlers: Map = new Map([ if (!isEOF(code)) { // EXTDELEGATECALL cannot call legacy contracts runState.stack.push(BIGINT_1) + runState.returnBytes = new Uint8Array(0) return } @@ -1619,7 +1638,7 @@ export const handlers: Map = new Map([ // 0xfb: EXTSTATICCALL [ 0xfb, - async function (runState, _common) { + async function (runState) { if (runState.env.eof === undefined) { // Opcode not available in legacy contracts trap(ERROR.INVALID_OPCODE) diff --git a/packages/evm/src/opcodes/gas.ts b/packages/evm/src/opcodes/gas.ts index a85868c214..8c4dec0275 100644 --- a/packages/evm/src/opcodes/gas.ts +++ b/packages/evm/src/opcodes/gas.ts @@ -468,6 +468,10 @@ export const dynamicGasHandlers: Map { + if (runState.env.eof === undefined) { + // Opcode not available in legacy contracts + trap(ERROR.INVALID_OPCODE) + } // Note: TX_CREATE_COST is in the base fee (this is 32000 and same as CREATE / CREATE2) // Note: in `gas.ts` programCounter is not yet incremented (which it is in `functions.ts`) @@ -802,6 +810,10 @@ export const dynamicGasHandlers: Map { + if (runState.env.eof === undefined) { + // Opcode not available in legacy contracts + trap(ERROR.INVALID_OPCODE) + } // Charge WARM_STORAGE_READ_COST (100) -> done in accessAddressEIP2929 // Peek stack values @@ -875,6 +887,10 @@ export const dynamicGasHandlers: Map { + if (runState.env.eof === undefined) { + // Opcode not available in legacy contracts + trap(ERROR.INVALID_OPCODE) + } // Charge WARM_STORAGE_READ_COST (100) -> done in accessAddressEIP2929 // Peek stack values @@ -973,6 +989,10 @@ export const dynamicGasHandlers: Map { + if (runState.env.eof === undefined) { + // Opcode not available in legacy contracts + trap(ERROR.INVALID_OPCODE) + } // Charge WARM_STORAGE_READ_COST (100) -> done in accessAddressEIP2929 // Peek stack values diff --git a/packages/evm/test/eips/eof-header-validation.ts b/packages/evm/test/eips/eof-header-validation.ts index 5b51da3ead..3dcd19ca27 100644 --- a/packages/evm/test/eips/eof-header-validation.ts +++ b/packages/evm/test/eips/eof-header-validation.ts @@ -52,8 +52,8 @@ await new Promise((resolve, reject) => { const code = hexToBytes(test.code) - const expected = test.results.Prague.result - const _exception = test.results.Prague.exception + const expected = test.results.Osaka.result + const _exception = test.results.Osaka.exception let containerSectionType = ContainerSectionType.RuntimeCode let eofContainerMode = EOFContainerMode.Default diff --git a/packages/vm/test/tester/config.ts b/packages/vm/test/tester/config.ts index 608dcb2752..71aa1d4f9b 100644 --- a/packages/vm/test/tester/config.ts +++ b/packages/vm/test/tester/config.ts @@ -108,6 +108,7 @@ const normalHardforks = [ 'arrowGlacier', // This network has no tests, but need to add it due to common generation logic 'cancun', 'prague', + 'osaka', ] const transitionNetworks = {