From 34f5fd0b1a33281699bb09c72849a86355f08cf9 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Tue, 26 May 2020 22:04:48 -0700 Subject: [PATCH 01/10] Implement EIP-2315: subroutines --- packages/vm/lib/evm/interpreter.ts | 6 +- packages/vm/lib/evm/opFns.ts | 26 ++++ packages/vm/lib/evm/opcodes.ts | 6 + packages/vm/lib/evm/stack.ts | 6 +- packages/vm/lib/exceptions.ts | 2 + packages/vm/tests/api/berlin/eip-2315.js | 179 +++++++++++++++++++++++ packages/vm/tests/api/evm/stack.js | 15 +- 7 files changed, 234 insertions(+), 6 deletions(-) create mode 100644 packages/vm/tests/api/berlin/eip-2315.js diff --git a/packages/vm/lib/evm/interpreter.ts b/packages/vm/lib/evm/interpreter.ts index 2a8fa53656..84766dcfec 100644 --- a/packages/vm/lib/evm/interpreter.ts +++ b/packages/vm/lib/evm/interpreter.ts @@ -20,6 +20,7 @@ export interface RunState { memoryWordCount: BN highestMemCost: BN stack: Stack + returnStack: Stack code: Buffer validJumps: number[] _common: Common @@ -36,6 +37,7 @@ export interface InterpreterStep { gasLeft: BN stateManager: StateManager stack: BN[] + returnStack: BN[] pc: number depth: number address: Buffer @@ -70,6 +72,7 @@ export default class Interpreter { memoryWordCount: new BN(0), highestMemCost: new BN(0), stack: new Stack(), + returnStack: new Stack(1023), // 1023 return stack height limit per EIP 2315 spec code: Buffer.alloc(0), validJumps: [], // TODO: Replace with EEI methods @@ -167,6 +170,7 @@ export default class Interpreter { isAsync: opcode.isAsync, }, stack: this._runState.stack._store, + returnStack: this._runState.returnStack._store, depth: this._eei._env.depth, address: this._eei._env.address, account: this._eei._env.contract, @@ -206,7 +210,7 @@ export default class Interpreter { i += code[i] - 0x5f } - if (curOpCode === 'JUMPDEST') { + if (curOpCode === 'JUMPDEST' || curOpCode === 'BEGINSUB') { jumps.push(i) } } diff --git a/packages/vm/lib/evm/opFns.ts b/packages/vm/lib/evm/opFns.ts index 560c22b6cd..3ed042cf48 100644 --- a/packages/vm/lib/evm/opFns.ts +++ b/packages/vm/lib/evm/opFns.ts @@ -546,6 +546,32 @@ export const handlers: { [k: string]: OpHandler } = { runState.stack.push(new BN(runState.eei.getGasLeft())) }, JUMPDEST: function (runState: RunState) {}, + BEGINSUB: function (runState: RunState) { + trap(ERROR.INVALID_SUBROUTINE_ENTRY + ' at ' + describeLocation(runState)) + }, + JUMPSUB: function (runState: RunState) { + const dest = runState.stack.pop() + if (dest.gt(runState.eei.getCodeSize())) { + trap(ERROR.INVALID_JUMP + ' at ' + describeLocation(runState)) + } + + const destNum = dest.toNumber() + + if (!jumpIsValid(runState, destNum)) { + trap(ERROR.INVALID_JUMP + ' at ' + describeLocation(runState)) + } + + runState.returnStack.push(new BN(runState.programCounter)) + runState.programCounter = destNum + 1 + }, + RETURNSUB: function (runState: RunState) { + if (runState.returnStack.length < 1) { + trap(ERROR.INVALID_SUBROUTINE_RETURN) + } + + const dest = runState.returnStack.pop() + runState.programCounter = dest.toNumber() + }, PUSH: function (runState: RunState) { const numToPush = runState.opCode - 0x5f const loaded = new BN( diff --git a/packages/vm/lib/evm/opcodes.ts b/packages/vm/lib/evm/opcodes.ts index 0c20f1dd00..cc89697d2e 100644 --- a/packages/vm/lib/evm/opcodes.ts +++ b/packages/vm/lib/evm/opcodes.ts @@ -179,6 +179,12 @@ const opcodes = { 0xa3: { name: 'LOG', isAsync: false }, 0xa4: { name: 'LOG', isAsync: false }, + // '0xb0' range - subroutines + // Temporary Placement pending Berlin Hardfork updates + 0xb2: { name: 'BEGINSUB', fee: 2, isAsync: false }, + 0xb3: { name: 'JUMPSUB', fee: 8, isAsync: false }, + 0xb7: { name: 'RETURNSUB', fee: 3, isAsync: false }, + // '0xf0' range - closures 0xf0: { name: 'CREATE', isAsync: true }, 0xf1: { name: 'CALL', isAsync: true }, diff --git a/packages/vm/lib/evm/stack.ts b/packages/vm/lib/evm/stack.ts index f997027d26..eb430e7669 100644 --- a/packages/vm/lib/evm/stack.ts +++ b/packages/vm/lib/evm/stack.ts @@ -7,9 +7,11 @@ const { ERROR, VmError } = require('../exceptions') */ export default class Stack { _store: BN[] + _maxHeight: number - constructor() { + constructor(maxHeight?: number) { this._store = [] + this._maxHeight = maxHeight || 1024 } get length() { @@ -25,7 +27,7 @@ export default class Stack { throw new VmError(ERROR.OUT_OF_RANGE) } - if (this._store.length > 1023) { + if (this._store.length >= this._maxHeight) { throw new VmError(ERROR.STACK_OVERFLOW) } diff --git a/packages/vm/lib/exceptions.ts b/packages/vm/lib/exceptions.ts index 60a97b0404..62e201c0c0 100644 --- a/packages/vm/lib/exceptions.ts +++ b/packages/vm/lib/exceptions.ts @@ -12,6 +12,8 @@ export enum ERROR { STOP = 'stop', REFUND_EXHAUSTED = 'refund exhausted', VALUE_OVERFLOW = 'value overflow', + INVALID_SUBROUTINE_ENTRY = 'invalid subroutine entry', + INVALID_SUBROUTINE_RETURN = 'invalid retsub', } export class VmError { diff --git a/packages/vm/tests/api/berlin/eip-2315.js b/packages/vm/tests/api/berlin/eip-2315.js new file mode 100644 index 0000000000..f4f197dfc1 --- /dev/null +++ b/packages/vm/tests/api/berlin/eip-2315.js @@ -0,0 +1,179 @@ +const tape = require('tape') +const BN = require('bn.js') +const VM = require('../../../dist/index').default + +tape('Berlin: EIP 2315 tests', t => { + let callArgs; + let stepCounter; + let vm; + + const runTest = async function(test, st){ + let i = 0; + vm = new VM(); + + vm.on('step', function(step){ + if (test.steps.length){ + st.equal(step.pc, test.steps[i].expectedPC) + st.equal(step.opcode.name, test.steps[i].expectedOpcode) + } + i++; + }) + + const result = await vm.runCode({ + code: Buffer.from(test.code, 'hex'), + gasLimit: new BN(0xffffffffff) + }) + + st.equal(i, test.totalSteps) + return result; + } + + // EIP test case 1 + t.test('should jump into a subroutine, back out and stop', async st => { + const test = { + code: "6004b300b2b7", + totalSteps: 4, + steps: [ + { expectedPC: 0, expectedOpcode: "PUSH1" }, + { expectedPC: 2, expectedOpcode: "JUMPSUB" }, + { expectedPC: 5, expectedOpcode: "RETURNSUB" }, + { expectedPC: 3, expectedOpcode: "STOP" } + ] + } + + const result = await runTest(test, st) + st.equal(undefined, result.exceptionError) + st.end() + }) + + // EIP test case 2 + t.test('should go into two depths of subroutines', async st => { + const test = { + code: "6800000000000000000cb300b26011b3b7b2b7", + totalSteps: 7, + steps: [ + { expectedPC: 0, expectedOpcode: "PUSH9" }, + { expectedPC: 10, expectedOpcode: "JUMPSUB" }, + { expectedPC: 13, expectedOpcode: "PUSH1" }, + { expectedPC: 15, expectedOpcode: "JUMPSUB" }, + { expectedPC: 18, expectedOpcode: "RETURNSUB" }, + { expectedPC: 16, expectedOpcode: "RETURNSUB" }, + { expectedPC: 11, expectedOpcode: "STOP" } + ] + } + + const result = await runTest(test, st) + st.equal(undefined, result.exceptionError) + st.end() + }) + + // EIP test case 3 + t.test('should error on invalid jump (location out of code range)', async st => { + const test = { + code: "6801000000000000000cb300b26011b3b7b2b7", + totalSteps: 2, + steps: [ + { expectedPC: 0, expectedOpcode: "PUSH9" }, + { expectedPC: 10, expectedOpcode: "JUMPSUB" }, + ] + } + + result = await runTest(test, st) + st.equal(true, result.exceptionError.error.includes('invalid JUMP at')) + st.end() + }) + + // hyperledger/besu PR 717 test case + // https://github.com/hyperledger/besu/pull/717/files#diff-5d1330bc567b68d81941896ef2d2ce88R114 + t.test('should error on invalid jump (dest not BEGINSUB)', async st => { + const test = { + code: "6005b300b2b7", + totalSteps: 2, + steps: [ + { expectedPC: 0, expectedOpcode: "PUSH1" }, + { expectedPC: 2, expectedOpcode: "JUMPSUB" } + ] + } + + result = await runTest(test, st) + st.equal(true, result.exceptionError.error.includes('invalid JUMP at')) + st.end() + }) + + // EIP test case 4 + t.test('should error when the return stack is too shallow', async st => { + const test = { + code: "b75858", + totalSteps: 1, + steps: [ + { expectedPC: 0, expectedOpcode: "RETURNSUB" } + ] + } + + result = await runTest(test, st) + st.equal(true, result.exceptionError.error.includes('invalid retsub')) + st.end() + }) + + // EIP test case 5 + // Note: this case differs slightly from the EIP spec which expects STOP as the last step. + t.test('it should hit the `virtual stop` when JUMP is on the last byte of code (EIP)', async st => { + const test = { + code: "600556b2b75b6003b3", + totalSteps: 6, + steps: [ + { expectedPC: 0, expectedOpcode: "PUSH1" }, + { expectedPC: 2, expectedOpcode: "JUMP" }, + { expectedPC: 5, expectedOpcode: "JUMPDEST" }, + { expectedPC: 6, expectedOpcode: "PUSH1" }, + { expectedPC: 8, expectedOpcode: "JUMPSUB" }, + { expectedPC: 4, expectedOpcode: "RETURNSUB" } + ] + } + + result = await runTest(test, st) + st.equal(undefined, result.exceptionError) + st.end() + }) + + // The code recursively calls itself. It should error when the returns-stack grows above 1023 + t.test('it should error if the return stack size limit (1023) is hit', async st => { + const ops = [ + '60', '03', // PUSH1 3 # 1 + 'b3', // JUMPSUB # 2 + 'b2', // BEGINSUB # 3 + '60', '03', // PUSH1 3 # 4 + 'b3', // JUMPSUB # 5 + ] + + // Max return stack height is 1023 + // First return stack entry runs 4 ops (1, 2, 4, 5) + // Next 1022 are a loop of 2 ops (4, 5) + const expectedTotalSteps = (1022 * 2) + 4 + const test = { + code: ops.join(''), + totalSteps: expectedTotalSteps, + steps: [] + } + + result = await runTest(test, st) + st.equal(true, result.exceptionError.error.includes('stack overflow')) + st.end() + }) + + // EIP test case 6 + t.test('should error when walking into BEGINSUB', async st => { + const test = { + code: "b2", + totalSteps: 1, + steps: [ + { expectedPC: 0, expectedOpcode: "BEGINSUB" } + ] + } + + result = await runTest(test, st) + st.equal(true, result.exceptionError.error.includes('invalid subroutine entry')) + st.end() + }) +}) + diff --git a/packages/vm/tests/api/evm/stack.js b/packages/vm/tests/api/evm/stack.js index ad9240f9ff..08854abeb2 100644 --- a/packages/vm/tests/api/evm/stack.js +++ b/packages/vm/tests/api/evm/stack.js @@ -64,6 +64,15 @@ tape('Stack', t => { st.end() }) + t.test('overflow limit should be configurable', st => { + const s = new Stack(1023) + for (let i = 0; i < 1023; i++) { + s.push(new BN(i)) + } + st.throws(() => s.push(new BN(1023))) + st.end() + }) + t.test('should swap top with itself', st => { const s = new Stack() s.push(new BN(5)) @@ -130,11 +139,11 @@ tape('Stack', t => { DUP1 DUP1 PUSH1 0x01 - CALLER + CALLER DUP3 CALL stack: [0, CALLER, 1, 0, 0, 0, 0, 0] POP pop the call result (1) - PUSH1 0x00 + PUSH1 0x00 MSTORE we now expect that the stack (prior to MSTORE) is [0, 0] PUSH1 0x20 PUSH1 0x00 @@ -150,7 +159,7 @@ tape('Stack', t => { } try { const res = await vm.runCall(runCallArgs) - const executionReturnValue = res.execResult.returnValue + const executionReturnValue = res.execResult.returnValue st.assert(executionReturnValue.equals(expectedReturnValue)) st.end() } catch(e) { From a70c80bcef546ed7bfaf1231229aeb3fcf3b9276 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Tue, 26 May 2020 22:32:56 -0700 Subject: [PATCH 02/10] Update gas costs to latest EIP state --- packages/vm/lib/evm/opcodes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vm/lib/evm/opcodes.ts b/packages/vm/lib/evm/opcodes.ts index cc89697d2e..3a8961f45e 100644 --- a/packages/vm/lib/evm/opcodes.ts +++ b/packages/vm/lib/evm/opcodes.ts @@ -181,9 +181,9 @@ const opcodes = { // '0xb0' range - subroutines // Temporary Placement pending Berlin Hardfork updates - 0xb2: { name: 'BEGINSUB', fee: 2, isAsync: false }, + 0xb2: { name: 'BEGINSUB', fee: 1, isAsync: false }, 0xb3: { name: 'JUMPSUB', fee: 8, isAsync: false }, - 0xb7: { name: 'RETURNSUB', fee: 3, isAsync: false }, + 0xb7: { name: 'RETURNSUB', fee: 2, isAsync: false }, // '0xf0' range - closures 0xf0: { name: 'CREATE', isAsync: true }, From 6c28a86ed09322e6ed323b0150afc9c8eb966ca7 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Wed, 27 May 2020 11:37:31 -0700 Subject: [PATCH 03/10] Use valid JUMPSUB table and normalize error messages --- packages/vm/lib/evm/interpreter.ts | 25 ++++++++++++++++++----- packages/vm/lib/evm/opFns.ts | 15 +++++++++----- packages/vm/lib/exceptions.ts | 5 +++-- packages/vm/tests/api/berlin/eip-2315.js | 26 +++++++++++++++++++----- 4 files changed, 54 insertions(+), 17 deletions(-) diff --git a/packages/vm/lib/evm/interpreter.ts b/packages/vm/lib/evm/interpreter.ts index 84766dcfec..26e2948abe 100644 --- a/packages/vm/lib/evm/interpreter.ts +++ b/packages/vm/lib/evm/interpreter.ts @@ -23,6 +23,7 @@ export interface RunState { returnStack: Stack code: Buffer validJumps: number[] + validJumpSubs: number[] _common: Common stateManager: StateManager eei: EEI @@ -52,6 +53,11 @@ export interface InterpreterStep { codeAddress: Buffer } +interface JumpDests { + jumps: number[] + jumpSubs: number[] +} + /** * Parses and executes EVM bytecode. */ @@ -75,6 +81,7 @@ export default class Interpreter { returnStack: new Stack(1023), // 1023 return stack height limit per EIP 2315 spec code: Buffer.alloc(0), validJumps: [], + validJumpSubs: [], // TODO: Replace with EEI methods _common: this._vm._common, stateManager: this._state, @@ -85,7 +92,10 @@ export default class Interpreter { async run(code: Buffer, opts: InterpreterOpts = {}): Promise { this._runState.code = code this._runState.programCounter = opts.pc || this._runState.programCounter - this._runState.validJumps = this._getValidJumpDests(code) + + const valid = this._getValidJumpDests(code) + this._runState.validJumps = valid.jumps + this._runState.validJumpSubs = valid.jumpSubs // Check that the programCounter is in range const pc = this._runState.programCounter @@ -198,9 +208,10 @@ export default class Interpreter { return this._vm._emit('step', eventObj) } - // Returns all valid jump destinations. - _getValidJumpDests(code: Buffer): number[] { + // Returns all valid jump and jumpsub destinations. + _getValidJumpDests(code: Buffer): JumpDests { const jumps = [] + const jumpSubs = [] for (let i = 0; i < code.length; i++) { const curOpCode = this.lookupOpInfo(code[i]).name @@ -210,11 +221,15 @@ export default class Interpreter { i += code[i] - 0x5f } - if (curOpCode === 'JUMPDEST' || curOpCode === 'BEGINSUB') { + if (curOpCode === 'JUMPDEST') { jumps.push(i) } + + if (curOpCode === 'BEGINSUB') { + jumpSubs.push(i) + } } - return jumps + return { jumps, jumpSubs } } } diff --git a/packages/vm/lib/evm/opFns.ts b/packages/vm/lib/evm/opFns.ts index 3ed042cf48..319dbbe44e 100644 --- a/packages/vm/lib/evm/opFns.ts +++ b/packages/vm/lib/evm/opFns.ts @@ -547,18 +547,18 @@ export const handlers: { [k: string]: OpHandler } = { }, JUMPDEST: function (runState: RunState) {}, BEGINSUB: function (runState: RunState) { - trap(ERROR.INVALID_SUBROUTINE_ENTRY + ' at ' + describeLocation(runState)) + trap(ERROR.INVALID_BEGINSUB + ' at ' + describeLocation(runState)) }, JUMPSUB: function (runState: RunState) { const dest = runState.stack.pop() if (dest.gt(runState.eei.getCodeSize())) { - trap(ERROR.INVALID_JUMP + ' at ' + describeLocation(runState)) + trap(ERROR.INVALID_JUMPSUB + ' at ' + describeLocation(runState)) } const destNum = dest.toNumber() - if (!jumpIsValid(runState, destNum)) { - trap(ERROR.INVALID_JUMP + ' at ' + describeLocation(runState)) + if (!jumpSubIsValid(runState, destNum)) { + trap(ERROR.INVALID_JUMPSUB + ' at ' + describeLocation(runState)) } runState.returnStack.push(new BN(runState.programCounter)) @@ -566,7 +566,7 @@ export const handlers: { [k: string]: OpHandler } = { }, RETURNSUB: function (runState: RunState) { if (runState.returnStack.length < 1) { - trap(ERROR.INVALID_SUBROUTINE_RETURN) + trap(ERROR.INVALID_RETURNSUB) } const dest = runState.returnStack.pop() @@ -953,6 +953,11 @@ function maxCallGas(gasLimit: BN, gasLeft: BN, runState: RunState): BN { } } +// checks if a jumpsub is valid given a destination +function jumpSubIsValid(runState: RunState, dest: number): boolean { + return runState.validJumpSubs.indexOf(dest) !== -1 +} + async function getContractStorage(runState: RunState, address: Buffer, key: Buffer) { const current = await runState.stateManager.getContractStorage(address, key) if ( diff --git a/packages/vm/lib/exceptions.ts b/packages/vm/lib/exceptions.ts index 62e201c0c0..0c4230cd67 100644 --- a/packages/vm/lib/exceptions.ts +++ b/packages/vm/lib/exceptions.ts @@ -12,8 +12,9 @@ export enum ERROR { STOP = 'stop', REFUND_EXHAUSTED = 'refund exhausted', VALUE_OVERFLOW = 'value overflow', - INVALID_SUBROUTINE_ENTRY = 'invalid subroutine entry', - INVALID_SUBROUTINE_RETURN = 'invalid retsub', + INVALID_BEGINSUB = 'invalid BEGINSUB', + INVALID_RETURNSUB = 'invalid RETURNSUB', + INVALID_JUMPSUB = 'invalid JUMPSUB', } export class VmError { diff --git a/packages/vm/tests/api/berlin/eip-2315.js b/packages/vm/tests/api/berlin/eip-2315.js index f4f197dfc1..5d1730f275 100644 --- a/packages/vm/tests/api/berlin/eip-2315.js +++ b/packages/vm/tests/api/berlin/eip-2315.js @@ -68,7 +68,7 @@ tape('Berlin: EIP 2315 tests', t => { }) // EIP test case 3 - t.test('should error on invalid jump (location out of code range)', async st => { + t.test('should error on invalid jumpsub (location out of code range)', async st => { const test = { code: "6801000000000000000cb300b26011b3b7b2b7", totalSteps: 2, @@ -79,13 +79,13 @@ tape('Berlin: EIP 2315 tests', t => { } result = await runTest(test, st) - st.equal(true, result.exceptionError.error.includes('invalid JUMP at')) + st.equal(true, result.exceptionError.error.includes('invalid JUMPSUB at')) st.end() }) // hyperledger/besu PR 717 test case // https://github.com/hyperledger/besu/pull/717/files#diff-5d1330bc567b68d81941896ef2d2ce88R114 - t.test('should error on invalid jump (dest not BEGINSUB)', async st => { + t.test('should error on invalid jumpsub (dest not BEGINSUB)', async st => { const test = { code: "6005b300b2b7", totalSteps: 2, @@ -96,6 +96,22 @@ tape('Berlin: EIP 2315 tests', t => { } result = await runTest(test, st) + st.equal(true, result.exceptionError.error.includes('invalid JUMPSUB at')) + st.end() + }) + + // Code is same as EIP example 1 above, with JUMP substituted for JUMPSUB + t.test('BEGINSUB should not be a valid dest for JUMP', async st => { + const test = { + code: "60045600b2b7", + totalSteps: 2, + steps: [ + { expectedPC: 0, expectedOpcode: "PUSH1" }, + { expectedPC: 2, expectedOpcode: "JUMP" } + ] + } + + const result = await runTest(test, st) st.equal(true, result.exceptionError.error.includes('invalid JUMP at')) st.end() }) @@ -111,7 +127,7 @@ tape('Berlin: EIP 2315 tests', t => { } result = await runTest(test, st) - st.equal(true, result.exceptionError.error.includes('invalid retsub')) + st.equal(true, result.exceptionError.error.includes('invalid RETURNSUB')) st.end() }) @@ -172,7 +188,7 @@ tape('Berlin: EIP 2315 tests', t => { } result = await runTest(test, st) - st.equal(true, result.exceptionError.error.includes('invalid subroutine entry')) + st.equal(true, result.exceptionError.error.includes('invalid BEGINSUB')) st.end() }) }) From 6a94625c27f0fe01f8d6a3eaa429011f6de8234b Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 28 May 2020 09:30:07 -0700 Subject: [PATCH 04/10] Add berlin to list of supported hardforks --- packages/vm/lib/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vm/lib/index.ts b/packages/vm/lib/index.ts index ceaece0fab..9beef4e267 100644 --- a/packages/vm/lib/index.ts +++ b/packages/vm/lib/index.ts @@ -118,6 +118,7 @@ export default class VM extends AsyncEventEmitter { 'petersburg', 'istanbul', 'muirGlacier', + 'berlin', ] this._common = new Common(chain, hardfork, supportedHardforks) From 553ddef44a96c2e0763277b7382f9c01a8e1edc0 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Mon, 1 Jun 2020 18:20:10 -0700 Subject: [PATCH 05/10] Update opcodes and gas costs --- packages/common/src/hardforks/berlin.json | 15 ++++++++++++++- packages/vm/lib/evm/opcodes.ts | 9 +++------ packages/vm/tests/api/berlin/eip-2315.js | 22 +++++++++++----------- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/packages/common/src/hardforks/berlin.json b/packages/common/src/hardforks/berlin.json index e9dba8f68b..c2474ade77 100644 --- a/packages/common/src/hardforks/berlin.json +++ b/packages/common/src/hardforks/berlin.json @@ -6,7 +6,20 @@ "status": "Draft" }, "gasConfig": {}, - "gasPrices": {}, + "gasPrices": { + "beginsub": { + "v": 2, + "d": "Base fee of the BEGINSUB opcode" + }, + "returnsub": { + "v": 5, + "d": "Base fee of the RETURNSUB opcode" + }, + "jumpsub": { + "v": 10, + "d": "Base fee of the JUMPSUB opcode" + } + }, "vm": {}, "pow": {} } diff --git a/packages/vm/lib/evm/opcodes.ts b/packages/vm/lib/evm/opcodes.ts index 3a8961f45e..3ae30274a4 100644 --- a/packages/vm/lib/evm/opcodes.ts +++ b/packages/vm/lib/evm/opcodes.ts @@ -104,6 +104,9 @@ const opcodes = { 0x59: { name: 'MSIZE', isAsync: false }, 0x5a: { name: 'GAS', isAsync: false }, 0x5b: { name: 'JUMPDEST', isAsync: false }, + 0x5c: { name: 'BEGINSUB', isAsync: false }, + 0x5d: { name: 'RETURNSUB', isAsync: false }, + 0x5e: { name: 'JUMPSUB', isAsync: false }, // 0x60, range 0x60: { name: 'PUSH', isAsync: false }, @@ -179,12 +182,6 @@ const opcodes = { 0xa3: { name: 'LOG', isAsync: false }, 0xa4: { name: 'LOG', isAsync: false }, - // '0xb0' range - subroutines - // Temporary Placement pending Berlin Hardfork updates - 0xb2: { name: 'BEGINSUB', fee: 1, isAsync: false }, - 0xb3: { name: 'JUMPSUB', fee: 8, isAsync: false }, - 0xb7: { name: 'RETURNSUB', fee: 2, isAsync: false }, - // '0xf0' range - closures 0xf0: { name: 'CREATE', isAsync: true }, 0xf1: { name: 'CALL', isAsync: true }, diff --git a/packages/vm/tests/api/berlin/eip-2315.js b/packages/vm/tests/api/berlin/eip-2315.js index 5d1730f275..96f34e35dc 100644 --- a/packages/vm/tests/api/berlin/eip-2315.js +++ b/packages/vm/tests/api/berlin/eip-2315.js @@ -31,7 +31,7 @@ tape('Berlin: EIP 2315 tests', t => { // EIP test case 1 t.test('should jump into a subroutine, back out and stop', async st => { const test = { - code: "6004b300b2b7", + code: "60045e005c5d", totalSteps: 4, steps: [ { expectedPC: 0, expectedOpcode: "PUSH1" }, @@ -49,7 +49,7 @@ tape('Berlin: EIP 2315 tests', t => { // EIP test case 2 t.test('should go into two depths of subroutines', async st => { const test = { - code: "6800000000000000000cb300b26011b3b7b2b7", + code: "6800000000000000000c5e005c60115e5d5c5d", totalSteps: 7, steps: [ { expectedPC: 0, expectedOpcode: "PUSH9" }, @@ -70,7 +70,7 @@ tape('Berlin: EIP 2315 tests', t => { // EIP test case 3 t.test('should error on invalid jumpsub (location out of code range)', async st => { const test = { - code: "6801000000000000000cb300b26011b3b7b2b7", + code: "6801000000000000000c5e005c60115e5d5c5d", totalSteps: 2, steps: [ { expectedPC: 0, expectedOpcode: "PUSH9" }, @@ -87,7 +87,7 @@ tape('Berlin: EIP 2315 tests', t => { // https://github.com/hyperledger/besu/pull/717/files#diff-5d1330bc567b68d81941896ef2d2ce88R114 t.test('should error on invalid jumpsub (dest not BEGINSUB)', async st => { const test = { - code: "6005b300b2b7", + code: "60055e005c5d", totalSteps: 2, steps: [ { expectedPC: 0, expectedOpcode: "PUSH1" }, @@ -103,7 +103,7 @@ tape('Berlin: EIP 2315 tests', t => { // Code is same as EIP example 1 above, with JUMP substituted for JUMPSUB t.test('BEGINSUB should not be a valid dest for JUMP', async st => { const test = { - code: "60045600b2b7", + code: "600456005c5d", totalSteps: 2, steps: [ { expectedPC: 0, expectedOpcode: "PUSH1" }, @@ -119,7 +119,7 @@ tape('Berlin: EIP 2315 tests', t => { // EIP test case 4 t.test('should error when the return stack is too shallow', async st => { const test = { - code: "b75858", + code: "5d5858", totalSteps: 1, steps: [ { expectedPC: 0, expectedOpcode: "RETURNSUB" } @@ -135,7 +135,7 @@ tape('Berlin: EIP 2315 tests', t => { // Note: this case differs slightly from the EIP spec which expects STOP as the last step. t.test('it should hit the `virtual stop` when JUMP is on the last byte of code (EIP)', async st => { const test = { - code: "600556b2b75b6003b3", + code: "6005565c5d5b60035e", totalSteps: 6, steps: [ { expectedPC: 0, expectedOpcode: "PUSH1" }, @@ -156,10 +156,10 @@ tape('Berlin: EIP 2315 tests', t => { t.test('it should error if the return stack size limit (1023) is hit', async st => { const ops = [ '60', '03', // PUSH1 3 # 1 - 'b3', // JUMPSUB # 2 - 'b2', // BEGINSUB # 3 + '5e', // JUMPSUB # 2 + '5c', // BEGINSUB # 3 '60', '03', // PUSH1 3 # 4 - 'b3', // JUMPSUB # 5 + '5e', // JUMPSUB # 5 ] // Max return stack height is 1023 @@ -180,7 +180,7 @@ tape('Berlin: EIP 2315 tests', t => { // EIP test case 6 t.test('should error when walking into BEGINSUB', async st => { const test = { - code: "b2", + code: "5c", totalSteps: 1, steps: [ { expectedPC: 0, expectedOpcode: "BEGINSUB" } From 170e5e84773558837f99494037ff1a5dbc195f31 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Mon, 15 Jun 2020 19:56:59 -0700 Subject: [PATCH 06/10] Restrict opcodes to berlin hardfork --- packages/vm/lib/evm/opFns.ts | 38 ++++++++++++++++-------- packages/vm/tests/api/berlin/eip-2315.js | 5 +++- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/vm/lib/evm/opFns.ts b/packages/vm/lib/evm/opFns.ts index 319dbbe44e..a7b64eb421 100644 --- a/packages/vm/lib/evm/opFns.ts +++ b/packages/vm/lib/evm/opFns.ts @@ -1,5 +1,5 @@ import BN = require('bn.js') -import { keccak256, setLengthRight, TWO_POW256, MAX_INTEGER, KECCAK256_NULL } from 'ethereumjs-util' +import * as utils from 'ethereumjs-util' import { ERROR, VmError } from '../exceptions' import { RunState } from './interpreter' @@ -38,12 +38,12 @@ export const handlers: { [k: string]: OpHandler } = { }, ADD: function (runState: RunState) { const [a, b] = runState.stack.popN(2) - const r = a.add(b).mod(TWO_POW256) + const r = a.add(b).mod(utils.TWO_POW256) runState.stack.push(r) }, MUL: function (runState: RunState) { const [a, b] = runState.stack.popN(2) - const r = a.mul(b).mod(TWO_POW256) + const r = a.mul(b).mod(utils.TWO_POW256) runState.stack.push(r) }, SUB: function (runState: RunState) { @@ -137,7 +137,7 @@ export const handlers: { [k: string]: OpHandler } = { runState.stack.push(new BN(0)) return } - const m = BN.red(TWO_POW256) + const m = BN.red(utils.TWO_POW256) const redBase = base.toRed(m) const r = redBase.redPow(exponent) runState.stack.push(r.fromRed()) @@ -226,7 +226,7 @@ export const handlers: { [k: string]: OpHandler } = { return } - const r = b.shln(a.toNumber()).iand(MAX_INTEGER) + const r = b.shln(a.toNumber()).iand(utils.MAX_INTEGER) runState.stack.push(r) }, SHR: function (runState: RunState) { @@ -246,7 +246,7 @@ export const handlers: { [k: string]: OpHandler } = { const isSigned = b.testn(255) if (a.gten(256)) { if (isSigned) { - r = new BN(MAX_INTEGER) + r = new BN(utils.MAX_INTEGER) } else { r = new BN(0) } @@ -257,7 +257,7 @@ export const handlers: { [k: string]: OpHandler } = { const c = b.shrn(a.toNumber()) if (isSigned) { const shiftedOutWidth = 255 - a.toNumber() - const mask = MAX_INTEGER.shrn(shiftedOutWidth).shln(shiftedOutWidth) + const mask = utils.MAX_INTEGER.shrn(shiftedOutWidth).shln(shiftedOutWidth) r = c.ior(mask) } else { r = c @@ -276,7 +276,7 @@ export const handlers: { [k: string]: OpHandler } = { runState.eei.useGas( new BN(runState._common.param('gasPrices', 'sha3Word')).imul(divCeil(length, new BN(32))), ) - const r = new BN(keccak256(data)) + const r = new BN(utils.keccak256(data)) runState.stack.push(r) }, // 0x30 range - closure state @@ -308,7 +308,7 @@ export const handlers: { [k: string]: OpHandler } = { const i = pos.toNumber() let loaded = runState.eei.getCallData().slice(i, i + 32) loaded = loaded.length ? loaded : Buffer.from([0]) - const r = new BN(setLengthRight(loaded, 32)) + const r = new BN(utils.setLengthRight(loaded, 32)) runState.stack.push(r) }, @@ -382,11 +382,11 @@ export const handlers: { [k: string]: OpHandler } = { const code = await runState.eei.getExternalCode(address) if (code.length === 0) { - runState.stack.push(new BN(KECCAK256_NULL)) + runState.stack.push(new BN(utils.KECCAK256_NULL)) return } - runState.stack.push(new BN(keccak256(code))) + runState.stack.push(new BN(utils.keccak256(code))) }, RETURNDATASIZE: function (runState: RunState) { runState.stack.push(runState.eei.getReturnDataSize()) @@ -547,10 +547,18 @@ export const handlers: { [k: string]: OpHandler } = { }, JUMPDEST: function (runState: RunState) {}, BEGINSUB: function (runState: RunState) { + if (!runState._common.gteHardfork('berlin')) { + trap(ERROR.INVALID_OPCODE) + } + trap(ERROR.INVALID_BEGINSUB + ' at ' + describeLocation(runState)) }, JUMPSUB: function (runState: RunState) { const dest = runState.stack.pop() + if (!runState._common.gteHardfork('berlin')) { + trap(ERROR.INVALID_OPCODE) + } + if (dest.gt(runState.eei.getCodeSize())) { trap(ERROR.INVALID_JUMPSUB + ' at ' + describeLocation(runState)) } @@ -565,6 +573,10 @@ export const handlers: { [k: string]: OpHandler } = { runState.programCounter = destNum + 1 }, RETURNSUB: function (runState: RunState) { + if (!runState._common.gteHardfork('berlin')) { + trap(ERROR.INVALID_OPCODE) + } + if (runState.returnStack.length < 1) { trap(ERROR.INVALID_RETURNSUB) } @@ -862,7 +874,7 @@ export const handlers: { [k: string]: OpHandler } = { } function describeLocation(runState: RunState) { - var hash = keccak256(runState.eei.getCode()).toString('hex') + var hash = utils.keccak256(runState.eei.getCode()).toString('hex') var address = runState.eei.getAddress().toString('hex') var pc = runState.programCounter - 1 return hash + '/' + address + ':' + pc @@ -922,7 +934,7 @@ function getDataSlice(data: Buffer, offset: BN, length: BN): Buffer { data = data.slice(offset.toNumber(), end.toNumber()) // Right-pad with zeros to fill dataLength bytes - data = setLengthRight(data, length.toNumber()) + data = utils.setLengthRight(data, length.toNumber()) return data } diff --git a/packages/vm/tests/api/berlin/eip-2315.js b/packages/vm/tests/api/berlin/eip-2315.js index 96f34e35dc..bb33e9ddb4 100644 --- a/packages/vm/tests/api/berlin/eip-2315.js +++ b/packages/vm/tests/api/berlin/eip-2315.js @@ -1,15 +1,18 @@ const tape = require('tape') const BN = require('bn.js') const VM = require('../../../dist/index').default +const Common = require('@ethereumjs/common').default + tape('Berlin: EIP 2315 tests', t => { let callArgs; let stepCounter; let vm; + const common = new Common('mainnet', 'berlin') const runTest = async function(test, st){ let i = 0; - vm = new VM(); + vm = new VM({ common: common }); vm.on('step', function(step){ if (test.steps.length){ From a8fb708f27ea2691bbf85edb3f339f904484f061 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Mon, 15 Jun 2020 21:01:47 -0700 Subject: [PATCH 07/10] Update ethereumjs-testing to 1.3.3 --- packages/vm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vm/package.json b/packages/vm/package.json index ad662d9635..32166468d7 100644 --- a/packages/vm/package.json +++ b/packages/vm/package.json @@ -68,7 +68,7 @@ "@types/node": "^11.13.4", "@types/tape": "^4.13.0", "browserify": "^16.5.1", - "ethereumjs-testing": "git+https://github.com/ethereumjs/ethereumjs-testing.git#v1.3.1", + "ethereumjs-testing": "git+https://github.com/ethereumjs/ethereumjs-testing.git#v1.3.3", "karma": "^4.1.0", "karma-browserify": "^6.0.0", "karma-chrome-launcher": "^2.2.0", From 8efca8f82c16a3126d6fbcaaf362c4dd1ef41698 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Mon, 15 Jun 2020 21:44:08 -0700 Subject: [PATCH 08/10] Remove rebase/merge regressions --- packages/vm/lib/evm/opFns.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/vm/lib/evm/opFns.ts b/packages/vm/lib/evm/opFns.ts index a7b64eb421..481d627157 100644 --- a/packages/vm/lib/evm/opFns.ts +++ b/packages/vm/lib/evm/opFns.ts @@ -1,5 +1,5 @@ import BN = require('bn.js') -import * as utils from 'ethereumjs-util' +import { keccak256, setLengthRight, TWO_POW256, MAX_INTEGER, KECCAK256_NULL } from 'ethereumjs-util' import { ERROR, VmError } from '../exceptions' import { RunState } from './interpreter' @@ -38,12 +38,12 @@ export const handlers: { [k: string]: OpHandler } = { }, ADD: function (runState: RunState) { const [a, b] = runState.stack.popN(2) - const r = a.add(b).mod(utils.TWO_POW256) + const r = a.add(b).mod(TWO_POW256) runState.stack.push(r) }, MUL: function (runState: RunState) { const [a, b] = runState.stack.popN(2) - const r = a.mul(b).mod(utils.TWO_POW256) + const r = a.mul(b).mod(TWO_POW256) runState.stack.push(r) }, SUB: function (runState: RunState) { @@ -137,7 +137,7 @@ export const handlers: { [k: string]: OpHandler } = { runState.stack.push(new BN(0)) return } - const m = BN.red(utils.TWO_POW256) + const m = BN.red(TWO_POW256) const redBase = base.toRed(m) const r = redBase.redPow(exponent) runState.stack.push(r.fromRed()) @@ -226,7 +226,7 @@ export const handlers: { [k: string]: OpHandler } = { return } - const r = b.shln(a.toNumber()).iand(utils.MAX_INTEGER) + const r = b.shln(a.toNumber()).iand(MAX_INTEGER) runState.stack.push(r) }, SHR: function (runState: RunState) { @@ -246,7 +246,7 @@ export const handlers: { [k: string]: OpHandler } = { const isSigned = b.testn(255) if (a.gten(256)) { if (isSigned) { - r = new BN(utils.MAX_INTEGER) + r = new BN(MAX_INTEGER) } else { r = new BN(0) } @@ -257,7 +257,7 @@ export const handlers: { [k: string]: OpHandler } = { const c = b.shrn(a.toNumber()) if (isSigned) { const shiftedOutWidth = 255 - a.toNumber() - const mask = utils.MAX_INTEGER.shrn(shiftedOutWidth).shln(shiftedOutWidth) + const mask = MAX_INTEGER.shrn(shiftedOutWidth).shln(shiftedOutWidth) r = c.ior(mask) } else { r = c @@ -276,7 +276,7 @@ export const handlers: { [k: string]: OpHandler } = { runState.eei.useGas( new BN(runState._common.param('gasPrices', 'sha3Word')).imul(divCeil(length, new BN(32))), ) - const r = new BN(utils.keccak256(data)) + const r = new BN(keccak256(data)) runState.stack.push(r) }, // 0x30 range - closure state @@ -308,7 +308,7 @@ export const handlers: { [k: string]: OpHandler } = { const i = pos.toNumber() let loaded = runState.eei.getCallData().slice(i, i + 32) loaded = loaded.length ? loaded : Buffer.from([0]) - const r = new BN(utils.setLengthRight(loaded, 32)) + const r = new BN(setLengthRight(loaded, 32)) runState.stack.push(r) }, @@ -382,11 +382,11 @@ export const handlers: { [k: string]: OpHandler } = { const code = await runState.eei.getExternalCode(address) if (code.length === 0) { - runState.stack.push(new BN(utils.KECCAK256_NULL)) + runState.stack.push(new BN(KECCAK256_NULL)) return } - runState.stack.push(new BN(utils.keccak256(code))) + runState.stack.push(new BN(keccak256(code))) }, RETURNDATASIZE: function (runState: RunState) { runState.stack.push(runState.eei.getReturnDataSize()) @@ -874,7 +874,7 @@ export const handlers: { [k: string]: OpHandler } = { } function describeLocation(runState: RunState) { - var hash = utils.keccak256(runState.eei.getCode()).toString('hex') + var hash = keccak256(runState.eei.getCode()).toString('hex') var address = runState.eei.getAddress().toString('hex') var pc = runState.programCounter - 1 return hash + '/' + address + ':' + pc @@ -934,7 +934,7 @@ function getDataSlice(data: Buffer, offset: BN, length: BN): Buffer { data = data.slice(offset.toNumber(), end.toNumber()) // Right-pad with zeros to fill dataLength bytes - data = utils.setLengthRight(data, length.toNumber()) + data = setLengthRight(data, length.toNumber()) return data } From 59d338cfe9c54dae5122c043fcdc486a890cae61 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Wed, 12 Aug 2020 10:06:55 +0200 Subject: [PATCH 09/10] vm: moved BEGINSUB, RETURNSUB and JUMPSUB opcodes to dedicated Berlin HF opcode section in opcodes.ts --- packages/vm/lib/evm/opcodes.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/vm/lib/evm/opcodes.ts b/packages/vm/lib/evm/opcodes.ts index 3ae30274a4..455516c2c2 100644 --- a/packages/vm/lib/evm/opcodes.ts +++ b/packages/vm/lib/evm/opcodes.ts @@ -104,9 +104,6 @@ const opcodes = { 0x59: { name: 'MSIZE', isAsync: false }, 0x5a: { name: 'GAS', isAsync: false }, 0x5b: { name: 'JUMPDEST', isAsync: false }, - 0x5c: { name: 'BEGINSUB', isAsync: false }, - 0x5d: { name: 'RETURNSUB', isAsync: false }, - 0x5e: { name: 'JUMPSUB', isAsync: false }, // 0x60, range 0x60: { name: 'PUSH', isAsync: false }, @@ -228,6 +225,14 @@ const hardforkOpcodes = [ 0x47: { name: 'SELFBALANCE', isAsync: false }, // EIP 1884 }, }, + { + hardforkName: 'berlin', + opcodes: { + 0x5c: { name: 'BEGINSUB', isAsync: false }, // EIP 2315 + 0x5d: { name: 'RETURNSUB', isAsync: false }, // EIP 2315 + 0x5e: { name: 'JUMPSUB', isAsync: false }, // EIP 2315 + }, + }, ] /** From 10f4186dabc92a24ed485cfbd6bc8b2ee607fe2f Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Fri, 14 Aug 2020 09:51:50 +0200 Subject: [PATCH 10/10] vm: removed invalid opcode checks for BEGINSUB, JUMPSUB, RETURNSUB (opcodes are now preselected on a HF basis in opcodes.ts) --- packages/vm/lib/evm/opFns.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/vm/lib/evm/opFns.ts b/packages/vm/lib/evm/opFns.ts index 481d627157..3f4345d3dd 100644 --- a/packages/vm/lib/evm/opFns.ts +++ b/packages/vm/lib/evm/opFns.ts @@ -547,17 +547,10 @@ export const handlers: { [k: string]: OpHandler } = { }, JUMPDEST: function (runState: RunState) {}, BEGINSUB: function (runState: RunState) { - if (!runState._common.gteHardfork('berlin')) { - trap(ERROR.INVALID_OPCODE) - } - trap(ERROR.INVALID_BEGINSUB + ' at ' + describeLocation(runState)) }, JUMPSUB: function (runState: RunState) { const dest = runState.stack.pop() - if (!runState._common.gteHardfork('berlin')) { - trap(ERROR.INVALID_OPCODE) - } if (dest.gt(runState.eei.getCodeSize())) { trap(ERROR.INVALID_JUMPSUB + ' at ' + describeLocation(runState)) @@ -573,10 +566,6 @@ export const handlers: { [k: string]: OpHandler } = { runState.programCounter = destNum + 1 }, RETURNSUB: function (runState: RunState) { - if (!runState._common.gteHardfork('berlin')) { - trap(ERROR.INVALID_OPCODE) - } - if (runState.returnStack.length < 1) { trap(ERROR.INVALID_RETURNSUB) }