Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Op create #51

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/evm/src/Bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down Expand Up @@ -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)
}
Expand Down
10 changes: 10 additions & 0 deletions packages/evm/src/Bytes32.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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))
}
Expand All @@ -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())
}
Expand Down
1 change: 1 addition & 0 deletions packages/evm/src/ExecutionContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export class ExecutionContext {
stack = new Stack()
memory: Memory
returnValue?: Bytes
previousCallReturnValue = Bytes.EMPTY
reverted = false
programCounter = 0

Expand Down
6 changes: 3 additions & 3 deletions packages/evm/src/Memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -34,7 +34,7 @@ export class Memory {
}
}

private onMemoryAccess (offset: number, length: number) {
useGasForAccess (offset: number, length: number) {
if (length === 0) {
return
}
Expand Down
6 changes: 6 additions & 0 deletions packages/evm/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion packages/evm/src/executeCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions packages/evm/src/opcodes/code.ts
Original file line number Diff line number Diff line change
@@ -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)
}
104 changes: 104 additions & 0 deletions packages/evm/src/opcodes/create.ts
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions packages/evm/src/opcodes/gasCosts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const GasCost = {
ZERO: 0,
BASE: 2,
VERYLOW: 3,
COPY: 3,
LOW: 5,
MID: 8,
HIGH: 10,
Expand All @@ -11,6 +12,7 @@ export const GasCost = {
SSET: 20_000,
SRESET: 5_000,
CODEDEPOSIT: 200,
CREATE: 32_000,
}

export const GasRefund = {
Expand Down
5 changes: 5 additions & 0 deletions packages/evm/src/opcodes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -75,6 +77,8 @@ const OP_CODES: Record<number, Opcode | undefined> = {
0x1b: opSHL,
0x1c: opSHR,
0x1d: opSAR,
0x38: opCODESIZE,
0x39: opCODECOPY,
0x50: opPOP,
0x51: opMLOAD,
0x52: opMSTORE,
Expand Down Expand Up @@ -118,6 +122,7 @@ const OP_CODES: Record<number, Opcode | undefined> = {
0x9d: makeOpSWAP(14),
0x9e: makeOpSWAP(15),
0x9f: makeOpSWAP(16),
0xf0: opCREATE,
0xf3: opRETURN,
0xfd: opREVERT,
}
5 changes: 5 additions & 0 deletions packages/evm/src/opcodes/storage.ts
Original file line number Diff line number Diff line change
@@ -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()

Expand Down
10 changes: 10 additions & 0 deletions packages/evm/test/Bytes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
16 changes: 16 additions & 0 deletions packages/evm/test/Bytes32.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 5 additions & 2 deletions packages/evm/test/helpers/executeAssembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion packages/evm/test/helpers/expectations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
Loading