diff --git a/packages/evm/src/Bytes.ts b/packages/evm/src/Bytes.ts index a73bdb3..42c4008 100644 --- a/packages/evm/src/Bytes.ts +++ b/packages/evm/src/Bytes.ts @@ -7,7 +7,7 @@ export class Bytes { static fromString (value: string) { if (!HEX_REGEX.test(value) || value.length % 2 !== 0) { - throw new TypeError('Invalid value') + throw new TypeError('Invalid value ' + value) } return new Bytes(value.toLowerCase()) } @@ -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/Bytes32.ts b/packages/evm/src/Bytes32.ts index 8452acd..9e29494 100644 --- a/packages/evm/src/Bytes32.ts +++ b/packages/evm/src/Bytes32.ts @@ -1,4 +1,5 @@ import BN from 'bn.js' +import { Address } from './Address' import { Bytes } from './Bytes' const TWO_POW256 = new BN('1' + '0'.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 Bytes.fromString(this.toHex()) } 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/Memory.ts b/packages/evm/src/Memory.ts index 30e2ad3..4d8c399 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 Bytes.EMPTY } @@ -24,7 +24,7 @@ export class Memory { } setBytes (offset: number, bytes: Bytes) { - 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/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/code.ts b/packages/evm/src/opcodes/code.ts new file mode 100644 index 0000000..d8443b0 --- /dev/null +++ b/packages/evm/src/opcodes/code.ts @@ -0,0 +1,23 @@ +import { ExecutionContext } from '../ExecutionContext' +import { GasCost } from './gasCosts' +import { Bytes32 } from '../Bytes32' + +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) + .padZeroesEnd(codeOffset + memorySize) + ctx.memory.setBytes(memoryOffset, code) +} diff --git a/packages/evm/src/opcodes/create.ts b/packages/evm/src/opcodes/create.ts new file mode 100644 index 0000000..8a8d798 --- /dev/null +++ b/packages/evm/src/opcodes/create.ts @@ -0,0 +1,104 @@ +import { ExecutionContext } from '../ExecutionContext' +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() + + // We need to calculate this before return because memory access uses gas + const initCode = ctx.memory.getBytes(memoryOffset, memoryBytes) + + const balance = ctx.state.getBalance(ctx.message.account) + + ctx.previousCallReturnValue = Bytes.EMPTY + + 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) + const contract = getContractAddress(ctx.message.account, nonce) + const gasLimit = allButOne64th(ctx.message.gasLimit - ctx.gasUsed) + + const result = executeContractCreation({ + account: contract, + callDepth: ctx.message.callDepth + 1, + sender: ctx.message.account, + origin: ctx.message.origin, + gasLimit, + gasPrice: ctx.message.gasPrice, + code: initCode, + data: Bytes.EMPTY, + enableStateModifications: true, + value, + }, ctx.state) + + if (result.type === 'ExecutionSuccess') { + ctx.stack.push(Bytes32.fromAddress(contract)) + ctx.state = result.state + ctx.useGas(result.gasUsed) + ctx.refund(result.gasRefund) + // 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 6c7d06b..5bf60b8 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, @@ -11,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/index.ts b/packages/evm/src/opcodes/index.ts index 43b0d3a..4343bb1 100644 --- a/packages/evm/src/opcodes/index.ts +++ b/packages/evm/src/opcodes/index.ts @@ -38,6 +38,8 @@ import { invalidOpcode } from './invalid' import { makeOpDUP, makeOpSWAP, opPOP } from './stack' import { opMSIZE, opMLOAD, opMSTORE, opMSTORE8 } from './memory' import { opSSTORE, opSLOAD } from './storage' +import { opCODESIZE, opCODECOPY } from './code' +import { opCREATE } from './create' export { opUnreachable } from './invalid' export { makeOpPUSH } from './stack' @@ -75,6 +77,8 @@ const OP_CODES: Record = { 0x1b: opSHL, 0x1c: opSHR, 0x1d: opSAR, + 0x38: opCODESIZE, + 0x39: opCODECOPY, 0x50: opPOP, 0x51: opMLOAD, 0x52: opMSTORE, @@ -118,6 +122,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/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/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') diff --git a/packages/evm/test/Bytes32.test.ts b/packages/evm/test/Bytes32.test.ts index 649a7c4..949eceb 100644 --- a/packages/evm/test/Bytes32.test.ts +++ b/packages/evm/test/Bytes32.test.ts @@ -2,6 +2,7 @@ import { expect } from 'chai' import { Bytes32 } from '../src/Bytes32' import { TestCases } from './opcodes/bytes32/cases' import { TestCase } from './opcodes/bytes32/cases/helpers' +import { Address } from '../src/Address' import { Bytes } from '../src/Bytes' describe('Bytes32', () => { @@ -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 641d158..b57da3a 100644 --- a/packages/evm/test/helpers/executeAssembly.ts +++ b/packages/evm/test/helpers/executeAssembly.ts @@ -29,8 +29,11 @@ export function executeAssembly ( return executeCode({ ...DEFAULT_MESSAGE, ...params, code }, state) } -function assemblyToBytecode (code: string): Bytes { - const instructions = code.trim().split(/\s+/) +export function assemblyToBytecode (code: string): Bytes { + const instructions = code + .replace(/\/\/.*/g, ' ') // remove comments + .trim() + .split(/\s+/) let result = Bytes.EMPTY for (const instruction of instructions) { const opcode = OPCODES[instruction] 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 }) 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) + }) +}) diff --git a/packages/evm/test/opcodes/create.test.ts b/packages/evm/test/opcodes/create.test.ts new file mode 100644 index 0000000..a6d865e --- /dev/null +++ b/packages/evm/test/opcodes/create.test.ts @@ -0,0 +1,99 @@ +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('execution fails an reverts when balance is insufficient', () => { + 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) + } + }) + + 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 new file mode 100644 index 0000000..5ce3e53 --- /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 (initially 0) +- Iw - state modification permission + +### Other + +- g - remaining computation gas +- o - resultant output +- Ξ - new state function