Skip to content

Commit

Permalink
Merge pull request #9 from krzkaczor/kk/support-for-contract-deploy
Browse files Browse the repository at this point in the history
Run real world contracts
  • Loading branch information
krzkaczor committed Jul 13, 2018
2 parents 5605422 + 820a03f commit d026a2d
Show file tree
Hide file tree
Showing 40 changed files with 1,068 additions and 218 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.nyc_output
coverage
lib/__tests__/contracts/*.bin
lib/__tests__/contracts/*.abi
7 changes: 7 additions & 0 deletions @types/not-typed.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
declare module "ethereumjs-vm";
declare module "ethereumjs-tx";
declare module "ethereumjs-account";
declare module "ethereumjs-util";
declare module "merkle-patricia-tree";
declare module "rlp";

declare module "web3";
2 changes: 2 additions & 0 deletions @types/std.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export type DeepReadonly<T> = {
? ReadonlyArray<DeepReadonly<U>>
: T[P] extends BN ? BN : DeepReadonly<T[P]>
};

export type TDictionary<T> = { [key: string]: T };
62 changes: 62 additions & 0 deletions lib/Blockchain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { BN } from "bn.js";
import * as invariant from "invariant";

import { VM, IMachineState } from "./VM";
import { TDictionary } from "../@types/std";

export interface IAccount {
value: BN;
code?: ReadonlyArray<number>;
storage?: number[];
}

// @todo this is too permissive
export interface ITransaction {
to?: string;
data?: number[];
value?: BN;
}

export interface ITransactionResult {
account: IAccount;
runState: IMachineState;
accountCreated?: string;
}

export class Blockchain {
public readonly vm = new VM();
private accounts: TDictionary<IAccount> = {};
private nextAccountId = 0x0; // for now we dont implement priv/pub keys or anything

// @todo REFACTOR
public runTx(tx: ITransaction): ITransactionResult {
invariant(tx.data || tx.to || tx.value, "Tx is empty");

if (!tx.to) {
const result = this.vm.runCode({ code: tx.data, value: tx.value || new BN(0) });
invariant(result.state.return, "Code deployment has to return code");

this.accounts[this.nextAccountId++] = {
value: new BN(0), // @todo tx value always 0.
code: result.state.return,
// storage: result.state. // @todo missing storage
};

return {
account: this.accounts[this.nextAccountId],
runState: result.state,
accountCreated: (this.nextAccountId - 1).toString(),
};
} else {
const contract = this.accounts[tx.to];
invariant(contract, `Account ${tx.to} not found!`);

const result = this.vm.runCode({ code: contract.code, value: tx.value || new BN(0), data: tx.data });

return {
account: contract,
runState: result.state,
};
}
}
}
90 changes: 62 additions & 28 deletions lib/VM.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,108 @@
import { BN } from "bn.js";
import StrictEventEmitter from "strict-event-emitter-types";

import { Stack } from "./utils/Stack";
import { DeepReadonly } from "../@types/std";
import { IProgram } from "./decodeBytecode";
import { DeepReadonly, TDictionary } from "../@types/std";
import { decodeOpcode } from "./decodeBytecode";
import { PeekableIterator } from "./utils/PeekableIterator";
import { EventEmitter } from "events";
import { Opcode } from "./opcodes/common";

export type TStorage = TDictionary<string>;

export interface IMachineState {
pc: number;
stack: Stack<BN>;
memory: number[];
storage: TStorage;
stopped: boolean;
return?: ReadonlyArray<number>;
}

export type IEnvironment = DeepReadonly<{
value: BN;
data: number[];
code: number[];
}>;

const initialState: IMachineState = {
pc: 0,
stack: new Stack(),
memory: [],
storage: {},
stopped: false,
};

const initialEnvironment = {
const initialEnvironment: IEnvironment = {
value: new BN(0),
code: [],
data: [],
};

export class VM {
public readonly environment: IEnvironment;

constructor(
public program: IProgram,
environment: Partial<IEnvironment> = initialEnvironment,
public state = deepCloneState(initialState),
) {
this.environment = {
...initialEnvironment,
...environment,
};
export interface IStepContext {
previousState: IMachineState;
currentOpcode: Opcode;
}

interface IVmEvents {
step: IStepContext;
}

const VmEventsEmitter: { new (): StrictEventEmitter<EventEmitter, IVmEvents> } = EventEmitter as any;

export class VM extends VmEventsEmitter {
private environment: IEnvironment = initialEnvironment;
private codeIterator?: PeekableIterator<number>;

constructor(public state = deepCloneState(initialState)) {
super();
}

step(): void {
private step(): void {
if (this.state.stopped) {
throw new Error("Machine stopped!");
}
if (!this.environment.code || !this.codeIterator) {
throw new Error("No code to execute");
}
if (!this.environment) {
throw new Error("No environment to execute");
}

const instructionIndex = this.program.sourceMap[this.state.pc];
// opcodes mutate states so we deep clone it first
const newState = deepCloneState(this.state);

if (instructionIndex === undefined) {
this.state = {
...this.state,
stopped: true,
};
if (this.state.pc >= this.environment.code.length) {
newState.stopped = true;
this.state = newState;
return;
}
const instruction = this.program.opcodes[instructionIndex];

// opcodes mutate states so we deep clone it first
const newState = deepCloneState(this.state);
this.codeIterator.index = this.state.pc;
const opcode = decodeOpcode(this.codeIterator);

this.emit("step", { currentOpcode: opcode, previousState: this.state });

try {
instruction.run(newState, this.environment);
opcode.run(newState, this.environment);
} catch (e) {
throw new Error(`Error while running bytecode at ${this.state.pc}: ${e.message}`);
throw new Error(`Error while running ${opcode.type} at position ${this.state.pc}: ${e.message}`);
}
this.state = newState;
}

run(): void {
runCode(environment: Partial<IEnvironment> = initialEnvironment): { state: IMachineState } {
this.environment = { ...initialEnvironment, ...environment, value: environment.value || initialEnvironment.value };
this.codeIterator = new PeekableIterator(this.environment.code);
this.state = deepCloneState(initialState);

while (!this.state.stopped) {
this.step();
}

return {
state: this.state,
};
}
}

Expand All @@ -79,5 +112,6 @@ function deepCloneState(state: IMachineState): IMachineState {
stopped: state.stopped,
stack: new Stack(state.stack),
memory: [...state.memory],
storage: { ...state.storage },
};
}
85 changes: 39 additions & 46 deletions lib/__tests__/VM.spec.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,57 @@
import { expect } from "chai";
import { BN } from "bn.js";

import { VM, IMachineState, IEnvironment } from "../VM";
import { VM } from "../VM";
import * as opcodes from "../opcodes";
import { Opcode } from "../opcodes/common";
import { Stack } from "../utils/Stack";
import { IProgram } from "../decodeBytecode";

/* tslint:disable */

describe("VM", () => {
it("should run simple program", () => {
const input: IProgram = {
opcodes: [new opcodes.PushOpcode(1, new BN(1)), new opcodes.PushOpcode(1, new BN(2)), new opcodes.AddOpcode()],
sourceMap: { 0: 0, 2: 1, 4: 2 },
};
// here we need some nice way to decode input into opcodes
it.skip("should run simple program", () => {
const input = [new opcodes.PushOpcode(1, new BN(1)), new opcodes.PushOpcode(1, new BN(2)), new opcodes.AddOpcode()];
const expectedState = {
pc: 5,
stopped: true,
stack: [new BN(3)],
memory: [],
};

const bytecodeRunner = new VM(input);
bytecodeRunner.run();
const bytecodeRunner = new VM();
bytecodeRunner.runCode();
expect(bytecodeRunner.state).to.deep.eq(expectedState);
});

it("should clone state before passing it to opcodes", () => {
const initialState: IMachineState = {
pc: 0,
stack: new Stack([new BN(1), new BN(2)]),
memory: [],
stopped: false,
};

const initialEnv: IEnvironment = { value: new BN(0) };

class StateMutatingOpcode extends Opcode {
run(state: IMachineState): void {
state.stack.push(new BN(6));
state.memory.push(1);
state.pc += 1;
}
}

const input: IProgram = { opcodes: [new StateMutatingOpcode()], sourceMap: { 0: 0 } };
const expected = "Cannot add property 0, object is not extensible";

const bytecodeRunner = new VM(input, initialEnv, initialState);
expect(() => bytecodeRunner.run()).to.not.throw(Error, expected);
expect(initialState).to.deep.eq({
pc: 0,
stack: new Stack([new BN(1), new BN(2)]),
memory: [],
stopped: false,
});
expect(bytecodeRunner.state).to.deep.eq({
stack: [new BN(1), new BN(2), new BN(6)],
memory: [1],
pc: 1,
stopped: true,
});
it.skip("should clone state before passing it to opcodes", () => {
// const initialState: IMachineState = {
// pc: 0,
// stack: new Stack([new BN(1), new BN(2)]),
// memory: [],
// stopped: false,
// };
// const initialEnv: IEnvironment = { value: new BN(0) };
// class StateMutatingOpcode extends Opcode {
// run(state: IMachineState): void {
// state.stack.push(new BN(6));
// state.memory.push(1);
// state.pc += 1;
// }
// }
// const input: IProgram = { opcodes: [new StateMutatingOpcode()], sourceMap: { 0: 0 } };
// const expected = "Cannot add property 0, object is not extensible";
// const bytecodeRunner = new VM(input, initialEnv, initialState);
// expect(() => bytecodeRunner.run()).to.not.throw(Error, expected);
// expect(initialState).to.deep.eq({
// pc: 0,
// stack: new Stack([new BN(1), new BN(2)]),
// memory: [],
// stopped: false,
// });
// expect(bytecodeRunner.state).to.deep.eq({
// stack: [new BN(1), new BN(2), new BN(6)],
// memory: [1],
// pc: 1,
// stopped: true,
// });
});
});
13 changes: 13 additions & 0 deletions lib/__tests__/contracts/DumbContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
pragma solidity ^0.4.17;

contract DumbContract {
uint256 a;
function C() {
a = 666;
}

function test() public returns (uint256) {
a += 1;
return 5;
}
}

0 comments on commit d026a2d

Please sign in to comment.