Ethereum Virtual Machine in Rust - Part 5: How to execute a smart contract with Rocket (2/2)

I didn’t get to code a lot while writing the previous article so let’s fix that now! Here I will create a simple EVM that can receive message from an HTTP POST request. The smart contract will be written in assembly code (and very simple obviously).

With Rust, I will have to:

In Assembly, I will create the following contract:

Crafting the smart contract

According to the previous post, a smart contract is structured the following way:

We haven’t seen how the EVM is handling return values so for now I’ll assume we can get return values from the stack.

The add and square methods

Here we’ll implement the business logic and the parameters extraction from the input data. We’ll also be careful to not keep useless values on the stack. The code will most likely look a bit different than the one produced by solc. Remember: I used the non-optimized mode to easily follow the assembly code.

First of all, add takes two input parameters (integer). We saw before that uint is encoded as 32 bytes value in the input byte array. We can just fetch 32-bytes values at indexes 0x04 and 0x24. Then, adding two values on the stack is a simple opcode ADD.

PUSH1 0x4
CALLDATALOAD
PUSH1 0x24
CALLDATALOAD
ADD
STOP

square is similar, except that we have only one input parameter that we need to duplicate and multiple with itself.

PUSH1 0x4
CALLDATALOAD
DUP1
MUL
STOP

The STOP instruction will just tell the VM to stop the execution as we reached the end of the method. These two piece of codes will be respectively labeled add and square. I’ll replace manually the label by the real location when writing the final version of the code.

Routing to the correct method

Routing is done by comparing the 4 first bytes of the input data to the method ID of add and square. Method IDS are generated by the compiler using the Keccak hash, but to make it simpler I’ll just say that add corresponds to 0x01 and square corresponds to 0x02.

Combination of EQ and JUMPI takes care of the routing.

PUSH1 0                             
CALLDATALOAD                       
PUSH29 10000000000000000000000000000
DIV                                
DUP1                              
PUSH1 0x01                       
EQ                              
PUSH add
JUMPI
PUSH1 0x02
EQ
PUSH square
JUMPI
PUSH error
JUMP

Here we use the same trick as solc. Divide by (1 « 29) to get the 4 first bytes from the 32 bytes number. If no method ID is matched, we will jump to the error block.

Validation and error handling

Error handling will halt the execution, using the REVERT opcode. Before running the routing code, we also need to check whether the input size if at least 4 bytes. This can be done with

CALLDATASIZE
PUSH 0x4
LT
ISZERO
PUSH error
JUMPI
PUSH routing
JUMP

All in all

At the end, the assembly code is quite small. First, start by writing the size check. Then, add the routing part and the method’s implementation. Finish by adding the error handling code. JUMPDEST should not be forgotten. It will indicate that an instruction is the destination of a JUMP instruction. By the way, this smart contract is not using the memory so the free-pointer address is not set. Now, we can write this as a hexadecimal string the same way solc would compile our solidity code. I replaced the labels by the correct addresses here.

0x00    0x36    CALLDATASIZE
0x01    0x60    PUSH1 0x04
0x02    0x04
0x03    0x10    LT
0x04    0x15    ISZERO
0x05    0x60    PUSH1 error
0x06    0x4A
0x07    0x57    JUMPI
0x08    0x60    PUSH 0x00
0x09    0x00
0x0A    0x35    CALLDATALOAD
0x0B    0x7C    PUSH29 10000000000000000000000000000
0x0C    0x01
0x0D    0x00 
0x0E    0x00 
0x0F    0x00 
0x10    0x00 
0x11    0x00 
0x12    0x00 
0x13    0x00 
0x14    0x00 
0x15    0x00 
0x16    0x00 
0x17    0x00 
0x18    0x00 
0x19    0x00 
0x1A    0x00 
0x1B    0x00 
0x1C    0x00 
0x1D    0x00 
0x1E    0x00 
0x1F    0x00 
0x20    0x00 
0x21    0x00 
0x22    0x00 
0x23    0x00 
0x24    0x00 
0x25    0x00 
0x26    0x00 
0x27    0x00 
0x28    0x00 
0x29    0x90    SWAP1
0x2A    0x04    DIV
0x2B    0x80    DUP1
0x2C    0x60    PUSH 0x01
0x2D    0x01
0x2E    0x14    EQ
0x2F    0x60    PUSH1 add
0x30    0x3A
0x31    0x57    JUMPI
0x32    0x60    PUSH1 0x02
0x33    0x02
0x34    0x14    EQ
0x35    0x60    PUSH1 square
0x36    0x43
0x37    0x57    JUMPI
0x38    0x60    PUSH1 error
0x39    0x4A
0x3A    0x5b    JUMPDEST
0x3B    0x60    PUSH1 0x04
0x3C    0x04
0x3D    0x35    CALLDATALOAD
0x3E    0x60    PUSH1 0x24
0x3F    0x24
0x40    0x35    CALLDATALOAD
0x41    0x01    ADD
0x42    0x00    STOP
0x43    0x5b    JUMPDEST
0x44    0x60    PUSH1 0x04
0x45    0x04
0x46    0x35    CALLDATALOAD
0x47    0x80    DUP1
0x48    0x02    MUL
0x49    0x00    STOP
0x4A    0x5b    JUMPDEST
0x4B    0xfd    REVERT

Writing everything by hand is very error prone, no wonder high-level programming languages have been created :D

Final assembly as hexadecimal string: “0x3660041015604A576000357C0100000000000000000000000000000000000000000000000000000000900480600114603A57600214604357604a5b60043560243501005b6004358002005bfd”

We can try this without the HTTP interface using the existing code. Use this as input parameter and run the binary. It should finish with 0x04 at the top of the stack.

    let input_data = params::InputParameters::new(
        vec![0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);

JSON interface

In real life, the user does not interact directly with the EVM. The EVM is integrated inside the ethereum client and its API is not exposed to the user. Instead, Ethereum defines a JSON-RPC API, which can be found here in the Ethereum wiki. This API exposes a lot of functions, but the one we are interested in is how to send a transaction to a smart contract, which will be executed by the EVM.

eth_sendTransaction is the one we want.

Creates new message call transaction or a contract creation, if the data field contains code.

And it accepts the following JSON file as input. Notice that the hexadecimal strings under data is actually our EVM’s input parameters packed in a byte array. to is the address of the smart contract.

params: [{
  "from": "0xb60e8dd61c5d32be8058bb8eb970870f07233155",
  "to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
  "gas": "0x76c0",
  "gasPrice": "0x9184e72a000",
  "value": "0x9184e72a",
  "data": "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"
}]

In this article, I will not use JSON-RPC to expose an API to users. Instead, I will create a simple HTTP server that will accept POST requests, extract the parameters from the body in JSON format, execute the smart contract with the given input and send back the output.

Introducing Rocket, “A Web Framework for Rust”

Rocket is a web framework developed with ease-of-use in mind. I don’t want to dwell too much on the details here as it is an article about Ethereum, so you can find more details here. Please note that Rocket is using Rust Nightly so you have to use the nightly compiler for your project (just run rustup override set nightly in your cargo directory).

First of all, let’s create a structure to represent the input data we expect from our users.

#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use] extern crate rocket;
#[macro_use] extern crate rocket_contrib;
#[macro_use] extern crate serde_derive;

use rocket::State;
use rocket_contrib::json::{Json, JsonValue};
mod evm;
mod params;
use std::io;
use evm::vm::Vm;
use evm::opcode::Opcode;
use std::error::Error;
use std::env;


#[derive(Serialize, Deserialize)]
struct TransactionInput {
    // Address of the smart contract
    to: String,

    // Input data for the smart contract
    data: String,
}

The function transact will be called if a user sends a POST request with a JSON body representing TransactionInput. Then main will start Rocket. Routes are not automatically registered so you should not forget to add any additional routes using the routes! macro.

#[post("/transact", format = "json", data = "<message>")]
fn transact(message: Json<TransactionInput>) -> JsonValue {
    json!({ "status": "ok" })
}

fn main() {
    rocket::ignite().mount("/", routes![transact]).launch();
}

Now, you can compile and run the project. You can use curl, python and whatever HTTP client to test the API. In my case, I just use python with requests.

Rocket server is running!

Making a request with Python

Rocket is nice with us and will validate the request coming from from the user. Now that we have a functioning endpoint, we can implement the transact function. For each request, we instantiate a new VM and pass it the input parameters. Then we run the code until completion and return the result to the user. I will also add another endpoint to get a smart contract from a POST request and deploy it to the blockchain.

Storing smart contracts in Rocket’s managed state

The server can maintain state by using the rocket’s managed state. Note that Rocket is multithreaded so the state must be thread safe. In this case, an user can deploy a smart contract and another user can execute a transaction simultaneously, so the structure that hold smart contracts should be thread-safe.

struct CodeRepo {
    contracts: std::sync::Mutex<HashMap<String, Vec<u8>>>
}

The hashmap will match contract addresses (string) to bytecode. It is stored in a mutex to ensure the thread safeness. I only store the contracts as Vec<u8>. In reality, Ethereum has a set data structure for accounts (personal or contract). Contract accounts include a persistent state that can be accessed from the EVM code. Maybe I’ll add this functionality to the code later.

Then, the state can be added to Rocket before launch.

    rocket::ignite()
        .manage(CodeRepo { contracts: std::sync::Mutex::new(HashMap::new()) })
        .mount("/", routes![transact]).launch();

To deploy a smart contract, we will just get the hexadecimal string from the user request, generate an unique String ID and add it to the managed state. Then, when a user needs to execute the smart contract, he will have to provide this unique ID and the input parameters. Like before, we need to define a new structure to represent the deploy message. Similarly to transact, a deploy endpoint should be added. It will just decode the hexadecimal string to a vector of bytes and store it inside the managed state.

#[derive(Serialize, Deserialize)]
struct DeployInput {
    binary: String,
}

#[post("/deploy", format = "json", data = "<message>")]
fn deploy(message: Json<DeployInput>, state: State<CodeRepo>) -> JsonValue {

    match vm::decode(&message.0.binary) {
    Ok(v) => {
        // contract address
        let addr = format!("{}", Uuid::new_v4());
        {
            let mut contracts = state.contracts.lock().unwrap();
            let addr_clone = addr.clone();
            contracts.insert(addr_clone, v);
        }
        let addr_str = addr.as_str();
        json!({"address": addr_str})
    },
    _ => json!({"error": "cannot decode the binary data"})
    }

}

// ...

fn main() {
    rocket::ignite()
        .manage(CodeRepo { contracts: std::sync::Mutex::new(HashMap::new()) })
        .mount("/", routes![transact, deploy]).launch();
}

A few gotchas here:

Then we can implement the transact function. It should read the contract address and try to get it from the managed state. Then, it will instantiate a new EVM, set the input parameters and try to run it. The last part is not pretty: In this article, I will assume that the return value is a 64-bit value on top of the stack. This is a first approximation until I learn more about how the EVM returns value.

#[post("/transact", format = "json", data = "<message>")]
fn transact(message: Json<TransactionInput>, state: State<CodeRepo>) -> JsonValue {

    let mut code: Vec<u8> = Vec::new();
    {
        let contracts = state.contracts.lock().unwrap();
        match contracts.get(&message.0.to) {
            Some(contract) => code = contract.clone(),
            None => return json!({"error": "Cannot find contract"}),
        }
    }
    let input_str = message.0.data;

    // No error handling here :D
    let v = vm::decode(&input_str).expect("Input data should be hexadecimal");
    let mut vm = Vm::new(code, params::InputParameters::new(v));
    
    while !vm.at_end {
        vm.interpret();
    }

    match vm.status {
        vm::VmStatus::DONE => {
            match vm.stack.pop() {
                let returned = v.low_u64();
                Ok(v) => json!({"result": returned})
                Err => json!({"error": "Tried to return by no value on top of stack"})
            }
        },
        vm::VmStatus::REVERT => json!({"error": "error while running smart contract"}),
        _ => panic!("ABORRTTTTT"),
    }

}

Yay!

Interacting with our EVM with Python

Well, it does not look too bad.