From 8f5f3bbdc8b6674e96892789264ccfbbdeaa7092 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Wed, 23 Feb 2022 16:27:08 -0500 Subject: [PATCH 1/5] EIP3670 initial changes --- packages/vm/src/evm/evm.ts | 22 ++++++++++++-- packages/vm/src/evm/opcodes/functions.ts | 1 + packages/vm/src/evm/opcodes/util.ts | 29 ++++++++++++++++++- .../EIPs/eip-3540-evm--object-format.spec.ts | 5 +--- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/packages/vm/src/evm/evm.ts b/packages/vm/src/evm/evm.ts index 1ded143ab8..d966987158 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,27 @@ 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 { + // EIP-3670 EOF1 code check + const codeStart = eof1CodeAnalysisResults?.data > 0 ? 10 : 7 + if ( + !eof1ValidOpcodes( + result.returnValue.slice(codeStart, codeStart + eof1CodeAnalysisResults!.code), + 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..fb17f6f8f1 100644 --- a/packages/vm/src/evm/opcodes/functions.ts +++ b/packages/vm/src/evm/opcodes/functions.ts @@ -1012,6 +1012,7 @@ export const handlers: Map = new Map([ const [offset, length] = runState.stack.popN(2) let returnData = Buffer.alloc(0) if (!(length === BigInt(0))) { + console.log(runState.memory) returnData = runState.memory.read(Number(offset), Number(length)) } runState.eei.finish(returnData) diff --git a/packages/vm/src/evm/opcodes/util.ts b/packages/vm/src/evm/opcodes/util.ts index d1295c2be9..4d6d62bd14 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,35 @@ 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, codeLength: number) => { + // EIP-3670 - validate all opcodes + const opcodes = new Set(handlers.keys()) + opcodes.add(0xfe) // Add INVALID opcode to set + for (let x = 0; x < codeLength; x++) { + const opcode = code[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 > codeLength) { + // Push blocks mmust not exceed end of code section + return false + } + } + } + const terminatingOpcodes = new Set([0x00, 0xd3, 0xfd, 0xfe, 0xff]) + if (!terminatingOpcodes.has(code[codeLength - 1])) { + // Final opcode of code section must be terminating opcode + return false + } + return true +} 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.ts index cde4c131fb..6386169b05 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.ts @@ -3,15 +3,12 @@ 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: [] }) - t.test('invalid object formats', async (st) => { const vm = new VM({ common }) const account = await vm.stateManager.getAccount(sender) @@ -93,7 +90,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, From f14b8695964617753a27c3c2b957fdb61f971599 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Thu, 24 Feb 2022 09:51:36 -0500 Subject: [PATCH 2/5] Fix code validation and add tests --- packages/common/src/eips/3670.json | 16 +++++ packages/common/src/eips/index.ts | 1 + packages/vm/src/evm/evm.ts | 7 +-- packages/vm/src/evm/opcodes/functions.ts | 1 - packages/vm/src/evm/opcodes/util.ts | 11 ++-- packages/vm/src/index.ts | 2 +- ...> eip-3540-evm-object-format.spec copy.ts} | 10 ++- .../EIPs/eip-3670-eof-code-validation.spec.ts | 61 +++++++++++++++++++ 8 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 packages/common/src/eips/3670.json rename packages/vm/tests/api/EIPs/{eip-3540-evm--object-format.spec.ts => eip-3540-evm-object-format.spec copy.ts} (95%) create mode 100644 packages/vm/tests/api/EIPs/eip-3670-eof-code-validation.spec.ts diff --git a/packages/common/src/eips/3670.json b/packages/common/src/eips/3670.json new file mode 100644 index 0000000000..d39b69018c --- /dev/null +++ b/packages/common/src/eips/3670.json @@ -0,0 +1,16 @@ +{ + "name": "EIP-3670", + "number": 3670, + "comment": "EOF - Code Validation", + "url": "https://eips.ethereum.org/EIPS/eip-3670", + "status": "Review", + "minimumHardfork": "london", + "requiredEIPs": [ + 3540, + 3541 + ], + "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 d966987158..284e1546b3 100644 --- a/packages/vm/src/evm/evm.ts +++ b/packages/vm/src/evm/evm.ts @@ -415,13 +415,12 @@ export default class EVM { ...result, ...INVALID_BYTECODE_RESULT(message.gasLimit), } - } else { + } else if (this._vm._common.isActivatedEIP(3670)) { // EIP-3670 EOF1 code check - const codeStart = eof1CodeAnalysisResults?.data > 0 ? 10 : 7 + const codeStart = eof1CodeAnalysisResults.data > 0 ? 10 : 7 if ( !eof1ValidOpcodes( - result.returnValue.slice(codeStart, codeStart + eof1CodeAnalysisResults!.code), - eof1CodeAnalysisResults!.code + result.returnValue.slice(codeStart, codeStart + eof1CodeAnalysisResults.code) ) ) { result = { diff --git a/packages/vm/src/evm/opcodes/functions.ts b/packages/vm/src/evm/opcodes/functions.ts index fb17f6f8f1..a154e811ba 100644 --- a/packages/vm/src/evm/opcodes/functions.ts +++ b/packages/vm/src/evm/opcodes/functions.ts @@ -1012,7 +1012,6 @@ export const handlers: Map = new Map([ const [offset, length] = runState.stack.popN(2) let returnData = Buffer.alloc(0) if (!(length === BigInt(0))) { - console.log(runState.memory) returnData = runState.memory.read(Number(offset), Number(length)) } runState.eei.finish(returnData) diff --git a/packages/vm/src/evm/opcodes/util.ts b/packages/vm/src/evm/opcodes/util.ts index 4d6d62bd14..9ccee708c2 100644 --- a/packages/vm/src/evm/opcodes/util.ts +++ b/packages/vm/src/evm/opcodes/util.ts @@ -341,12 +341,15 @@ export const eof1CodeAnalysis = (container: Buffer) => { } } -export const eof1ValidOpcodes = (code: Buffer, codeLength: number) => { +export const eof1ValidOpcodes = (code: Buffer) => { // EIP-3670 - validate all opcodes const opcodes = new Set(handlers.keys()) opcodes.add(0xfe) // Add INVALID opcode to set - for (let x = 0; x < codeLength; x++) { + + let x = 0 + while (x < code.length) { const opcode = code[x] + x++ if (!opcodes.has(opcode)) { // No invalid/undefined opcodes return false @@ -354,14 +357,14 @@ export const eof1ValidOpcodes = (code: Buffer, codeLength: number) => { if (opcode >= 0x60 && opcode <= 0x7f) { // Skip data block following push x += opcode - 0x5f - if (x > codeLength) { + 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]) - if (!terminatingOpcodes.has(code[codeLength - 1])) { + if (!terminatingOpcodes.has(code[code.length - 1])) { // Final opcode of code section must be terminating opcode return false } 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 95% 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 6386169b05..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 @@ -8,7 +8,11 @@ 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 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) @@ -100,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') + }) +}) From 7595d6f1278456a7304575f46c78c928c3cbb140 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Thu, 24 Feb 2022 13:06:42 -0500 Subject: [PATCH 3/5] lint --- packages/vm/src/evm/opcodes/functions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vm/src/evm/opcodes/functions.ts b/packages/vm/src/evm/opcodes/functions.ts index a154e811ba..de03930490 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, From 3dbe33d1a70adc5b15ae415fd6cfa9e31131f8bd Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Fri, 25 Feb 2022 09:22:44 -0500 Subject: [PATCH 4/5] Add clarifying comments --- packages/common/src/eips/3670.json | 3 +-- packages/vm/src/evm/evm.ts | 3 +++ packages/vm/src/evm/opcodes/util.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/common/src/eips/3670.json b/packages/common/src/eips/3670.json index d39b69018c..9b3e814dae 100644 --- a/packages/common/src/eips/3670.json +++ b/packages/common/src/eips/3670.json @@ -6,8 +6,7 @@ "status": "Review", "minimumHardfork": "london", "requiredEIPs": [ - 3540, - 3541 + 3540 ], "gasConfig": {}, "gasPrices": {}, diff --git a/packages/vm/src/evm/evm.ts b/packages/vm/src/evm/evm.ts index 284e1546b3..ca2cb4e0f5 100644 --- a/packages/vm/src/evm/evm.ts +++ b/packages/vm/src/evm/evm.ts @@ -418,6 +418,9 @@ export default class EVM { } 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) diff --git a/packages/vm/src/evm/opcodes/util.ts b/packages/vm/src/evm/opcodes/util.ts index 9ccee708c2..b49819ad89 100644 --- a/packages/vm/src/evm/opcodes/util.ts +++ b/packages/vm/src/evm/opcodes/util.ts @@ -364,8 +364,8 @@ export const eof1ValidOpcodes = (code: Buffer) => { } } 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])) { - // Final opcode of code section must be terminating opcode return false } return true From 0368488f7fb985d252ba744db32398cb0974951a Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Fri, 25 Feb 2022 14:16:37 -0500 Subject: [PATCH 5/5] Fix big in push handler --- packages/vm/src/evm/opcodes/functions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vm/src/evm/opcodes/functions.ts b/packages/vm/src/evm/opcodes/functions.ts index de03930490..f740ca933f 100644 --- a/packages/vm/src/evm/opcodes/functions.ts +++ b/packages/vm/src/evm/opcodes/functions.ts @@ -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)