diff --git a/packages/common/src/eips/5656.json b/packages/common/src/eips/5656.json new file mode 100644 index 0000000000..4f7d36309e --- /dev/null +++ b/packages/common/src/eips/5656.json @@ -0,0 +1,17 @@ +{ + "name": "EIP-5656", + "number": 5656, + "comment": "MCOPY - Memory copying instruction", + "url": "https://eips.ethereum.org/EIPS/eip-5656", + "status": "Draft", + "minimumHardfork": "shanghai", + "gasConfig": {}, + "gasPrices": { + "mcopy": { + "v": 3, + "d": "Base fee of the MCOPY opcode" + } + }, + "vm": {}, + "pow": {} +} diff --git a/packages/common/src/eips/index.ts b/packages/common/src/eips/index.ts index e7ac399cb4..e7275e5dd6 100644 --- a/packages/common/src/eips/index.ts +++ b/packages/common/src/eips/index.ts @@ -23,6 +23,7 @@ import * as eip4399 from './4399.json' import * as eip4844 from './4844.json' import * as eip4895 from './4895.json' import * as eip5133 from './5133.json' +import * as eip5656 from './5656.json' import * as eip6780 from './6780.json' export const EIPs: { [key: number]: any } = { @@ -51,5 +52,6 @@ export const EIPs: { [key: number]: any } = { 4844: eip4844, 4895: eip4895, 5133: eip5133, + 5656: eip5656, 6780: eip6780, } diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index 872fc3b3ad..88da98368e 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -87,7 +87,7 @@ export interface EVMOpts { * - [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) - Shard Blob Transactions (`experimental`) * - [EIP-4895](https://eips.ethereum.org/EIPS/eip-4895) - Beacon chain push withdrawals as operations * - [EIP-5133](https://eips.ethereum.org/EIPS/eip-5133) - Delaying Difficulty Bomb to mid-September 2022 - * + * - [EIP-5656](https://eips.ethereum.org/EIPS/eip-5656) - MCOPY - Memory copying instruction (`experimental`) * *Annotations:* * * - `experimental`: behaviour can change on patch versions @@ -286,7 +286,7 @@ export class EVM implements EVMInterface { // Supported EIPs const supportedEIPs = [ 1153, 1559, 2315, 2537, 2565, 2718, 2929, 2930, 3074, 3198, 3529, 3540, 3541, 3607, 3651, - 3670, 3855, 3860, 4399, 4895, 4844, 5133, 6780, + 3670, 3855, 3860, 4399, 4895, 4844, 5133, 5656, 6780, ] for (const eip of this._common.eips()) { diff --git a/packages/evm/src/opcodes/codes.ts b/packages/evm/src/opcodes/codes.ts index 08e7d6e724..8b69d2346a 100644 --- a/packages/evm/src/opcodes/codes.ts +++ b/packages/evm/src/opcodes/codes.ts @@ -303,6 +303,12 @@ const eipOpcodes: { eip: number; opcodes: OpcodeEntry }[] = [ 0x49: { name: 'BLOBHASH', isAsync: false, dynamicGas: false }, }, }, + { + eip: 5656, + opcodes: { + 0x5e: { name: 'MCOPY', isAsync: false, dynamicGas: true }, + }, + }, ] /** diff --git a/packages/evm/src/opcodes/functions.ts b/packages/evm/src/opcodes/functions.ts index 78ba10ae51..6459aa5847 100644 --- a/packages/evm/src/opcodes/functions.ts +++ b/packages/evm/src/opcodes/functions.ts @@ -796,24 +796,32 @@ export const handlers: Map = new Map([ runState.programCounter = Number(dest) }, ], - // 0x5e: JUMPSUB + // 0x5e: JUMPSUB (2315) / MCOPY (5656) [ 0x5e, - function (runState) { - const dest = runState.stack.pop() + function (runState, common) { + if (common.isActivatedEIP(2315)) { + // JUMPSUB + const dest = runState.stack.pop() - if (dest > runState.interpreter.getCodeSize()) { - trap(ERROR.INVALID_JUMPSUB + ' at ' + describeLocation(runState)) - } + if (dest > runState.interpreter.getCodeSize()) { + trap(ERROR.INVALID_JUMPSUB + ' at ' + describeLocation(runState)) + } - const destNum = Number(dest) + const destNum = Number(dest) - if (!jumpSubIsValid(runState, destNum)) { - trap(ERROR.INVALID_JUMPSUB + ' at ' + describeLocation(runState)) - } + if (!jumpSubIsValid(runState, destNum)) { + trap(ERROR.INVALID_JUMPSUB + ' at ' + describeLocation(runState)) + } - runState.returnStack.push(BigInt(runState.programCounter)) - runState.programCounter = destNum + 1 + runState.returnStack.push(BigInt(runState.programCounter)) + runState.programCounter = destNum + 1 + } else if (common.isActivatedEIP(5656)) { + // MCOPY + const [dst, src, length] = runState.stack.popN(3) + const data = runState.memory.read(Number(src), Number(length), true) + runState.memory.write(Number(dst), Number(length), data) + } }, ], // 0x5f: PUSH0 diff --git a/packages/evm/src/opcodes/gas.ts b/packages/evm/src/opcodes/gas.ts index da1baeb498..7c6c81bc8d 100644 --- a/packages/evm/src/opcodes/gas.ts +++ b/packages/evm/src/opcodes/gas.ts @@ -261,6 +261,18 @@ export const dynamicGasHandlers: Map { + const [dst, src, length] = runState.stack.peek(3) + const wordsCopied = (length + BigInt(31)) / BigInt(32) + gas += BigInt(3) * wordsCopied + gas += subMemUsage(runState, src, length, common) + gas += subMemUsage(runState, dst, length, common) + return gas + }, + ], [ /* LOG */ 0xa0, diff --git a/packages/evm/test/eips/eip-5656.spec.ts b/packages/evm/test/eips/eip-5656.spec.ts new file mode 100644 index 0000000000..adc2712277 --- /dev/null +++ b/packages/evm/test/eips/eip-5656.spec.ts @@ -0,0 +1,108 @@ +import { Chain, Common, Hardfork } from '@ethereumjs/common' +import { DefaultStateManager } from '@ethereumjs/statemanager' +import { bytesToHex, hexStringToBytes } from '@ethereumjs/util' +import { assert, describe, it } from 'vitest' + +import { EVM } from '../../src' + +type Situation = { + pre: string + post: string + dst: number + src: number + length: number +} + +// Taken from EIP + +const situations: Situation[] = [ + { + pre: '0000000000000000000000000000000000000000000000000000000000000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f', + post: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f', + dst: 0, + src: 32, + length: 32, + }, + { + pre: '0101010101010101010101010101010101010101010101010101010101010101', + post: '0101010101010101010101010101010101010101010101010101010101010101', + dst: 0, + src: 0, + length: 32, + }, + // For the situation below, pre/post have 1 byte less than in the current state of the EIP + { + pre: '0001020304050607080000000000000000000000000000000000000000000000', + post: '0102030405060708080000000000000000000000000000000000000000000000', + dst: 0, + src: 1, + length: 8, + }, + // For the situation below, pre/post have 1 byte less than in the current state of the EIP + { + pre: '0001020304050607080000000000000000000000000000000000000000000000', + post: '0000010203040506070000000000000000000000000000000000000000000000', + dst: 1, + src: 0, + length: 8, + }, +] + +function numToOpcode(num: number) { + return num.toString(16).padStart(2, '0') +} + +const PUSH1 = '60' +const MSTORE8 = '53' +const MCOPY = '5E' +const STOP = '00' + +describe('should test mcopy', () => { + for (const situation of situations) { + it('should produce correct output', async () => { + // create bytecode + let bytecode = '' + // prepare the memory + for (let i = 0; i < situation.pre.length / 2; i++) { + const start = i * 2 + const hexNum = situation.pre.slice(start, start + 2) + bytecode += PUSH1 + hexNum + PUSH1 + numToOpcode(i) + MSTORE8 + } + // mcopy + bytecode += + PUSH1 + + numToOpcode(situation.length) + + PUSH1 + + numToOpcode(situation.src) + + PUSH1 + + numToOpcode(situation.dst) + bytecode += MCOPY + STOP + + const common = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.Shanghai, + eips: [5656], + }) + + const evm = await EVM.create({ + common, + stateManager: new DefaultStateManager(), + }) + + let currentMem = '' + + evm.events.on('step', (e) => { + if (e.opcode.name === 'STOP') { + currentMem = bytesToHex(e.memory) + } + }) + + await evm.runCall({ + data: hexStringToBytes(bytecode), + gasLimit: BigInt(0xffffff), + }) + + assert.equal(currentMem, situation.post, 'post-memory correct') + }) + } +})