From 662af358c75369c0235ef95a51d2846465a33b80 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Fri, 13 Dec 2024 21:53:45 +0100 Subject: [PATCH 1/2] EVM: fix EOF fixtures (#3568) * evm: fix swapn stack validation * common/evm/vm: add osaka support * evm: guard eof gas methods * evm: fix callf/jumpf stack validation * evm: correctly guard eof methods * evm: implement missing RETURNDATALOAD * evm: fix delegatecall return buffer, eofcreate static mode * evm: fix 7702 extcodehash on empty * evm: remove old eof logic * evm/eof: fix dangling bytes header * evm/eof: throw on invalid returning sections * evm/eof: verify rjumps per code section, not entire body * eof: validation script update * evm: remove unused _common args --------- Co-authored-by: Holger Drewes --- packages/common/src/chains.ts | 4 + packages/common/src/enums.ts | 1 + packages/common/src/hardforks.ts | 5 +- packages/evm/src/eof/container.ts | 4 + packages/evm/src/eof/errors.ts | 1 + packages/evm/src/eof/verify.ts | 75 ++++++++++-------- packages/evm/src/evm.ts | 1 + packages/evm/src/opcodes/functions.ts | 77 ++++++++++++------- packages/evm/src/opcodes/gas.ts | 20 +++++ .../evm/test/eips/eof-header-validation.ts | 4 +- packages/vm/test/tester/config.ts | 1 + 11 files changed, 129 insertions(+), 64 deletions(-) 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 = { From 4da1f03e950c6388334973ea76466decdbb075bc Mon Sep 17 00:00:00 2001 From: Scorbajio Date: Mon, 16 Dec 2024 13:02:51 -0700 Subject: [PATCH 2/2] Implement debug_setHead (#3811) * Implement debug_setHead * Update packages/client/src/rpc/modules/debug.ts Co-authored-by: acolytec3 <17355484+acolytec3@users.noreply.github.com> * Complete the implementation of debug_setHead * Add helpers for creating blockchains and blocks for testing * Add test for debug_setHead * Set head of vmexecution as well * Do not prerun block before setting as vmexecution head * Move and export testSetup function and clean up tests * Remove unused util functions * Do not return nonstandard string from debug_setHead * Fix lint issues --------- Co-authored-by: acolytec3 <17355484+acolytec3@users.noreply.github.com> --- packages/client/src/rpc/modules/debug.ts | 29 +++++++++++ .../client/test/execution/vmexecution.spec.ts | 11 +---- .../client/test/rpc/debug/setHead.spec.ts | 48 +++++++++++++++++++ packages/client/test/rpc/helpers.ts | 9 ++++ 4 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 packages/client/test/rpc/debug/setHead.spec.ts diff --git a/packages/client/src/rpc/modules/debug.ts b/packages/client/src/rpc/modules/debug.ts index c99331957c..9562c5cf3f 100644 --- a/packages/client/src/rpc/modules/debug.ts +++ b/packages/client/src/rpc/modules/debug.ts @@ -148,6 +148,9 @@ export class Debug { 1, [[validators.hex]], ) + this.setHead = middleware(callWithStackTrace(this.setHead.bind(this), this._rpcDebug), 1, [ + [validators.blockOption], + ]) this.verbosity = middleware(callWithStackTrace(this.verbosity.bind(this), this._rpcDebug), 1, [ [validators.unsignedInteger], ]) @@ -457,4 +460,30 @@ export class Debug { this.client.config.logger.configure({ level: logLevels[level] }) return `level: ${this.client.config.logger.level}` } + + /** + * Sets the current head of the local chain by block number. Note, this is a + * destructive action and may severely damage your chain. Use with extreme + * caution. + * @param blockOpt Block number or tag to set as head of chain + */ + async setHead(params: [string]) { + const [blockOpt] = params + if (blockOpt === 'pending') { + throw { + code: INVALID_PARAMS, + message: `"pending" is not supported`, + } + } + + const block = await getBlockByOption(blockOpt, this.chain) + try { + await this.service.skeleton?.setHead(block, true) + await this.service.execution.setHead([block]) + } catch (e) { + throw { + code: INTERNAL_ERROR, + } + } + } } diff --git a/packages/client/test/execution/vmexecution.spec.ts b/packages/client/test/execution/vmexecution.spec.ts index 4b30946748..50c9db8fd6 100644 --- a/packages/client/test/execution/vmexecution.spec.ts +++ b/packages/client/test/execution/vmexecution.spec.ts @@ -8,7 +8,7 @@ import { assert, describe, it } from 'vitest' import { Chain } from '../../src/blockchain/index.js' import { Config } from '../../src/config.js' import { VMExecution } from '../../src/execution/index.js' -import { closeRPC, setupChain } from '../rpc/helpers.js' +import { closeRPC, setupChain, testSetup } from '../rpc/helpers.js' import { goerliData } from '../testdata/blocks/goerli.js' import { mainnetData } from '../testdata/blocks/mainnet.js' import { testnetData } from '../testdata/common/testnet.js' @@ -94,15 +94,6 @@ describe('[VMExecution]', () => { assert.equal(exec.vm, vm, 'should use vm provided') }) - async function testSetup(blockchain: Blockchain, common?: Common) { - const config = new Config({ common, accountCache: 10000, storageCache: 1000 }) - const chain = await Chain.create({ config, blockchain }) - const exec = new VMExecution({ config, chain }) - await chain.open() - await exec.open() - return exec - } - it('Block execution / Hardforks PoW (mainnet)', async () => { let blockchain = await createBlockchain({ validateBlocks: true, diff --git a/packages/client/test/rpc/debug/setHead.spec.ts b/packages/client/test/rpc/debug/setHead.spec.ts new file mode 100644 index 0000000000..78121b3d1a --- /dev/null +++ b/packages/client/test/rpc/debug/setHead.spec.ts @@ -0,0 +1,48 @@ +import { createBlockchainFromBlocksData } from '@ethereumjs/blockchain' +import { assert, describe, it } from 'vitest' + +import { mainnetData } from '../../testdata/blocks/mainnet.js' +import { createClient, createManager, getRPCClient, startRPC, testSetup } from '../helpers.js' + +import type { Blockchain } from '@ethereumjs/blockchain' + +const method = 'debug_setHead' + +describe(method, async () => { + it('call with valid arguments', async () => { + const blockchain = await createBlockchainFromBlocksData(mainnetData, { + validateBlocks: true, + validateConsensus: false, + }) + const blocks = await blockchain.getBlocks(0, 6, 0, false) + const exec = await testSetup(blockchain) + await exec.run() + const newHead = await (exec.vm.blockchain as Blockchain).getIteratorHead!() + assert.equal(newHead.header.number, BigInt(5), 'should run all blocks') + + const a = await createClient({ blockchain }) + await a.service.skeleton?.open() + ;(a.service.execution as any) = exec + + const manager = createManager(a) + const rpc = getRPCClient(startRPC(manager.getMethods())) + assert.equal( + await a.service.skeleton?.headHash(), + undefined, + 'should return undefined when head is not set', + ) + for (let i = 0; i < blocks.length; i++) { + await rpc.request(method, [`0x${i}`]) + assert.deepEqual( + await a.service.skeleton?.headHash()!, + blocks[i].header.hash(), + `skeleton chain should return hash of block number ${i} set as head`, + ) + assert.deepEqual( + a.service.execution.chainStatus?.hash!, + blocks[i].header.hash(), + `vm execution should set hash to new head`, + ) + } + }, 30000) +}) diff --git a/packages/client/test/rpc/helpers.ts b/packages/client/test/rpc/helpers.ts index 71d71d5251..5796bc4441 100644 --- a/packages/client/test/rpc/helpers.ts +++ b/packages/client/test/rpc/helpers.ts @@ -348,3 +348,12 @@ export const batchBlocks = async (rpc: HttpClient, inputBlocks: any[]) => { assert.equal(res.result.status, 'VALID') } } + +export async function testSetup(blockchain: Blockchain, common?: Common) { + const config = new Config({ common, accountCache: 10000, storageCache: 1000 }) + const chain = await Chain.create({ config, blockchain }) + const exec = new VMExecution({ config, chain }) + await chain.open() + await exec.open() + return exec +}