/solevm

Partial implementation of the Ethereum runtime in Solidity (PoC)

Primary LanguageJavaScriptMIT LicenseMIT

Solidity EVM and Runtime (PoC)

This is a simple Ethereum runtime written in Solidity. The runtime contract allows you to execute evm bytecode with calldata and various other parameters. It is meant to be used for one-off execution, like is done with the "evm" executables that comes with the ethereum clients.

This is still very early in development. There is no support for gas metering, and the limit to contract code-size could make it impossible to add. See the roadmap section for the plans ahead.

NOTE: This is only an experiment in it's early PoC stages. Do not rely on this library to test or verify EVM bytecode.

Update 2018-06-05: There is no way to add all gas metering without exceeding the maximum codesize

Building and Testing

The runtime is a regular Solidity contract that can be compiled by solc, and can therefore be used with libraries such as web3js or just executed by the various different stand-alone evm implementations. The limitations is that a lot of gas is required in order to run the code, and that web3js does not have support for Solidity structs (ABI tuples).

In order to build and test the code you need go-ethereum's evm as well as solc on your path. The code is tested using the solidity 0.4.24 release version and the evm 1.8.7 version - both with constantinople settings.

bin/compile.js can be executed to create bin, bin-runtime, abi and signatures files for the runtime contract. The files are put in the bin_output folder.

npm run test can be run to test the runtime contract. It will automatically compile all the involved contracts.

bin/test.js can be used to test some of the supporting contracts and libraries.

bin/perf.js can be used to compute gas cost estimates for the runtime and supporting libraries.

Runtime

First of all, the EthereumRuntime code is designed to run on a constantinople net, with all constantinople features. The genesis.json file in the root folder can be used to configure the geth EVM (through the --prestate option).

The executable contract is EthereumRuntime.sol. The contract has an execute method which is used to run code. It has many overloaded versions, but the simplest version takes two arguments - code and data.

code is the bytecode to run.

data is the calldata.

The solidity type for both of them is bytes memory.

// Execute the given code and call-data.
    function execute(bytes memory code, bytes memory data) public pure returns (Result memory state);

    // Execute the given transaction.
    function execute(TxInput memory input) public pure returns (Result memory result);

    // Execute the given transaction in the given context.
    function execute(TxInput memory input, Context memory context) public pure returns (Result memory result);

The other alternatives have two objects, TxInput and Context:

    struct TxInput {
        uint64 gas;
        uint gasPrice;
        address caller;
        uint callerBalance;
        uint value;
        address target;
        uint targetBalance;
        bytes targetCode;
        bytes data;
        bool staticExec;
    }

The gas and gasPrice fields are reserved but never used, since gas is not yet supported. All the other params are obvious except for staticExec which should be set to true if the call should be executed as a STATICCALL, i.e. what used to be called a read-only call (as opposed to a transaction).

struct Context {
    address origin;
    uint gasPrice;
    uint gasLimit;
    uint coinBase;
    uint blockNumber;
    uint time;
    uint difficulty;
}

These fields all speak for themselves.

NOTE: There is no actual CREATE operation taking place for the contract account in which the code is run, i.e. the code to execute would normally be runtime code; however, the code being run can create new contracts.

The return value from the execute functions is a struct on the form:

struct Result {
    uint errno;
    uint errpc;
    bytes returnData;
    uint[] stack;
    bytes mem;
    uint[] accounts;
    bytes accountsCode;
    uint[] logs;
    bytes logsData;
}

errno - an error code. If execution was normal, this is set to 0.

errpc - the program counter at the time when execution stopped.

returnData - the return data. It will be empty if no data was returned.

stack - The stack when execution stopped.

mem - The memory when execution stopped.

accounts - Account data packed into an uint-array, omitting the account code.

accountsCode - The combined code for all accounts.

logs - Logs packed into an uint-array, omitting the log data.

logsData - The combined data for all logs.

Note that errpc is only meant to be used when the error is non-zero, in which case it will be the program counter at the time when the error was thrown.

There is a javascript (typescript) adapter at script/adapter.ts which allow you to run the execute function from within this library, and automatically format input and output parameters. The return data is formatted as such:

{
    errno: number,
    errpc: number,
    returnData: string (hex),
    stack: [BigNumber],
    mem: string (hex),
    accounts: [{
        address: string (hex),
        balance: BigNumber,
        nonce: BigNumber,
        destroyed: boolean
        storage: [{
            address: BigNumber,
            value: BigNumber
        }]
    }]
    logs: [{
        account: string (hex)
        topics: [BigNumber] (size 4)
        data: string (hex)
    }]
}

There is a pretty-print function in the adapter as well.

Accounts

Accounts are on the following format:

account : {
    addr: address,
    balance: uint,
    nonce: uint8,
    destroyed: bool
    code: bytes
    storage: [{
        addr: uint,
        val: uint
    }]
}

nonce is restricted to uint8 (max 255) to make new account creation easier, since it will get a simpler RLP encoding.

The destroyed flag is used to indicate whether or not a (contract) account has been destroyed during execution. This can only happen if SELFDESTRUCT is called in that contract.

When executing code, two accounts will always be created - the caller account, and the contract account used to run the provided code. In the simple "code + data" call, the caller and contract account are assigned default addresses.

In contract code, accounts and account storage are both arrays instead of maps. Technically they are implemented as (singly) linked lists. This will be improved later.

The "raw" int arrays in the return object has an account packed in the following way:

  • 0: account address

  • 1: account balance

  • 2: account nonce

  • 3: account destroyed (true or false)

  • 4: code starting index (in combined 'accountsCode' array).

  • 5: code size

  • 6: number of entries in storage

  • 7+ : pairs of (address, value) storage entries

The size of an account is thus: 7 + storageEntries*2.

The accounts array is a series of accounts: [account0, account1, ... ]

Logs

Logs are on the following format:

log: {
     account: address
     topics: uint[4]
     data: bytes
 }

account - The address of the account that generated the log entry.

topics - The topics.

data - The data.

The "raw" int arrays in the return object has a log packed in the following way:

  • 0: account address

  • 1 - 4: topics

  • 5: data starting index (in combined 'logsData' array).

  • 6: data size.

Blockchain

There are no blocks, so BLOCKHASH will always return 0. The only blockchain related parameters that can be set are those in the context object.

Javascript

In addition to the contracts, the library also comes with some rudimentary javascript (typescript) for compiling the contract, and for executing unit tests through the geth evm. In order for this to work, both solc and the geth evm must be on the path.

The script/adapter.ts file can be used to call the runtime contract with code and data provided as hex-strings. Currently, only the code + data version and the TxInput overload is supported, but the one using both TxInput and Context will soon be supported as well.

The supporting script will be improved over time.

Current status

The initial version only lets you run code. There is no gas metering system in place.

Calls are currently being tested, and is not yet verified to work well in all cases. CALLCODE has not yet been added (and may not be).

Of the precompiled contracts, only ecrecover, sha256, ripemd160 and identity has been implemented. Neither of them are properly tested.

CREATE2 has not been added.

Roadmap

The plan for 0.2.0, is to support all instructions, and to have a full test suite done.

The plan for 0.3.0 is that the runtime should be properly checked against the yellow paper specification - or at least the parts of the protocol that is supported (gas may never be).

The plan for 0.4.0 is to extend the capacities of the runtime and add some performance optimization. Gas may or may not be added here.

The long-term plan is to add gas metering, and also add a way to add block data, chain settings, and other things. Whether any of that will be possible depends on many things, including the limitations of the EVM and Solidity (such as the maximum allowed code-size for contracts, and Solidity's stack limitations).