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/interpreter.ts b/packages/vm/lib/evm/interpreter.ts index 2a8fa53656..26e2948abe 100644 --- a/packages/vm/lib/evm/interpreter.ts +++ b/packages/vm/lib/evm/interpreter.ts @@ -20,8 +20,10 @@ export interface RunState { memoryWordCount: BN highestMemCost: BN stack: Stack + returnStack: Stack code: Buffer validJumps: number[] + validJumpSubs: number[] _common: Common stateManager: StateManager eei: EEI @@ -36,6 +38,7 @@ export interface InterpreterStep { gasLeft: BN stateManager: StateManager stack: BN[] + returnStack: BN[] pc: number depth: number address: Buffer @@ -50,6 +53,11 @@ export interface InterpreterStep { codeAddress: Buffer } +interface JumpDests { + jumps: number[] + jumpSubs: number[] +} + /** * Parses and executes EVM bytecode. */ @@ -70,8 +78,10 @@ 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: [], + validJumpSubs: [], // TODO: Replace with EEI methods _common: this._vm._common, stateManager: this._state, @@ -82,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 @@ -167,6 +180,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, @@ -194,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 @@ -209,8 +224,12 @@ export default class Interpreter { 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 560c22b6cd..3f4345d3dd 100644 --- a/packages/vm/lib/evm/opFns.ts +++ b/packages/vm/lib/evm/opFns.ts @@ -546,6 +546,33 @@ 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_BEGINSUB + ' at ' + describeLocation(runState)) + }, + JUMPSUB: function (runState: RunState) { + const dest = runState.stack.pop() + + if (dest.gt(runState.eei.getCodeSize())) { + trap(ERROR.INVALID_JUMPSUB + ' at ' + describeLocation(runState)) + } + + const destNum = dest.toNumber() + + if (!jumpSubIsValid(runState, destNum)) { + trap(ERROR.INVALID_JUMPSUB + ' 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_RETURNSUB) + } + + const dest = runState.returnStack.pop() + runState.programCounter = dest.toNumber() + }, PUSH: function (runState: RunState) { const numToPush = runState.opCode - 0x5f const loaded = new BN( @@ -927,6 +954,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/evm/opcodes.ts b/packages/vm/lib/evm/opcodes.ts index 0c20f1dd00..455516c2c2 100644 --- a/packages/vm/lib/evm/opcodes.ts +++ b/packages/vm/lib/evm/opcodes.ts @@ -225,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 + }, + }, ] /** 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..0c4230cd67 100644 --- a/packages/vm/lib/exceptions.ts +++ b/packages/vm/lib/exceptions.ts @@ -12,6 +12,9 @@ export enum ERROR { STOP = 'stop', REFUND_EXHAUSTED = 'refund exhausted', VALUE_OVERFLOW = 'value overflow', + INVALID_BEGINSUB = 'invalid BEGINSUB', + INVALID_RETURNSUB = 'invalid RETURNSUB', + INVALID_JUMPSUB = 'invalid JUMPSUB', } export class VmError { 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) 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", 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..bb33e9ddb4 --- /dev/null +++ b/packages/vm/tests/api/berlin/eip-2315.js @@ -0,0 +1,198 @@ +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({ common: common }); + + 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: "60045e005c5d", + 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: "6800000000000000000c5e005c60115e5d5c5d", + 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 jumpsub (location out of code range)', async st => { + const test = { + code: "6801000000000000000c5e005c60115e5d5c5d", + totalSteps: 2, + steps: [ + { expectedPC: 0, expectedOpcode: "PUSH9" }, + { expectedPC: 10, expectedOpcode: "JUMPSUB" }, + ] + } + + result = await runTest(test, st) + 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 jumpsub (dest not BEGINSUB)', async st => { + const test = { + code: "60055e005c5d", + totalSteps: 2, + steps: [ + { expectedPC: 0, expectedOpcode: "PUSH1" }, + { expectedPC: 2, expectedOpcode: "JUMPSUB" } + ] + } + + 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: "600456005c5d", + 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() + }) + + // EIP test case 4 + t.test('should error when the return stack is too shallow', async st => { + const test = { + code: "5d5858", + totalSteps: 1, + steps: [ + { expectedPC: 0, expectedOpcode: "RETURNSUB" } + ] + } + + result = await runTest(test, st) + st.equal(true, result.exceptionError.error.includes('invalid RETURNSUB')) + 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: "6005565c5d5b60035e", + 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 + '5e', // JUMPSUB # 2 + '5c', // BEGINSUB # 3 + '60', '03', // PUSH1 3 # 4 + '5e', // 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: "5c", + totalSteps: 1, + steps: [ + { expectedPC: 0, expectedOpcode: "BEGINSUB" } + ] + } + + result = await runTest(test, st) + st.equal(true, result.exceptionError.error.includes('invalid BEGINSUB')) + 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) {