From 3673b4f1647ee15378a42135904f228328a6d4f7 Mon Sep 17 00:00:00 2001 From: Piotr Szlachciak Date: Sat, 1 Feb 2020 21:12:10 +0100 Subject: [PATCH] Add a Message interface (#47) * Add a Message interface * Fix linter errors --- packages/evm/src/evm/ExecutionContext.ts | 61 ++-- packages/evm/src/evm/Memory.ts | 2 +- packages/evm/src/evm/Message.ts | 18 + packages/evm/src/evm/State.ts | 8 - packages/evm/src/evm/errors.ts | 5 +- packages/evm/src/evm/executeCode.ts | 48 +-- packages/evm/src/evm/opcodes/index.ts | 146 ++++---- packages/evm/src/evm/opcodes/invalid.ts | 3 +- packages/evm/src/evm/opcodes/stack.ts | 8 +- packages/evm/src/evm/opcodes/storage.ts | 9 +- packages/evm/src/evm/parseBytecode.ts | 25 +- .../evm/test/evm/ExecutionContext.test.ts | 30 +- packages/evm/test/evm/State.test.ts | 4 +- packages/evm/test/evm/exception.test.ts | 13 +- .../evm/test/evm/helpers/executeAssembly.ts | 338 +++++++++--------- packages/evm/test/evm/helpers/expectations.ts | 14 +- packages/evm/test/evm/opcodes/storage.test.ts | 41 +-- 17 files changed, 364 insertions(+), 409 deletions(-) create mode 100644 packages/evm/src/evm/Message.ts diff --git a/packages/evm/src/evm/ExecutionContext.ts b/packages/evm/src/evm/ExecutionContext.ts index da611d2..2d47bdf 100644 --- a/packages/evm/src/evm/ExecutionContext.ts +++ b/packages/evm/src/evm/ExecutionContext.ts @@ -1,61 +1,54 @@ import { Stack } from './Stack' -import { Opcode } from './opcodes' import { OutOfGas } from './errors' import { Memory, GasAwareMemory } from './Memory' -import { ReadonlyState, State } from './State' -import { Address } from './Address' +import { State } from './State' import { Byte } from './Byte' - -export interface ExecutionParameters { - address: Address, - gasLimit: number, - state: ReadonlyState, -} +import { Message } from './Message' +import { Opcode } from './opcodes' +import { parseBytecode } from './parseBytecode' export class ExecutionContext { + code: Opcode[] stack = new Stack() - memory = new GasAwareMemory(new Memory(), this.useGas.bind(this)) + memory: GasAwareMemory + state: State returnValue?: Byte[] reverted = false programCounter = 0 - address: Address - gasLimit: number - state: State - private gasUsed = 0 - private refund = 0 - - constructor ( - public code: Opcode[], - params: ExecutionParameters, - ) { - this.address = params.address - this.gasLimit = params.gasLimit - this.state = params.state.clone() + private _gasUsed = 0 + private _gasRefund = 0 + + constructor (public message: Message) { + this.state = message.state.clone() + this.code = parseBytecode(message.code) + this.memory = new GasAwareMemory( + new Memory(), + this.useGas.bind(this), + ) } - getGasUsed () { - return this.gasUsed + get gasUsed () { + return this._gasUsed } useGas (gas: number) { - this.gasUsed += gas - if (this.gasUsed > this.gasLimit) { - this.gasUsed = this.gasLimit + this._gasUsed += gas + if (this._gasUsed > this.message.gasLimit) { + this._gasUsed = this.message.gasLimit throw new OutOfGas() } } useRemainingGas () { - this.gasUsed = this.gasLimit + this._gasUsed = this.message.gasLimit } - addRefund (gas: number) { - this.refund += gas + get gasRefund () { + return this._gasRefund } - applyRefund () { - const refund = Math.min(Math.floor(this.gasUsed / 2), this.refund) - this.gasUsed -= refund + refund (gas: number) { + this._gasRefund += gas } } diff --git a/packages/evm/src/evm/Memory.ts b/packages/evm/src/evm/Memory.ts index 355faac..cbab27f 100644 --- a/packages/evm/src/evm/Memory.ts +++ b/packages/evm/src/evm/Memory.ts @@ -9,7 +9,7 @@ export interface IMemory { export class GasAwareMemory implements IMemory { constructor ( - public memory: IMemory, + public memory: Memory, private useGas: (gas: number) => void, ) {} diff --git a/packages/evm/src/evm/Message.ts b/packages/evm/src/evm/Message.ts new file mode 100644 index 0000000..0db728a --- /dev/null +++ b/packages/evm/src/evm/Message.ts @@ -0,0 +1,18 @@ +import { Address } from './Address' +import { Bytes32 } from './Bytes32' +import { Byte } from './Byte' +import { State } from './State' + +export interface Message { + account: Address, + code: Byte[], + data: Byte[], + origin: Address, + sender: Address, + gasLimit: number, + gasPrice: Bytes32, + value: Bytes32, + enableStateModifications: boolean, + callDepth: number, + state: State, +} diff --git a/packages/evm/src/evm/State.ts b/packages/evm/src/evm/State.ts index ff3f6e4..3d5074f 100644 --- a/packages/evm/src/evm/State.ts +++ b/packages/evm/src/evm/State.ts @@ -2,14 +2,6 @@ import { Bytes32 } from './Bytes32' import { Address } from './Address' import { Byte } from './Byte' -export interface ReadonlyState { - getBalance (address: Address): Bytes32, - getNonce (address: Address): number, - getStorage (address: Address, location: Bytes32): Bytes32, - getCode (address: Address): readonly Byte[], - clone (): State, -} - export class State { private balances: Record = {} private nonces: Record = {} diff --git a/packages/evm/src/evm/errors.ts b/packages/evm/src/evm/errors.ts index dfba903..f17e2d4 100644 --- a/packages/evm/src/evm/errors.ts +++ b/packages/evm/src/evm/errors.ts @@ -1,4 +1,5 @@ import { Bytes32 } from './Bytes32' +import { Byte } from './Byte' export class VMError extends Error { constructor (message: string) { @@ -25,8 +26,8 @@ export class InvalidBytecode extends VMError { } export class InvalidOpcode extends VMError { - constructor (opcode: string) { - super('Invalid opcode 0x' + opcode) + constructor (opcode: Byte) { + super('Invalid opcode 0x' + opcode.toString(16).padStart(2, '0')) } } diff --git a/packages/evm/src/evm/executeCode.ts b/packages/evm/src/evm/executeCode.ts index 6bdec0c..4f775a3 100644 --- a/packages/evm/src/evm/executeCode.ts +++ b/packages/evm/src/evm/executeCode.ts @@ -1,60 +1,50 @@ -import { Opcode } from './opcodes' -import { ExecutionContext, ExecutionParameters } from './ExecutionContext' +import { Byte } from './Byte' +import { ExecutionContext } from './ExecutionContext' +import { Memory } from './Memory' +import { Message } from './Message' +import { opSTOP } from './opcodes/control' import { Stack } from './Stack' +import { State } from './State' import { VMError } from './errors' -import { IMemory } from './Memory' -import { opSTOP } from './opcodes/control' -import { ReadonlyState } from './State' -import { Byte } from './Byte' export interface ExecutionResult { stack: Stack, - memory: IMemory, - state: ReadonlyState, + memory: Memory, + state: State, gasUsed: number, + gasRefund: number, programCounter: number, reverted: boolean, returnValue?: Byte[], error?: VMError, } -export function executeCode (code: Opcode[], params: ExecutionParameters): ExecutionResult { - const ctx = new ExecutionContext(code, params) - +export function executeCode (message: Message): ExecutionResult { + const ctx = new ExecutionContext(message) while (ctx.returnValue === undefined) { - const opCode = code[ctx.programCounter] || opSTOP + const opcode = ctx.code[ctx.programCounter] || opSTOP ctx.programCounter++ try { - opCode(ctx) + opcode(ctx) } catch (e) { if (e instanceof VMError) { ctx.useRemainingGas() - return toResult(ctx, params.state, e) + return toResult(ctx, e) } else { - throw e + throw e // this should never happen } } } - - if (ctx.reverted) { - // TODO: should there be a refund here? - return toResult(ctx, params.state) - } - ctx.applyRefund() return toResult(ctx) } -function toResult ( - ctx: ExecutionContext, - state?: ReadonlyState, - error?: VMError, -): ExecutionResult { +function toResult (ctx: ExecutionContext, error?: VMError): ExecutionResult { return { stack: ctx.stack, - // This prevents us from retaining a reference to ctx memory: ctx.memory.memory, - state: state ?? ctx.state, - gasUsed: ctx.getGasUsed(), + state: (ctx.reverted || error) ? ctx.message.state : ctx.state, + gasUsed: ctx.gasUsed, + gasRefund: ctx.gasRefund, reverted: ctx.reverted, programCounter: ctx.programCounter, returnValue: ctx.returnValue, diff --git a/packages/evm/src/evm/opcodes/index.ts b/packages/evm/src/evm/opcodes/index.ts index 3b9edb5..95dce86 100644 --- a/packages/evm/src/evm/opcodes/index.ts +++ b/packages/evm/src/evm/opcodes/index.ts @@ -38,87 +38,87 @@ import { invalidOpcode } from './invalid' import { makeOpDUP, makeOpSWAP, opPOP } from './stack' import { opMSIZE, opMLOAD, opMSTORE, opMSTORE8 } from './memory' import { opSSTORE, opSLOAD } from './storage' +import { Byte } from '../Byte' export { opUnreachable } from './invalid' export { makeOpPUSH } from './stack' export { Opcode } from './Opcode' export { GasCost, GasRefund } from './gasCosts' -export function getOpcode (hex: string) { +export function getOpcode (hex: Byte) { return OP_CODES[hex] ?? invalidOpcode(hex) } -/* eslint-disable quote-props */ -const OP_CODES: Record = { - '00': opSTOP, - '01': opADD, - '02': opMUL, - '03': opSUB, - '04': opDIV, - '05': opSDIV, - '06': opMOD, - '07': opSMOD, - '08': opADDMOD, - '09': opMULMOD, - '0a': opEXP, - '0b': opSIGNEXTEND, - '10': opLT, - '11': opGT, - '12': opSLT, - '13': opSGT, - '14': opEQ, - '15': opISZERO, - '16': opAND, - '17': opOR, - '18': opXOR, - '19': opNOT, - '1a': opBYTE, - '1b': opSHL, - '1c': opSHR, - '1d': opSAR, - '50': opPOP, - '51': opMLOAD, - '52': opMSTORE, - '53': opMSTORE8, - '54': opSLOAD, - '55': opSSTORE, - '56': opJUMP, - '57': opJUMPI, - '59': opMSIZE, - '5b': opJUMPDEST, +const OP_CODES: Record = { + 0x00: opSTOP, + 0x01: opADD, + 0x02: opMUL, + 0x03: opSUB, + 0x04: opDIV, + 0x05: opSDIV, + 0x06: opMOD, + 0x07: opSMOD, + 0x08: opADDMOD, + 0x09: opMULMOD, + 0x0a: opEXP, + 0x0b: opSIGNEXTEND, + 0x10: opLT, + 0x11: opGT, + 0x12: opSLT, + 0x13: opSGT, + 0x14: opEQ, + 0x15: opISZERO, + 0x16: opAND, + 0x17: opOR, + 0x18: opXOR, + 0x19: opNOT, + 0x1a: opBYTE, + 0x1b: opSHL, + 0x1c: opSHR, + 0x1d: opSAR, + 0x50: opPOP, + 0x51: opMLOAD, + 0x52: opMSTORE, + 0x53: opMSTORE8, + 0x54: opSLOAD, + 0x55: opSSTORE, + 0x56: opJUMP, + 0x57: opJUMPI, + 0x59: opMSIZE, + 0x5b: opJUMPDEST, // 60 - 7f PUSH - handled differently - '80': makeOpDUP(1), - '81': makeOpDUP(2), - '82': makeOpDUP(3), - '83': makeOpDUP(4), - '84': makeOpDUP(5), - '85': makeOpDUP(6), - '86': makeOpDUP(7), - '87': makeOpDUP(8), - '88': makeOpDUP(9), - '89': makeOpDUP(10), - '8a': makeOpDUP(11), - '8b': makeOpDUP(12), - '8c': makeOpDUP(13), - '8d': makeOpDUP(14), - '8e': makeOpDUP(15), - '8f': makeOpDUP(16), - '90': makeOpSWAP(1), - '91': makeOpSWAP(2), - '92': makeOpSWAP(3), - '93': makeOpSWAP(4), - '94': makeOpSWAP(5), - '95': makeOpSWAP(6), - '96': makeOpSWAP(7), - '97': makeOpSWAP(8), - '98': makeOpSWAP(9), - '99': makeOpSWAP(10), - '9a': makeOpSWAP(11), - '9b': makeOpSWAP(12), - '9c': makeOpSWAP(13), - '9d': makeOpSWAP(14), - '9e': makeOpSWAP(15), - '9f': makeOpSWAP(16), - 'f3': opRETURN, - 'fd': opREVERT, + 0x80: makeOpDUP(1), + 0x81: makeOpDUP(2), + 0x82: makeOpDUP(3), + 0x83: makeOpDUP(4), + 0x84: makeOpDUP(5), + 0x85: makeOpDUP(6), + 0x86: makeOpDUP(7), + 0x87: makeOpDUP(8), + 0x88: makeOpDUP(9), + 0x89: makeOpDUP(10), + 0x8a: makeOpDUP(11), + 0x8b: makeOpDUP(12), + 0x8c: makeOpDUP(13), + 0x8d: makeOpDUP(14), + 0x8e: makeOpDUP(15), + 0x8f: makeOpDUP(16), + 0x90: makeOpSWAP(1), + 0x91: makeOpSWAP(2), + 0x92: makeOpSWAP(3), + 0x93: makeOpSWAP(4), + 0x94: makeOpSWAP(5), + 0x95: makeOpSWAP(6), + 0x96: makeOpSWAP(7), + 0x97: makeOpSWAP(8), + 0x98: makeOpSWAP(9), + 0x99: makeOpSWAP(10), + 0x9a: makeOpSWAP(11), + 0x9b: makeOpSWAP(12), + 0x9c: makeOpSWAP(13), + 0x9d: makeOpSWAP(14), + 0x9e: makeOpSWAP(15), + 0x9f: makeOpSWAP(16), + 0xf3: opRETURN, + 0xfd: opREVERT, } diff --git a/packages/evm/src/evm/opcodes/invalid.ts b/packages/evm/src/evm/opcodes/invalid.ts index cb40f21..7cdb75c 100644 --- a/packages/evm/src/evm/opcodes/invalid.ts +++ b/packages/evm/src/evm/opcodes/invalid.ts @@ -1,7 +1,8 @@ import { ExecutionContext } from '../ExecutionContext' import { InvalidOpcode, UnreachableInstruction } from '../errors' +import { Byte } from '../Byte' -export function invalidOpcode (opcode: string) { +export function invalidOpcode (opcode: Byte) { return function (ctx: ExecutionContext) { throw new InvalidOpcode(opcode) } diff --git a/packages/evm/src/evm/opcodes/stack.ts b/packages/evm/src/evm/opcodes/stack.ts index 68a19b4..c6a9b96 100644 --- a/packages/evm/src/evm/opcodes/stack.ts +++ b/packages/evm/src/evm/opcodes/stack.ts @@ -1,13 +1,13 @@ import { Bytes32 } from '../Bytes32' import { ExecutionContext } from '../ExecutionContext' import { GasCost } from './gasCosts' +import { Byte } from '../Byte' -export function makeOpPUSH (bytes: string) { - const word = Bytes32.fromHex(bytes) - const count = bytes.length / 2 +export function makeOpPUSH (bytes: Byte[]) { + const word = Bytes32.fromBytes(bytes) return function opPUSH (ctx: ExecutionContext) { ctx.useGas(GasCost.VERYLOW) - ctx.programCounter += count + ctx.programCounter += bytes.length ctx.stack.push(word) } } diff --git a/packages/evm/src/evm/opcodes/storage.ts b/packages/evm/src/evm/opcodes/storage.ts index ee04aa5..ed80ba9 100644 --- a/packages/evm/src/evm/opcodes/storage.ts +++ b/packages/evm/src/evm/opcodes/storage.ts @@ -6,18 +6,19 @@ export function opSSTORE (ctx: ExecutionContext) { const location = ctx.stack.pop() const value = ctx.stack.pop() - const isZero = ctx.state.getStorage(ctx.address, location).equals(Bytes32.ZERO) + const stored = ctx.state.getStorage(ctx.message.account, location) + const isZero = stored.equals(Bytes32.ZERO) ctx.useGas(isZero ? GasCost.SSET : GasCost.SRESET) if (!isZero && value.equals(Bytes32.ZERO)) { - ctx.addRefund(GasRefund.SCLEAR) + ctx.refund(GasRefund.SCLEAR) } - ctx.state.setStorage(ctx.address, location, value) + ctx.state.setStorage(ctx.message.account, location, value) } export function opSLOAD (ctx: ExecutionContext) { ctx.useGas(GasCost.SLOAD) const location = ctx.stack.pop() - const value = ctx.state.getStorage(ctx.address, location) + const value = ctx.state.getStorage(ctx.message.account, location) ctx.stack.push(value) } diff --git a/packages/evm/src/evm/parseBytecode.ts b/packages/evm/src/evm/parseBytecode.ts index de1166a..d827deb 100644 --- a/packages/evm/src/evm/parseBytecode.ts +++ b/packages/evm/src/evm/parseBytecode.ts @@ -1,16 +1,16 @@ import { InvalidBytecode } from './errors' import { Opcode, getOpcode, makeOpPUSH, opUnreachable } from './opcodes' +import { Byte } from './Byte' -export function parseBytecode (bytecode: string) { +export function parseBytecode (bytes: Byte[]) { const result: Opcode[] = [] - const bytes = parseBytes(bytecode) for (let i = 0; i < bytes.length; i++) { const pushSize = getPushSize(bytes[i]) if (pushSize !== 0) { if (i + pushSize >= bytes.length) { throw new InvalidBytecode() } - const toPush = bytes.slice(i + 1, i + pushSize + 1).join('') + const toPush = bytes.slice(i + 1, i + pushSize + 1) result.push(makeOpPUSH(toPush)) for (let j = 0; j < pushSize; j++) { result.push(opUnreachable) @@ -23,22 +23,9 @@ export function parseBytecode (bytecode: string) { return result } -function getPushSize (byte: string) { - const num = parseInt(byte, 16) - if (num >= 0x60 && num <= 0x7f) { - return num + 1 - 0x60 +function getPushSize (byte: Byte) { + if (byte >= 0x60 && byte <= 0x7f) { + return byte + 1 - 0x60 } return 0 } - -function parseBytes (bytecode: string) { - if (!isHexBytes(bytecode)) { - throw new InvalidBytecode() - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return bytecode.match(/../g)!.map(x => x) -} - -function isHexBytes (value: string) { - return /^([\da-f]{2})+/.test(value) -} diff --git a/packages/evm/test/evm/ExecutionContext.test.ts b/packages/evm/test/evm/ExecutionContext.test.ts index 7ab33e7..c4d6cb2 100644 --- a/packages/evm/test/evm/ExecutionContext.test.ts +++ b/packages/evm/test/evm/ExecutionContext.test.ts @@ -1,15 +1,13 @@ import { expect } from 'chai' import { ExecutionContext } from '../../src/evm/ExecutionContext' import { OutOfGas } from '../../src/evm/errors' -import { State } from '../../src/evm/State' -import { Address } from '../../src/evm/Address' +import { DEFAULT_MESSAGE } from './helpers' describe('ExecutionContext', () => { function makeContext (gasLimit: number) { - return new ExecutionContext([], { - address: '0x1234' as Address, + return new ExecutionContext({ + ...DEFAULT_MESSAGE, gasLimit, - state: new State(), }) } @@ -17,7 +15,7 @@ describe('ExecutionContext', () => { const ctx = makeContext(Infinity) ctx.useGas(10) ctx.useGas(15) - expect(ctx.getGasUsed()).to.equal(25) + expect(ctx.gasUsed).to.equal(25) }) it('throws when too much gas is used', () => { @@ -30,23 +28,15 @@ describe('ExecutionContext', () => { it('can use all gas', () => { const ctx = makeContext(300) ctx.useRemainingGas() - expect(ctx.getGasUsed()).to.equal(300) + expect(ctx.gasUsed).to.equal(300) }) - it('can apply refund', () => { + it('can track refund', () => { const ctx = makeContext(Infinity) ctx.useGas(100) - ctx.addRefund(10) - ctx.addRefund(15) - ctx.applyRefund() - expect(ctx.getGasUsed()).to.equal(75) - }) - - it('refund cannot exceed 50% gas used rounded down', () => { - const ctx = makeContext(Infinity) - ctx.useGas(101) - ctx.addRefund(51) - ctx.applyRefund() - expect(ctx.getGasUsed()).to.equal(51) + ctx.refund(10) + ctx.refund(15) + expect(ctx.gasUsed).to.equal(100) + expect(ctx.gasRefund).to.equal(25) }) }) diff --git a/packages/evm/test/evm/State.test.ts b/packages/evm/test/evm/State.test.ts index 0b28cb2..1e94ee3 100644 --- a/packages/evm/test/evm/State.test.ts +++ b/packages/evm/test/evm/State.test.ts @@ -1,11 +1,11 @@ import { expect } from 'chai' import { State } from '../../src/evm/State' -import { Address } from '../../src/evm/Address' import { Bytes32 } from '../../src/evm/Bytes32' import { Byte } from '../../src/evm/Byte' +import { ADDRESS_ZERO } from './helpers' describe('State', () => { - const address = '0x1234' as Address + const address = ADDRESS_ZERO it('getBalance returns ZERO by default', () => { const state = new State() diff --git a/packages/evm/test/evm/exception.test.ts b/packages/evm/test/evm/exception.test.ts index 25edbbf..744e82c 100644 --- a/packages/evm/test/evm/exception.test.ts +++ b/packages/evm/test/evm/exception.test.ts @@ -1,8 +1,7 @@ import { expect } from 'chai' -import { executeAssembly } from './helpers' +import { executeAssembly, ADDRESS_ZERO } from './helpers' import { InvalidOpcode } from '../../src/evm/errors' import { Bytes32 } from '../../src/evm/Bytes32' -import { Address } from '../../src/evm/Address' import { State } from '../../src/evm/State' describe('When an exception occurs', () => { @@ -14,9 +13,9 @@ describe('When an exception occurs', () => { }) it('state changes are reverted', () => { - const address = '0x1234' as Address + const account = ADDRESS_ZERO const state = new State() - state.setStorage(address, Bytes32.ZERO, Bytes32.ONE) + state.setStorage(account, Bytes32.ZERO, Bytes32.ONE) const assembly = ` PUSH1 02 @@ -27,11 +26,11 @@ describe('When an exception occurs', () => { SSTORE INVALID ` - const result = executeAssembly(assembly, { address, state }) + const result = executeAssembly(assembly, { account, state }) expect(result.error).to.be.instanceOf(InvalidOpcode) - const storageAt0 = result.state.getStorage(address, Bytes32.ZERO) - const storageAt1 = result.state.getStorage(address, Bytes32.ONE) + const storageAt0 = result.state.getStorage(account, Bytes32.ZERO) + const storageAt1 = result.state.getStorage(account, Bytes32.ONE) expect(storageAt0.equals(Bytes32.ONE)).to.equal(true) expect(storageAt1.equals(Bytes32.ZERO)).to.equal(true) }) diff --git a/packages/evm/test/evm/helpers/executeAssembly.ts b/packages/evm/test/evm/helpers/executeAssembly.ts index 2409f9b..16c3af9 100644 --- a/packages/evm/test/evm/helpers/executeAssembly.ts +++ b/packages/evm/test/evm/helpers/executeAssembly.ts @@ -1,175 +1,189 @@ -import { parseBytecode } from '../../../src/evm/parseBytecode' import { executeCode } from '../../../src/evm/executeCode' -import { ExecutionParameters } from '../../../src/evm/ExecutionContext' import { State } from '../../../src/evm/State' import { Address } from '../../../src/evm/Address' +import { Byte } from '../../../src/evm/Byte' +import { Message } from '../../../src/evm/Message' +import { Bytes32 } from '../../../src/evm/Bytes32' -const DEFAULT_EXECUTION_PARAMS: ExecutionParameters = { - address: '0x1234' as Address, +export const ADDRESS_ZERO = '0'.repeat(40) as Address + +export const DEFAULT_MESSAGE: Message = { + account: ADDRESS_ZERO, + sender: ADDRESS_ZERO, + origin: ADDRESS_ZERO, gasLimit: 1_000_000_000, + gasPrice: Bytes32.ZERO, state: new State(), + callDepth: 0, + data: [], + enableStateModifications: true, + value: Bytes32.ZERO, + code: [], } -export function executeAssembly (assembly: string, params: Partial = {}) { - const bytecode = assemblyToBytecode(assembly) - const code = parseBytecode(bytecode) - return executeCode(code, { ...DEFAULT_EXECUTION_PARAMS, ...params }) -} - -function assemblyToBytecode (code: string) { - return code - .trim() - .split(/\s+/) - .map(instructionToBytecode) - .join('') +export function executeAssembly (assembly: string, params?: Partial) { + const code = assemblyToBytecode(assembly) + return executeCode({ ...DEFAULT_MESSAGE, ...params, code }) } -function instructionToBytecode (instruction: string) { - return (OPCODES[instruction] ?? instruction).toLowerCase() +function assemblyToBytecode (code: string): Byte[] { + const instructions = code.trim().split(/\s+/) + const result: Byte[] = [] + for (const instruction of instructions) { + const opcode = OPCODES[instruction] as Byte + if (opcode !== undefined) { + result.push(opcode) + } else { + const bytes = instruction.match(/../g)! + .map(x => parseInt(x, 16) as Byte) + result.push(...bytes) + } + } + return result } -const OPCODES: Record = { - STOP: '00', - ADD: '01', - MUL: '02', - SUB: '03', - DIV: '04', - SDIV: '05', - MOD: '06', - SMOD: '07', - ADDMOD: '08', - MULMOD: '09', - EXP: '0a', - SIGNEXTEND: '0b', - LT: '10', - GT: '11', - SLT: '12', - SGT: '13', - EQ: '14', - ISZERO: '15', - AND: '16', - OR: '17', - XOR: '18', - NOT: '19', - BYTE: '1a', - SHL: '1b', - SHR: '1c', - SAR: '1d', - SHA3: '20', - ADDRESS: '30', - BALANCE: '31', - ORIGIN: '32', - CALLER: '33', - CALLVALUE: '34', - CALLDATALOAD: '35', - CALLDATASIZE: '36', - CALLDATACOPY: '37', - CODESIZE: '38', - CODECOPY: '39', - GASPRICE: '3a', - EXTCODESIZE: '3b', - EXTCODECOPY: '3c', - RETURNDATASIZE: '3d', - RETURNDATACOPY: '3e', - EXTCODEHASH: '3f', - BLOCKHASH: '40', - COINBASE: '41', - TIMESTAMP: '42', - NUMBER: '43', - DIFFICULTY: '44', - GASLIMIT: '45', - POP: '50', - MLOAD: '51', - MSTORE: '52', - MSTORE8: '53', - SLOAD: '54', - SSTORE: '55', - JUMP: '56', - JUMPI: '57', - PC: '58', - MSIZE: '59', - GAS: '5a', - JUMPDEST: '5b', - PUSH1: '60', - PUSH2: '61', - PUSH3: '62', - PUSH4: '63', - PUSH5: '64', - PUSH6: '65', - PUSH7: '66', - PUSH8: '67', - PUSH9: '68', - PUSH10: '69', - PUSH11: '6a', - PUSH12: '6b', - PUSH13: '6c', - PUSH14: '6d', - PUSH15: '6e', - PUSH16: '6f', - PUSH17: '70', - PUSH18: '71', - PUSH19: '72', - PUSH20: '73', - PUSH21: '74', - PUSH22: '75', - PUSH23: '76', - PUSH24: '77', - PUSH25: '78', - PUSH26: '79', - PUSH27: '7a', - PUSH28: '7b', - PUSH29: '7c', - PUSH30: '7d', - PUSH31: '7e', - PUSH32: '7f', - DUP1: '80', - DUP2: '81', - DUP3: '82', - DUP4: '83', - DUP5: '84', - DUP6: '85', - DUP7: '86', - DUP8: '87', - DUP9: '88', - DUP10: '89', - DUP11: '8a', - DUP12: '8b', - DUP13: '8c', - DUP14: '8d', - DUP15: '8e', - DUP16: '8f', - SWAP1: '90', - SWAP2: '91', - SWAP3: '92', - SWAP4: '93', - SWAP5: '94', - SWAP6: '95', - SWAP7: '96', - SWAP8: '97', - SWAP9: '98', - SWAP10: '99', - SWAP11: '9a', - SWAP12: '9b', - SWAP13: '9c', - SWAP14: '9d', - SWAP15: '9e', - SWAP16: '9f', - LOG0: 'a0', - LOG1: 'a1', - LOG2: 'a2', - LOG3: 'a3', - LOG4: 'a4', - PUSH: 'b0', - DUP: 'b1', - SWAP: 'b2', - CREATE: 'f0', - CALL: 'f1', - CALLCODE: 'f2', - RETURN: 'f3', - DELEGATECALL: 'f4', - CREATE2: 'f5', - STATICCALL: 'fa', - REVERT: 'fd', - INVALID: 'fe', - SELFDESTRUCT: 'ff', +const OPCODES: Record = { + STOP: 0x00, + ADD: 0x01, + MUL: 0x02, + SUB: 0x03, + DIV: 0x04, + SDIV: 0x05, + MOD: 0x06, + SMOD: 0x07, + ADDMOD: 0x08, + MULMOD: 0x09, + EXP: 0x0a, + SIGNEXTEND: 0x0b, + LT: 0x10, + GT: 0x11, + SLT: 0x12, + SGT: 0x13, + EQ: 0x14, + ISZERO: 0x15, + AND: 0x16, + OR: 0x17, + XOR: 0x18, + NOT: 0x19, + BYTE: 0x1a, + SHL: 0x1b, + SHR: 0x1c, + SAR: 0x1d, + SHA3: 0x20, + ADDRESS: 0x30, + BALANCE: 0x31, + ORIGIN: 0x32, + CALLER: 0x33, + CALLVALUE: 0x34, + CALLDATALOAD: 0x35, + CALLDATASIZE: 0x36, + CALLDATACOPY: 0x37, + CODESIZE: 0x38, + CODECOPY: 0x39, + GASPRICE: 0x3a, + EXTCODESIZE: 0x3b, + EXTCODECOPY: 0x3c, + RETURNDATASIZE: 0x3d, + RETURNDATACOPY: 0x3e, + EXTCODEHASH: 0x3f, + BLOCKHASH: 0x40, + COINBASE: 0x41, + TIMESTAMP: 0x42, + NUMBER: 0x43, + DIFFICULTY: 0x44, + GASLIMIT: 0x45, + POP: 0x50, + MLOAD: 0x51, + MSTORE: 0x52, + MSTORE8: 0x53, + SLOAD: 0x54, + SSTORE: 0x55, + JUMP: 0x56, + JUMPI: 0x57, + PC: 0x58, + MSIZE: 0x59, + GAS: 0x5a, + JUMPDEST: 0x5b, + PUSH1: 0x60, + PUSH2: 0x61, + PUSH3: 0x62, + PUSH4: 0x63, + PUSH5: 0x64, + PUSH6: 0x65, + PUSH7: 0x66, + PUSH8: 0x67, + PUSH9: 0x68, + PUSH10: 0x69, + PUSH11: 0x6a, + PUSH12: 0x6b, + PUSH13: 0x6c, + PUSH14: 0x6d, + PUSH15: 0x6e, + PUSH16: 0x6f, + PUSH17: 0x70, + PUSH18: 0x71, + PUSH19: 0x72, + PUSH20: 0x73, + PUSH21: 0x74, + PUSH22: 0x75, + PUSH23: 0x76, + PUSH24: 0x77, + PUSH25: 0x78, + PUSH26: 0x79, + PUSH27: 0x7a, + PUSH28: 0x7b, + PUSH29: 0x7c, + PUSH30: 0x7d, + PUSH31: 0x7e, + PUSH32: 0x7f, + DUP1: 0x80, + DUP2: 0x81, + DUP3: 0x82, + DUP4: 0x83, + DUP5: 0x84, + DUP6: 0x85, + DUP7: 0x86, + DUP8: 0x87, + DUP9: 0x88, + DUP10: 0x89, + DUP11: 0x8a, + DUP12: 0x8b, + DUP13: 0x8c, + DUP14: 0x8d, + DUP15: 0x8e, + DUP16: 0x8f, + SWAP1: 0x90, + SWAP2: 0x91, + SWAP3: 0x92, + SWAP4: 0x93, + SWAP5: 0x94, + SWAP6: 0x95, + SWAP7: 0x96, + SWAP8: 0x97, + SWAP9: 0x98, + SWAP10: 0x99, + SWAP11: 0x9a, + SWAP12: 0x9b, + SWAP13: 0x9c, + SWAP14: 0x9d, + SWAP15: 0x9e, + SWAP16: 0x9f, + LOG0: 0xa0, + LOG1: 0xa1, + LOG2: 0xa2, + LOG3: 0xa3, + LOG4: 0xa4, + PUSH: 0xb0, + DUP: 0xb1, + SWAP: 0xb2, + CREATE: 0xf0, + CALL: 0xf1, + CALLCODE: 0xf2, + RETURN: 0xf3, + DELEGATECALL: 0xf4, + CREATE2: 0xf5, + STATICCALL: 0xfa, + REVERT: 0xfd, + INVALID: 0xfe, + SELFDESTRUCT: 0xff, } diff --git a/packages/evm/test/evm/helpers/expectations.ts b/packages/evm/test/evm/helpers/expectations.ts index 02f1853..638afb8 100644 --- a/packages/evm/test/evm/helpers/expectations.ts +++ b/packages/evm/test/evm/helpers/expectations.ts @@ -1,9 +1,8 @@ import { expect } from 'chai' import { StackUnderflow } from '../../../src/evm/errors' -import { executeAssembly } from './executeAssembly' +import { executeAssembly, ADDRESS_ZERO } from './executeAssembly' import { Int256 } from './Int256' import { Bytes32 } from '../../../src/evm/Bytes32' -import { Address } from '../../../src/evm/Address' import { Byte } from '../../../src/evm/Byte' export function expectUnderflow (opcode: string, minimumDepth: number) { @@ -29,6 +28,11 @@ export function expectGas (assembly: string, gasUsed: number) { expect(result.gasUsed).to.equal(gasUsed) } +export function expectRefund (assembly: string, refund: number) { + const result = executeAssembly(assembly) + expect(result.gasRefund).to.equal(refund) +} + export function expectError (assembly: string, error: unknown) { const result = executeAssembly(assembly) expect(result.error).to.be.instanceOf(error) @@ -47,12 +51,12 @@ export function expectRevert (assembly: string, value: Byte[]) { } export function expectStorage (assembly: string, values: Record) { - const address = '0x1234' as Address - const result = executeAssembly(assembly, { address }) + const account = ADDRESS_ZERO + const result = executeAssembly(assembly, { account }) const resultingStorage: Record = {} for (const key in values) { const location = Bytes32.fromHex(key) - resultingStorage[key] = result.state.getStorage(address, location).toHex() + resultingStorage[key] = result.state.getStorage(account, location).toHex() } expect(resultingStorage).to.deep.equal(values) } diff --git a/packages/evm/test/evm/opcodes/storage.test.ts b/packages/evm/test/evm/opcodes/storage.test.ts index 029c86b..079a07b 100644 --- a/packages/evm/test/evm/opcodes/storage.test.ts +++ b/packages/evm/test/evm/opcodes/storage.test.ts @@ -1,14 +1,12 @@ -import { expect } from 'chai' import { GasCost, GasRefund } from '../../../src/evm/opcodes' import { expectGas, expectUnderflow, expectStorage, Int256, - executeAssembly, expectStack, + expectRefund, } from '../helpers' -import { OutOfGas } from '../../../src/evm/errors' describe('Storage opcodes', () => { describe('SSTORE', () => { @@ -49,50 +47,17 @@ describe('Storage opcodes', () => { describe('refund', () => { it(`gets ${GasRefund.SCLEAR} refund when changing non-zero`, () => { - // NOTE: we do a "redundant" SSTORE here to make total gas larger than refund * 2 const assembly = ` PUSH1 01 PUSH1 00 SSTORE - PUSH1 01 - PUSH1 01 - SSTORE PUSH1 00 PUSH1 00 SSTORE ` - const gas = GasCost.VERYLOW * 6 + GasCost.SSET * 2 + GasCost.SRESET - GasRefund.SCLEAR + const gas = GasCost.VERYLOW * 4 + GasCost.SSET + GasCost.SRESET expectGas(assembly, gas) - }) - - it('the refund is limited to half of the gas used', () => { - const assembly = ` - PUSH1 01 - PUSH1 00 - SSTORE - PUSH1 00 - PUSH1 00 - SSTORE - ` - const gasUsed = GasCost.VERYLOW * 4 + GasCost.SSET + GasCost.SRESET - const refund = Math.floor(gasUsed / 2) - expectGas(assembly, gasUsed - refund) - }) - - it('ignores refund in case of out of gas', () => { - const assembly = ` - PUSH1 01 - PUSH1 00 - SSTORE - PUSH1 00 - PUSH1 00 - SSTORE - ` - const gasUsed = GasCost.VERYLOW * 4 + GasCost.SSET + GasCost.SRESET - const refund = Math.floor(gasUsed / 2) - - const result = executeAssembly(assembly, { gasLimit: gasUsed - refund }) - expect(result.error).to.be.instanceOf(OutOfGas) + expectRefund(assembly, GasRefund.SCLEAR) }) }) })