diff --git a/packages/common/src/eips/3670.json b/packages/common/src/eips/3670.json new file mode 100644 index 0000000000..9b3e814dae --- /dev/null +++ b/packages/common/src/eips/3670.json @@ -0,0 +1,15 @@ +{ + "name": "EIP-3670", + "number": 3670, + "comment": "EOF - Code Validation", + "url": "https://eips.ethereum.org/EIPS/eip-3670", + "status": "Review", + "minimumHardfork": "london", + "requiredEIPs": [ + 3540 + ], + "gasConfig": {}, + "gasPrices": {}, + "vm": {}, + "pow": {} +} \ No newline at end of file diff --git a/packages/common/src/eips/index.ts b/packages/common/src/eips/index.ts index 242a04bcda..813d811448 100644 --- a/packages/common/src/eips/index.ts +++ b/packages/common/src/eips/index.ts @@ -14,6 +14,7 @@ export const EIPs: eipsType = { 3541: require('./3541.json'), 3554: require('./3554.json'), 3607: require('./3607.json'), + 3670: require('./3670.json'), 3675: require('./3675.json'), 3855: require('./3855.json'), 4345: require('./4345.json'), diff --git a/packages/vm/src/evm/evm.ts b/packages/vm/src/evm/evm.ts index 1ded143ab8..ca2cb4e0f5 100644 --- a/packages/vm/src/evm/evm.ts +++ b/packages/vm/src/evm/evm.ts @@ -16,7 +16,7 @@ import TxContext from './txContext' import Message from './message' import EEI from './eei' // eslint-disable-next-line -import { eof1CodeAnalysis, short } from './opcodes/util' +import { eof1CodeAnalysis, eof1ValidOpcodes, short } from './opcodes/util' import { Log } from './types' import { default as Interpreter, InterpreterOpts, RunState } from './interpreter' @@ -372,6 +372,7 @@ export default class EVM { if (this._vm.DEBUG) { debug(`Start bytecode processing...`) } + let result = await this.runInterpreter(message) // fee for size of the return value @@ -407,12 +408,29 @@ export default class EVM { if (!this._vm._common.isActivatedEIP(3540)) { result = { ...result, ...INVALID_BYTECODE_RESULT(message.gasLimit) } } - // EIP-3540 EOF1 checks - if (!eof1CodeAnalysis(result.returnValue)) { + // EIP-3540 EOF1 header check + const eof1CodeAnalysisResults = eof1CodeAnalysis(result.returnValue) + if (!eof1CodeAnalysisResults?.code) { result = { ...result, ...INVALID_BYTECODE_RESULT(message.gasLimit), } + } else if (this._vm._common.isActivatedEIP(3670)) { + // EIP-3670 EOF1 code check + const codeStart = eof1CodeAnalysisResults.data > 0 ? 10 : 7 + // The start of the code section of an EOF1 compliant contract will either be + // index 7 (if no data section is present) or index 10 (if a data section is present) + // in the bytecode of the contract + if ( + !eof1ValidOpcodes( + result.returnValue.slice(codeStart, codeStart + eof1CodeAnalysisResults.code) + ) + ) { + result = { + ...result, + ...INVALID_BYTECODE_RESULT(message.gasLimit), + } + } } } else { result.gasUsed = totalGas diff --git a/packages/vm/src/evm/opcodes/functions.ts b/packages/vm/src/evm/opcodes/functions.ts index a154e811ba..f740ca933f 100644 --- a/packages/vm/src/evm/opcodes/functions.ts +++ b/packages/vm/src/evm/opcodes/functions.ts @@ -766,7 +766,7 @@ export const handlers: Map = new Map([ }, ], // 0x5b: JUMPDEST - [0x5b, function () { }], + [0x5b, function () {}], // 0x5c: BEGINSUB [ 0x5c, @@ -825,7 +825,7 @@ export const handlers: Map = new Map([ trap(ERROR.OUT_OF_RANGE) } const loaded = bufferToBigInt( - runState.eei.getCode().slice(runState.programCounter, runState.programCounter + numToPush) + runState.code.slice(runState.programCounter, runState.programCounter + numToPush) ) runState.programCounter += numToPush runState.stack.push(loaded) diff --git a/packages/vm/src/evm/opcodes/util.ts b/packages/vm/src/evm/opcodes/util.ts index d1295c2be9..b49819ad89 100644 --- a/packages/vm/src/evm/opcodes/util.ts +++ b/packages/vm/src/evm/opcodes/util.ts @@ -1,5 +1,6 @@ import Common from '@ethereumjs/common' import { keccak256, setLengthRight, setLengthLeft, bigIntToBuffer } from 'ethereumjs-util' +import { handlers } from '.' import { ERROR, VmError } from './../../exceptions' import { RunState } from './../interpreter' @@ -334,9 +335,38 @@ export const eof1CodeAnalysis = (container: Buffer) => { } if (container.length !== computedContainerSize) { // Scanned code does not match length of contract byte code - return } return sectionSizes } } + +export const eof1ValidOpcodes = (code: Buffer) => { + // EIP-3670 - validate all opcodes + const opcodes = new Set(handlers.keys()) + opcodes.add(0xfe) // Add INVALID opcode to set + + let x = 0 + while (x < code.length) { + const opcode = code[x] + x++ + if (!opcodes.has(opcode)) { + // No invalid/undefined opcodes + return false + } + if (opcode >= 0x60 && opcode <= 0x7f) { + // Skip data block following push + x += opcode - 0x5f + if (x > code.length - 1) { + // Push blocks mmust not exceed end of code section + return false + } + } + } + const terminatingOpcodes = new Set([0x00, 0xd3, 0xfd, 0xfe, 0xff]) + // Per EIP-3670, the final opcode of a code section must be STOP, RETURN, REVERT, INVALID, or SELFDESTRUCT + if (!terminatingOpcodes.has(code[code.length - 1])) { + return false + } + return true +} diff --git a/packages/vm/src/index.ts b/packages/vm/src/index.ts index d08bf491c7..8374eb31fc 100644 --- a/packages/vm/src/index.ts +++ b/packages/vm/src/index.ts @@ -196,7 +196,7 @@ export default class VM extends AsyncEventEmitter { if (opts.common) { // Supported EIPs const supportedEIPs = [ - 1559, 2315, 2537, 2565, 2718, 2929, 2930, 3198, 3529, 3540, 3541, 3607, 3855, + 1559, 2315, 2537, 2565, 2718, 2929, 2930, 3198, 3529, 3540, 3541, 3607, 3670, 3855, ] for (const eip of opts.common.eips()) { if (!supportedEIPs.includes(eip)) { diff --git a/packages/vm/tests/api/EIPs/eip-3540-evm--object-format.spec.ts b/packages/vm/tests/api/EIPs/eip-3540-evm-object-format.spec copy.ts similarity index 92% rename from packages/vm/tests/api/EIPs/eip-3540-evm--object-format.spec.ts rename to packages/vm/tests/api/EIPs/eip-3540-evm-object-format.spec copy.ts index cde4c131fb..93fd4270bd 100644 --- a/packages/vm/tests/api/EIPs/eip-3540-evm--object-format.spec.ts +++ b/packages/vm/tests/api/EIPs/eip-3540-evm-object-format.spec copy.ts @@ -3,15 +3,16 @@ import VM from '../../../src' import Common, { Chain, Hardfork } from '@ethereumjs/common' import { FeeMarketEIP1559Transaction } from '@ethereumjs/tx' import { Address, BN, privateToAddress } from 'ethereumjs-util' - const pkey = Buffer.from('20'.repeat(32), 'hex') const GWEI = new BN('1000000000') const sender = new Address(privateToAddress(pkey)) tape('EIP 3540 tests', (t) => { - const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.London, eips: [3540, 3541] }) - //const commonNoEIP3541 = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.London, eips: [] }) - + const common = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.London, + eips: [3540, 3541], + }) t.test('invalid object formats', async (st) => { const vm = new VM({ common }) const account = await vm.stateManager.getAccount(sender) @@ -93,7 +94,7 @@ tape('EIP 3540 tests', (t) => { await vm.stateManager.putAccount(sender, account) let tx = FeeMarketEIP1559Transaction.fromTxData({ - data: '0x67EF0001010001000060005260206007F3', + data: '0x67EF0001010001000060005260086018F3', gasLimit: 1000000, maxFeePerGas: 7, nonce: 0, @@ -103,8 +104,8 @@ tape('EIP 3540 tests', (t) => { let code = await vm.stateManager.getContractCode(created!) st.ok(code.length > 0, 'code section with no data section') tx = FeeMarketEIP1559Transaction.fromTxData({ - data: '0x6CEF00010100010000020001AA0060005260206007F3', - gasLimit: 1000000, + data: '0x6BEF00010100010200010000AA600052600C6014F3', + gasLimit: 100000000, maxFeePerGas: 7, nonce: 1, }).sign(pkey) diff --git a/packages/vm/tests/api/EIPs/eip-3670-eof-code-validation.spec.ts b/packages/vm/tests/api/EIPs/eip-3670-eof-code-validation.spec.ts new file mode 100644 index 0000000000..5df56a32f5 --- /dev/null +++ b/packages/vm/tests/api/EIPs/eip-3670-eof-code-validation.spec.ts @@ -0,0 +1,61 @@ +import tape from 'tape' +import VM from '../../../src' +import Common, { Chain, Hardfork } from '@ethereumjs/common' +import { FeeMarketEIP1559Transaction } from '@ethereumjs/tx' +import { Address, BN, privateToAddress } from 'ethereumjs-util' +import { eof1ValidOpcodes } from '../../../src/evm/opcodes' +const pkey = Buffer.from('20'.repeat(32), 'hex') +const GWEI = new BN('1000000000') +const sender = new Address(privateToAddress(pkey)) + +tape('EIP 3670 tests', (t) => { + const common = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.London, + eips: [3540, 3541, 3670], + }) + + t.test('eof1ValidOpcodes() tests', (st) => { + st.ok(eof1ValidOpcodes(Buffer.from([0])), 'valid -- STOP ') + st.notOk(eof1ValidOpcodes(Buffer.from([0xaa])), 'invalid -- AA -- undefined opcode') + st.ok(eof1ValidOpcodes(Buffer.from([0x60, 0xaa, 0])), 'valid - PUSH1 AA STOP') + st.notOk( + eof1ValidOpcodes(Buffer.from([0x7f, 0xaa, 0])), + 'invalid -- PUSH32 AA STOP -- truncated push' + ) + st.notOk( + eof1ValidOpcodes(Buffer.from([0x61, 0xaa, 0])), + 'invalid -- PUSH2 AA STOP -- truncated push' + ) + st.notOk(eof1ValidOpcodes(Buffer.from([0x30])), 'invalid -- ADDRESS -- invalid terminal opcode') + st.end() + }) + t.test('valid contract code transactions', async (st) => { + const vm = new VM({ common }) + const account = await vm.stateManager.getAccount(sender) + const balance = GWEI.muln(21000).muln(10000000) + account.balance = balance + await vm.stateManager.putAccount(sender, account) + + let tx = FeeMarketEIP1559Transaction.fromTxData({ + data: '0x67EF0001010001000060005260086018F3', + gasLimit: 1000000, + maxFeePerGas: 7, + nonce: 0, + }).sign(pkey) + let result = await vm.runTx({ tx }) + let created = result.createdAddress + let code = await vm.stateManager.getContractCode(created!) + st.ok(code.length > 0, 'code section with no data section') + tx = FeeMarketEIP1559Transaction.fromTxData({ + data: '0x6BEF00010100010200010000AA600052600C6014F3', + gasLimit: 100000000, + maxFeePerGas: 7, + nonce: 1, + }).sign(pkey) + result = await vm.runTx({ tx }) + created = result.createdAddress + code = await vm.stateManager.getContractCode(created!) + st.ok(code.length > 0, 'code section with data section') + }) +})