From f4b3f0b5b5576776287b167694293d5530eac5f5 Mon Sep 17 00:00:00 2001 From: Piotr Szlachciak Date: Sun, 9 Feb 2020 13:32:13 +0100 Subject: [PATCH 1/8] Implement CODESIZE and CODECOPY --- packages/evm/src/Memory.ts | 6 +-- packages/evm/src/opcodes/code.ts | 25 +++++++++ packages/evm/src/opcodes/gasCosts.ts | 1 + packages/evm/src/opcodes/index.ts | 3 ++ packages/evm/test/helpers/executeAssembly.ts | 2 +- packages/evm/test/opcodes/code.test.ts | 57 ++++++++++++++++++++ 6 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 packages/evm/src/opcodes/code.ts create mode 100644 packages/evm/test/opcodes/code.test.ts diff --git a/packages/evm/src/Memory.ts b/packages/evm/src/Memory.ts index f02d545..d2c1a71 100644 --- a/packages/evm/src/Memory.ts +++ b/packages/evm/src/Memory.ts @@ -15,7 +15,7 @@ export class Memory { } getBytes (offset: number, length: number) { - this.onMemoryAccess(offset, length) + this.useGasForAccess(offset, length) if (length === 0) { return [] } @@ -24,7 +24,7 @@ export class Memory { } setBytes (offset: number, bytes: Byte[]) { - this.onMemoryAccess(offset, bytes.length) + this.useGasForAccess(offset, bytes.length) if (bytes.length === 0) { return } @@ -34,7 +34,7 @@ export class Memory { } } - private onMemoryAccess (offset: number, length: number) { + useGasForAccess (offset: number, length: number) { if (length === 0) { return } diff --git a/packages/evm/src/opcodes/code.ts b/packages/evm/src/opcodes/code.ts new file mode 100644 index 0000000..1423de3 --- /dev/null +++ b/packages/evm/src/opcodes/code.ts @@ -0,0 +1,25 @@ +import { ExecutionContext } from '../ExecutionContext' +import { GasCost } from './gasCosts' +import { Bytes32 } from '../Bytes32' +import { Byte } from '../Byte' + +export function opCODESIZE (ctx: ExecutionContext) { + ctx.useGas(GasCost.BASE) + ctx.stack.push(Bytes32.fromNumber(ctx.message.code.length)) +} + +export function opCODECOPY (ctx: ExecutionContext) { + const memoryOffset = ctx.stack.pop().toUnsignedNumber() + const codeOffset = ctx.stack.pop().toUnsignedNumber() + const memorySize = ctx.stack.pop().toUnsignedNumber() + + ctx.useGas(GasCost.VERYLOW + GasCost.COPY * Math.ceil(memorySize / 32)) + // we subtract the gas early in case of OutOfGas + ctx.memory.useGasForAccess(memoryOffset, memorySize) + + const code = ctx.message.code.slice(codeOffset, codeOffset + memorySize) + while (code.length < memorySize) { + code.push(0x00 as Byte) + } + ctx.memory.setBytes(memoryOffset, code) +} diff --git a/packages/evm/src/opcodes/gasCosts.ts b/packages/evm/src/opcodes/gasCosts.ts index 6c7d06b..edf61fb 100644 --- a/packages/evm/src/opcodes/gasCosts.ts +++ b/packages/evm/src/opcodes/gasCosts.ts @@ -2,6 +2,7 @@ export const GasCost = { ZERO: 0, BASE: 2, VERYLOW: 3, + COPY: 3, LOW: 5, MID: 8, HIGH: 10, diff --git a/packages/evm/src/opcodes/index.ts b/packages/evm/src/opcodes/index.ts index 95dce86..d6d3c06 100644 --- a/packages/evm/src/opcodes/index.ts +++ b/packages/evm/src/opcodes/index.ts @@ -39,6 +39,7 @@ import { makeOpDUP, makeOpSWAP, opPOP } from './stack' import { opMSIZE, opMLOAD, opMSTORE, opMSTORE8 } from './memory' import { opSSTORE, opSLOAD } from './storage' import { Byte } from '../Byte' +import { opCODESIZE, opCODECOPY } from './code' export { opUnreachable } from './invalid' export { makeOpPUSH } from './stack' @@ -76,6 +77,8 @@ const OP_CODES: Record = { 0x1b: opSHL, 0x1c: opSHR, 0x1d: opSAR, + 0x38: opCODESIZE, + 0x39: opCODECOPY, 0x50: opPOP, 0x51: opMLOAD, 0x52: opMSTORE, diff --git a/packages/evm/test/helpers/executeAssembly.ts b/packages/evm/test/helpers/executeAssembly.ts index 2b3108a..fc822a9 100644 --- a/packages/evm/test/helpers/executeAssembly.ts +++ b/packages/evm/test/helpers/executeAssembly.ts @@ -29,7 +29,7 @@ export function executeAssembly ( return executeCode({ ...DEFAULT_MESSAGE, ...params, code }, state) } -function assemblyToBytecode (code: string): Byte[] { +export function assemblyToBytecode (code: string): Byte[] { const instructions = code.trim().split(/\s+/) const result: Byte[] = [] for (const instruction of instructions) { diff --git a/packages/evm/test/opcodes/code.test.ts b/packages/evm/test/opcodes/code.test.ts new file mode 100644 index 0000000..450be5a --- /dev/null +++ b/packages/evm/test/opcodes/code.test.ts @@ -0,0 +1,57 @@ +import { + expectStorage, + Int256, + expectGas, + expectReturn, + assemblyToBytecode, + memoryGas, + expectUnderflow, +} from '../helpers' +import { GasCost } from '../../src/opcodes' + +describe('CODESIZE opcode', () => { + it('returns the code size of the current environment', () => { + expectStorage('CODESIZE PUSH1 00 SSTORE', { + [Int256.of(0)]: Int256.of(4), + }) + }) + + it(`costs ${GasCost.BASE} gas`, () => { + expectGas('CODESIZE', GasCost.BASE) + }) +}) + +describe('CODECOPY opcode', () => { + it('copies the code to the memory', () => { + const assembly = ` + PUSH1 0C + PUSH1 00 + PUSH1 42 + CODECOPY + PUSH1 0C + PUSH1 42 + RETURN + ` + expectReturn(assembly, assemblyToBytecode(assembly)) + }) + + it('uses a formula to calculate gas cost', () => { + const assembly = ` + PUSH1 42 + PUSH1 00 + PUSH1 69 + CODECOPY + ` + const gas = ( + GasCost.VERYLOW * 3 + + GasCost.VERYLOW + + GasCost.COPY * Math.ceil(0x42 / 32) + + memoryGas(0x69 + 0x42) + ) + expectGas(assembly, gas) + }) + + it('can cause stack underflow', () => { + expectUnderflow('CODECOPY', 3) + }) +}) From edc113e0f049d63714b1a13bf24c74f343cc0a04 Mon Sep 17 00:00:00 2001 From: Piotr Szlachciak Date: Sun, 9 Feb 2020 17:32:06 +0100 Subject: [PATCH 2/8] Partially implement CREATE --- packages/evm/src/Bytes32.ts | 10 +++ packages/evm/src/opcodes/create.ts | 54 +++++++++++++ packages/evm/src/opcodes/index.ts | 2 + packages/evm/test/Bytes32.test.ts | 16 ++++ packages/evm/test/helpers/executeAssembly.ts | 5 +- packages/evm/test/opcodes/create.test.ts | 83 ++++++++++++++++++++ 6 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 packages/evm/src/opcodes/create.ts create mode 100644 packages/evm/test/opcodes/create.test.ts diff --git a/packages/evm/src/Bytes32.ts b/packages/evm/src/Bytes32.ts index 15451f3..a66ea76 100644 --- a/packages/evm/src/Bytes32.ts +++ b/packages/evm/src/Bytes32.ts @@ -1,5 +1,6 @@ import BN from 'bn.js' import { Byte } from './Byte' +import { Address } from './Address' const TWO_POW256 = new BN('1' + '0'.repeat(64), 16) const MAX_256 = new BN('f'.repeat(64), 16) @@ -23,6 +24,10 @@ export class Bytes32 { return new Bytes32(new BN(value).toTwos(256)) } + static fromAddress (value: Address) { + return Bytes32.fromHex(value) + } + static fromHex (value: string) { return new Bytes32(new BN(value, 16)) } @@ -43,6 +48,11 @@ export class Bytes32 { return this.value.toString(16, 64) } + toAddress () { + const hex = this.toHex() + return hex.substring(24) as Address + } + toBytes () { return this.value.toArray(undefined, 32) as Byte[] } diff --git a/packages/evm/src/opcodes/create.ts b/packages/evm/src/opcodes/create.ts new file mode 100644 index 0000000..aca396c --- /dev/null +++ b/packages/evm/src/opcodes/create.ts @@ -0,0 +1,54 @@ +import { ExecutionContext } from '../ExecutionContext' +import { Bytes32 } from '../Bytes32' +import { getContractAddress } from '../getContractAddress' +import { executeCode } from '../executeCode' + +export function opCREATE (ctx: ExecutionContext) { + const value = ctx.stack.pop() + const memoryOffset = ctx.stack.pop().toUnsignedNumber() + const memoryBytes = ctx.stack.pop().toUnsignedNumber() + + const gasLeft = ctx.message.gasLimit - ctx.gasUsed + const gasLimit = allButOne64th(gasLeft) + const initCode = ctx.memory.getBytes(memoryOffset, memoryBytes) + + const nonce = ctx.state.getNonce(ctx.message.account) + const balance = ctx.state.getBalance(ctx.message.account) + + const contract = getContractAddress(ctx.message.account, nonce) + + if (balance.lt(value)) { + ctx.stack.push(Bytes32.ZERO) + return + } + + ctx.state.setNonce(ctx.message.account, nonce + 1) + ctx.state.setBalance(ctx.message.account, balance.sub(value)) + + const result = executeCode({ + account: contract, + callDepth: ctx.message.callDepth + 1, + sender: ctx.message.account, + origin: ctx.message.origin, + gasLimit, + gasPrice: ctx.message.gasPrice, + code: initCode, + data: [], + enableStateModifications: ctx.message.enableStateModifications, + value, + }, ctx.state.clone()) + + if (result.type === 'ExecutionSuccess') { + ctx.stack.push(Bytes32.fromAddress(contract)) + ctx.state = result.state + + ctx.useGas(result.gasUsed) + ctx.refund(result.gasRefund) + } else { + ctx.stack.push(Bytes32.ZERO) + } +} + +function allButOne64th (value: number) { + return value - Math.floor(value / 64) +} diff --git a/packages/evm/src/opcodes/index.ts b/packages/evm/src/opcodes/index.ts index d6d3c06..6c38bff 100644 --- a/packages/evm/src/opcodes/index.ts +++ b/packages/evm/src/opcodes/index.ts @@ -40,6 +40,7 @@ import { opMSIZE, opMLOAD, opMSTORE, opMSTORE8 } from './memory' import { opSSTORE, opSLOAD } from './storage' import { Byte } from '../Byte' import { opCODESIZE, opCODECOPY } from './code' +import { opCREATE } from './create' export { opUnreachable } from './invalid' export { makeOpPUSH } from './stack' @@ -122,6 +123,7 @@ const OP_CODES: Record = { 0x9d: makeOpSWAP(14), 0x9e: makeOpSWAP(15), 0x9f: makeOpSWAP(16), + 0xf0: opCREATE, 0xf3: opRETURN, 0xfd: opREVERT, } diff --git a/packages/evm/test/Bytes32.test.ts b/packages/evm/test/Bytes32.test.ts index 3013c2d..ef5c56b 100644 --- a/packages/evm/test/Bytes32.test.ts +++ b/packages/evm/test/Bytes32.test.ts @@ -3,6 +3,7 @@ import { Bytes32 } from '../src/Bytes32' import { TestCases } from './opcodes/bytes32/cases' import { TestCase } from './opcodes/bytes32/cases/helpers' import { Byte } from '../src/Byte' +import { Address } from '../src/Address' describe('Bytes32', () => { runTestCases('add', TestCases.ADD) @@ -29,6 +30,21 @@ describe('Bytes32', () => { runTestCases('shr', invert(TestCases.SHR)) runTestCases('sar', invert(TestCases.SAR)) + describe('to and from Address', () => { + it('toAddress ignores first bytes', () => { + const hex = 'ab'.repeat(16) + 'cd'.repeat(16) + const value = Bytes32.fromHex(hex) + expect(value.toAddress()).to.equal('ab'.repeat(4) + 'cd'.repeat(16)) + }) + + it('fromAddress works like from hex', () => { + const address = 'ab'.repeat(20) as Address + const a = Bytes32.fromAddress(address) + const b = Bytes32.fromHex(address) + expect(a.eq(b)).to.equal(true) + }) + }) + describe('to and from number', () => { it('fromNumber works for positive numbers', () => { const a = Bytes32.fromNumber(42) diff --git a/packages/evm/test/helpers/executeAssembly.ts b/packages/evm/test/helpers/executeAssembly.ts index fc822a9..cb0485f 100644 --- a/packages/evm/test/helpers/executeAssembly.ts +++ b/packages/evm/test/helpers/executeAssembly.ts @@ -30,7 +30,10 @@ export function executeAssembly ( } export function assemblyToBytecode (code: string): Byte[] { - const instructions = code.trim().split(/\s+/) + const instructions = code + .replace(/\/\/.*/g, ' ') // remove comments + .trim() + .split(/\s+/) const result: Byte[] = [] for (const instruction of instructions) { const opcode = OPCODES[instruction] as Byte diff --git a/packages/evm/test/opcodes/create.test.ts b/packages/evm/test/opcodes/create.test.ts new file mode 100644 index 0000000..d96d79e --- /dev/null +++ b/packages/evm/test/opcodes/create.test.ts @@ -0,0 +1,83 @@ +import { expect } from 'chai' +import { executeAssembly } from "../helpers" +import { Address } from "../../src/Address" +import { State } from "../../src/State" +import { getContractAddress } from '../../src/getContractAddress' +import { Bytes32 } from '../../src/Bytes32' + +describe('CREATE opcode', () => { + const assembly = ` + PUSH1 05 // size of the code + PUSH1 12 // code offset + PUSH1 00 // memory offset of the code + CODECOPY + + PUSH1 05 // size of the code + PUSH1 00 // memory offset of the code + PUSH1 69 // value passed + CREATE + + PUSH1 00 + SSTORE // save the address of the created contract + STOP + + // code of the contract + PUSH1 01 + PUSH1 00 + SSTORE // save 1 at address 0 in the storage of the new contract + ` + const account = 'abcd'.repeat(10) as Address + + it('results in the creation of a new contract', () => { + const state = new State() + state.setNonce(account, 42) + state.setBalance(account, Bytes32.fromNumber(0x100)) + + const result = executeAssembly(assembly, { account }, state) + + if (result.type !== 'ExecutionSuccess') { + expect(result.type).to.equal('ExecutionSuccess') + } else { + // increments nonce + expect(result.state.getNonce(account)).to.equal(43) + + // subtracts balance + const balance = result.state.getBalance(account) + expect(balance.eq(Bytes32.fromNumber(0x100 - 0x69))).to.equal(true) + + // returns correct address + const expectedAddress = getContractAddress(account, 42) + const actualAddress = result.state + .getStorage(account, Bytes32.ZERO) + .toAddress() + expect(actualAddress).to.equal(expectedAddress) + + // actually runs the contract code + const stored = result.state.getStorage(actualAddress, Bytes32.ZERO) + expect(stored.eq(Bytes32.ONE)).to.equal(true) + } + }) + + it('account does not have the balance', () => { + const state = new State() + state.setNonce(account, 42) + state.setBalance(account, Bytes32.fromNumber(0x68)) + + const result = executeAssembly(assembly, { account }, state) + + if (result.type !== 'ExecutionSuccess') { + expect(result.type).to.equal('ExecutionSuccess') + } else { + // does not increment the nonce + expect(result.state.getNonce(account)).to.equal(42) + + // does not subtract the balance + const balance = result.state.getBalance(account) + expect(balance.eq(Bytes32.fromNumber(0x68))).to.equal(true) + + // returns zero + const returnValue = result.state.getStorage(account, Bytes32.ZERO) + expect(returnValue).to.equal(Bytes32.ZERO) + } + }) +}) From 609d850e596a0889bc485df6be8b52ba2e1073dd Mon Sep 17 00:00:00 2001 From: Piotr Szlachciak Date: Wed, 18 Mar 2020 11:41:17 +0100 Subject: [PATCH 3/8] Fix linter errros --- packages/evm/test/opcodes/create.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/evm/test/opcodes/create.test.ts b/packages/evm/test/opcodes/create.test.ts index d96d79e..0cd8fe7 100644 --- a/packages/evm/test/opcodes/create.test.ts +++ b/packages/evm/test/opcodes/create.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai' -import { executeAssembly } from "../helpers" -import { Address } from "../../src/Address" -import { State } from "../../src/State" +import { executeAssembly } from '../helpers' +import { Address } from '../../src/Address' +import { State } from '../../src/State' import { getContractAddress } from '../../src/getContractAddress' import { Bytes32 } from '../../src/Bytes32' From 35e8bf9850d2a5c6efc48d8ef5b05e839c066618 Mon Sep 17 00:00:00 2001 From: Piotr Szlachciak Date: Wed, 18 Mar 2020 13:20:59 +0100 Subject: [PATCH 4/8] Change some TODOs to FIXMEs --- packages/evm/test/helpers/expectations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/evm/test/helpers/expectations.ts b/packages/evm/test/helpers/expectations.ts index 5375448..7c17b6f 100644 --- a/packages/evm/test/helpers/expectations.ts +++ b/packages/evm/test/helpers/expectations.ts @@ -18,7 +18,7 @@ export function makeStack (depth: number) { .map((value, index) => Int256.of(depth - index)) } -// TODO: This function does not work if you return early !!! +// FIXME: This function does not work if you return early !!! export function expectStackTop (assembly: string, value: string) { const account = ADDRESS_ZERO const result = executeAssembly(assembly + ' PUSH1 00 SSTORE', { account }) From b92091f3f97f59748b461cc7c4f63dd9b17a7ac0 Mon Sep 17 00:00:00 2001 From: Piotr Szlachciak Date: Wed, 18 Mar 2020 17:02:55 +0100 Subject: [PATCH 5/8] Add whitepaper guide --- packages/evm/whitepaper-guide.md | 100 +++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 packages/evm/whitepaper-guide.md diff --git a/packages/evm/whitepaper-guide.md b/packages/evm/whitepaper-guide.md new file mode 100644 index 0000000..6926e0c --- /dev/null +++ b/packages/evm/whitepaper-guide.md @@ -0,0 +1,100 @@ +# Whitepaper Guide + +https://ethereum.github.io/yellowpaper/paper.pdf + +This glossary will make reading the whitepaper much easier + +## Terms + +### Machine State + +- μ - machine state +- μg - available gas +- μpc - program counter +- μm - memory content +- μi - memory word count +- μs - stack +- μo - data returned from previous call + +### System State + +- σ - system state +- σ[a]n - nonce +- σ[a]b - balance +- σ[a]s - storage root +- σ[a]c - code hash + +### Transaction + +- T - transaction +- Tn - nonce +- Tp - gas price +- Tg - gas limit +- Tt - recipient (to) +- Tv - value +- Tw, Tr, Ts - signature (v, r, s) +- Ti - init code +- Td - data + +### Block header + +- H - transaction +- Hp - parent hash +- Ho - uncle (ommers) root +- Hc - coinbase (beneficiary) +- Hr - state root +- Ht - transactions root +- He - receipts root +- Hb - logs bloom +- Hd - difficulty +- Hi - number (0 for genesis) +- Hl - gas limit +- Hg - gas used +- Hs - unix timestamp +- Hx - extra data +- Hm - mix hash (proof of work) +- Hn - nonce (proof of work) + +### Transaction receipt + +- R - transaction receipt +- BR[i] - i-th transaction receipt +- Ru - cumulative gas used +- Rl - logs +- Rb - bloom filter +- Rz - status code + +### Log entry + +- O - log entry +- Oa - address of the logger +- Ot1, Ot2, ... - topics +- Od - data + +### Accrued substate + +- A - accrued substate +- As - self-destruct set +- Al - log series +- At - touched accounts +- Ar - refund balance + +### Execution environment + +- I - execution environment +- Ia - address +- Io - origin +- Ip - gas price +- Id - input data +- Is - sender (from) +- Iv - value +- Ib - code to be executed +- IH - block header of current block +- Ie - call depth +- Iw - state modification permission + +### Other + +- g - remaining computation gas +- o - resultant output +- Ξ - new state function From 8e28e9cb7a4349d6bb535e44bd3f362c4d035250 Mon Sep 17 00:00:00 2001 From: Piotr Szlachciak Date: Wed, 18 Mar 2020 17:03:06 +0100 Subject: [PATCH 6/8] Improve test description --- packages/evm/test/opcodes/create.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/evm/test/opcodes/create.test.ts b/packages/evm/test/opcodes/create.test.ts index 0cd8fe7..ddb585c 100644 --- a/packages/evm/test/opcodes/create.test.ts +++ b/packages/evm/test/opcodes/create.test.ts @@ -58,7 +58,7 @@ describe('CREATE opcode', () => { } }) - it('account does not have the balance', () => { + it('execution fails an reverts when balance is insufficient', () => { const state = new State() state.setNonce(account, 42) state.setBalance(account, Bytes32.fromNumber(0x68)) From 8047bf0a19ce1dbfdff222f4bc4fc7de4932d2f6 Mon Sep 17 00:00:00 2001 From: Piotr Szlachciak Date: Wed, 18 Mar 2020 19:08:13 +0100 Subject: [PATCH 7/8] Add missing parts to opCREATE but no tests --- packages/evm/src/ExecutionContext.ts | 1 + packages/evm/src/errors.ts | 6 ++ packages/evm/src/executeCode.ts | 2 +- packages/evm/src/opcodes/create.ts | 71 +++++++++++++++++++---- packages/evm/src/opcodes/gasCosts.ts | 1 + packages/evm/src/opcodes/storage.ts | 5 ++ packages/evm/test/opcodes/create.test.ts | 16 +++++ packages/evm/test/opcodes/storage.test.ts | 2 + packages/evm/whitepaper-guide.md | 2 +- 9 files changed, 93 insertions(+), 13 deletions(-) diff --git a/packages/evm/src/ExecutionContext.ts b/packages/evm/src/ExecutionContext.ts index ce59540..48d5b13 100644 --- a/packages/evm/src/ExecutionContext.ts +++ b/packages/evm/src/ExecutionContext.ts @@ -12,6 +12,7 @@ export class ExecutionContext { stack = new Stack() memory: Memory returnValue?: Bytes + previousCallReturnValue = Bytes.EMPTY reverted = false programCounter = 0 diff --git a/packages/evm/src/errors.ts b/packages/evm/src/errors.ts index c4f9b08..390ddab 100644 --- a/packages/evm/src/errors.ts +++ b/packages/evm/src/errors.ts @@ -42,6 +42,12 @@ export class InvalidJumpDestination extends VMError { } } +export class IllegalStateModification extends VMError { + constructor (kind: string) { + super(`Illegal state modification attempted: ${kind}`) + } +} + export class OutOfGas extends VMError { constructor () { super('Out of gas') diff --git a/packages/evm/src/executeCode.ts b/packages/evm/src/executeCode.ts index 5019df8..63c0796 100644 --- a/packages/evm/src/executeCode.ts +++ b/packages/evm/src/executeCode.ts @@ -6,7 +6,7 @@ import { ExecutionResult } from './ExecutionResult' import { State } from './State' export function executeCode (message: Message, state: State): ExecutionResult { - const ctx = new ExecutionContext(message, state) + const ctx = new ExecutionContext(message, state.clone()) while (ctx.returnValue === undefined) { const opcode = ctx.code[ctx.programCounter] || opSTOP diff --git a/packages/evm/src/opcodes/create.ts b/packages/evm/src/opcodes/create.ts index e02bda3..8a8d798 100644 --- a/packages/evm/src/opcodes/create.ts +++ b/packages/evm/src/opcodes/create.ts @@ -3,30 +3,43 @@ import { Bytes32 } from '../Bytes32' import { getContractAddress } from '../getContractAddress' import { executeCode } from '../executeCode' import { Bytes } from '../Bytes' +import { IllegalStateModification, OutOfGas } from '../errors' +import { GasCost } from './gasCosts' +import { State } from '../State' +import { Message } from '../Message' +import { ExecutionResult } from '../ExecutionResult' + +const CODE_SIZE_LIMIT = 24_576 export function opCREATE (ctx: ExecutionContext) { + if (!ctx.message.enableStateModifications) { + throw new IllegalStateModification('CREATE') + } + + ctx.useGas(GasCost.CREATE) + const value = ctx.stack.pop() const memoryOffset = ctx.stack.pop().toUnsignedNumber() const memoryBytes = ctx.stack.pop().toUnsignedNumber() - const gasLeft = ctx.message.gasLimit - ctx.gasUsed - const gasLimit = allButOne64th(gasLeft) + // We need to calculate this before return because memory access uses gas const initCode = ctx.memory.getBytes(memoryOffset, memoryBytes) - const nonce = ctx.state.getNonce(ctx.message.account) const balance = ctx.state.getBalance(ctx.message.account) - const contract = getContractAddress(ctx.message.account, nonce) + ctx.previousCallReturnValue = Bytes.EMPTY - if (balance.lt(value)) { + if (balance.lt(value) || ctx.message.callDepth >= 1024) { ctx.stack.push(Bytes32.ZERO) return } + const nonce = ctx.state.getNonce(ctx.message.account) ctx.state.setNonce(ctx.message.account, nonce + 1) - ctx.state.setBalance(ctx.message.account, balance.sub(value)) + const contract = getContractAddress(ctx.message.account, nonce) + const gasLimit = allButOne64th(ctx.message.gasLimit - ctx.gasUsed) - const result = executeCode({ + const result = executeContractCreation({ account: contract, callDepth: ctx.message.callDepth + 1, sender: ctx.message.account, @@ -35,21 +48,57 @@ export function opCREATE (ctx: ExecutionContext) { gasPrice: ctx.message.gasPrice, code: initCode, data: Bytes.EMPTY, - enableStateModifications: ctx.message.enableStateModifications, + enableStateModifications: true, value, - }, ctx.state.clone()) + }, ctx.state) if (result.type === 'ExecutionSuccess') { ctx.stack.push(Bytes32.fromAddress(contract)) ctx.state = result.state - ctx.useGas(result.gasUsed) ctx.refund(result.gasRefund) - } else { + // TODO: only do this if contract didn't SELFDESCTRUCT + ctx.state.setCode(contract, result.returnValue) + } else if (result.type === 'ExecutionRevert') { ctx.stack.push(Bytes32.ZERO) + ctx.useGas(result.gasUsed) + } else if (result.type === 'ExecutionError') { + ctx.stack.push(Bytes32.ZERO) + ctx.useGas(gasLimit) } } function allButOne64th (value: number) { return value - Math.floor(value / 64) } + +function executeContractCreation (message: Message, state: State): ExecutionResult { + const newState = state.clone() + newState.setBalance( + message.sender, + newState.getBalance(message.sender).sub(message.value), + ) + newState.setNonce(message.account, 1) + newState.setBalance( + message.account, + newState.getBalance(message.account).add(message.value), + ) + + const result = executeCode(message, newState) + + if (result.type === 'ExecutionSuccess') { + const finalCreationCost = GasCost.CODEDEPOSIT * result.returnValue.length + const totalGas = result.gasUsed + finalCreationCost + if ( + totalGas > message.gasLimit || + result.returnValue.length > CODE_SIZE_LIMIT + ) { + return { + type: 'ExecutionError', + error: new OutOfGas(), + } + } + } + + return result +} diff --git a/packages/evm/src/opcodes/gasCosts.ts b/packages/evm/src/opcodes/gasCosts.ts index edf61fb..5bf60b8 100644 --- a/packages/evm/src/opcodes/gasCosts.ts +++ b/packages/evm/src/opcodes/gasCosts.ts @@ -12,6 +12,7 @@ export const GasCost = { SSET: 20_000, SRESET: 5_000, CODEDEPOSIT: 200, + CREATE: 32_000, } export const GasRefund = { diff --git a/packages/evm/src/opcodes/storage.ts b/packages/evm/src/opcodes/storage.ts index 038e081..05eddd5 100644 --- a/packages/evm/src/opcodes/storage.ts +++ b/packages/evm/src/opcodes/storage.ts @@ -1,7 +1,12 @@ import { ExecutionContext } from '../ExecutionContext' import { GasCost, GasRefund } from './gasCosts' +import { IllegalStateModification } from '../errors' export function opSSTORE (ctx: ExecutionContext) { + if (!ctx.message.enableStateModifications) { + throw new IllegalStateModification('SSTORE') + } + const location = ctx.stack.pop() const value = ctx.stack.pop() diff --git a/packages/evm/test/opcodes/create.test.ts b/packages/evm/test/opcodes/create.test.ts index ddb585c..a6d865e 100644 --- a/packages/evm/test/opcodes/create.test.ts +++ b/packages/evm/test/opcodes/create.test.ts @@ -80,4 +80,20 @@ describe('CREATE opcode', () => { expect(returnValue).to.equal(Bytes32.ZERO) } }) + + xit('sets the code of the new account') + xit('does not set the code in case of error') + xit('does not set the code in case of revert') + xit('does not set the code in case of selfdestruct') + + xit('sets the nonce of the new account') + xit('sets the balance of the new account') + xit('accounts for potential previous balance') + + xit('only uses partial gas in case of revert') + + xit('correctly uses gas') + xit('handles child out of gas') + xit('respects call depth limit') + xit('respects state modification permissions') }) diff --git a/packages/evm/test/opcodes/storage.test.ts b/packages/evm/test/opcodes/storage.test.ts index 6b16a3b..673a201 100644 --- a/packages/evm/test/opcodes/storage.test.ts +++ b/packages/evm/test/opcodes/storage.test.ts @@ -45,6 +45,8 @@ describe('Storage opcodes', () => { expectUnderflow('SSTORE', 2) }) + xit('respects state modification permissions') + describe('refund', () => { it(`gets ${GasRefund.SCLEAR} refund when changing non-zero`, () => { const assembly = ` diff --git a/packages/evm/whitepaper-guide.md b/packages/evm/whitepaper-guide.md index 6926e0c..5ce3e53 100644 --- a/packages/evm/whitepaper-guide.md +++ b/packages/evm/whitepaper-guide.md @@ -90,7 +90,7 @@ This glossary will make reading the whitepaper much easier - Iv - value - Ib - code to be executed - IH - block header of current block -- Ie - call depth +- Ie - call depth (initially 0) - Iw - state modification permission ### Other From 1bbcaa4d29861445924470995936badab9ad3c76 Mon Sep 17 00:00:00 2001 From: Piotr Szlachciak Date: Wed, 18 Mar 2020 19:39:55 +0100 Subject: [PATCH 8/8] Fix incorrect slice usage --- packages/evm/src/Bytes.ts | 4 ++++ packages/evm/src/opcodes/code.ts | 9 +++------ packages/evm/test/Bytes.test.ts | 10 ++++++++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/evm/src/Bytes.ts b/packages/evm/src/Bytes.ts index 9341588..42c4008 100644 --- a/packages/evm/src/Bytes.ts +++ b/packages/evm/src/Bytes.ts @@ -48,6 +48,10 @@ export class Bytes { return new Bytes(this.value.slice(start * 2, end * 2)) } + padZeroesEnd (length: number) { + return new Bytes(this.value.padEnd(length * 2, '0')) + } + concat (other: Bytes) { return new Bytes(this.value + other.value) } diff --git a/packages/evm/src/opcodes/code.ts b/packages/evm/src/opcodes/code.ts index 6f131c3..d8443b0 100644 --- a/packages/evm/src/opcodes/code.ts +++ b/packages/evm/src/opcodes/code.ts @@ -1,7 +1,6 @@ import { ExecutionContext } from '../ExecutionContext' import { GasCost } from './gasCosts' import { Bytes32 } from '../Bytes32' -import { Bytes } from '../Bytes' export function opCODESIZE (ctx: ExecutionContext) { ctx.useGas(GasCost.BASE) @@ -17,10 +16,8 @@ export function opCODECOPY (ctx: ExecutionContext) { // we subtract the gas early in case of OutOfGas ctx.memory.useGasForAccess(memoryOffset, memorySize) - // FIXME: slice does not work like this - let code = ctx.message.code.slice(codeOffset, codeOffset + memorySize) - while (code.length < memorySize) { - code = code.concat(Bytes.fromNumber(0x00)) - } + const code = ctx.message.code + .slice(codeOffset, codeOffset + memorySize) + .padZeroesEnd(codeOffset + memorySize) ctx.memory.setBytes(memoryOffset, code) } diff --git a/packages/evm/test/Bytes.test.ts b/packages/evm/test/Bytes.test.ts index 020dbde..05efb03 100644 --- a/packages/evm/test/Bytes.test.ts +++ b/packages/evm/test/Bytes.test.ts @@ -45,6 +45,16 @@ describe('Bytes', () => { expect(bytes.slice(1, 3)).to.deep.equal(Bytes.fromString('3456')) }) + it('slice returns less if there is no content', () => { + const bytes = Bytes.fromString('123456') + expect(bytes.slice(1, 10)).to.deep.equal(Bytes.fromString('3456')) + }) + + it('can pad zeroes at the end', () => { + const padded = Bytes.fromString('1234').padZeroesEnd(5) + expect(padded).to.deep.equal(Bytes.fromString('1234000000')) + }) + it('can concat', () => { const first = Bytes.fromString('1234') const second = Bytes.fromString('5678')