Skip to content
Merged
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
43 changes: 43 additions & 0 deletions .github/workflows/develop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Test on PR

on:
pull_request:
branches: [ main ]
workflow_dispatch:
push:
branches:
- main


env:
CARGO_TERM_COLOR: always

jobs:
Test:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3 # Git toolchain to check out code

- uses: actions-rs/toolchain@v1 # Rust toolchain
with:
toolchain: 1.73.0
components: rustfmt, clippy

- name: Get OS infomration
id: os
run: echo "KERNEL=$(uname -r)" >> $GITHUB_OUTPUT

- uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{steps.os.outputs.KERNEL}}-cargo-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('**/Cargo.toml') }}

- name: Run Rust tests
run: cargo test
49 changes: 49 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Release

on:
push:
tags:
- '*'

jobs:
release:
name: Build and publish binaries

runs-on: ubuntu-latest

env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

steps:

- uses: actions/checkout@v3 # Git toolchain to check out code

- uses: actions-rs/toolchain@v1 # Rust toolchain
with:
toolchain: 1.73.0

- name: Get OS infomration
id: os
run: echo "KERNEL=$(uname -r)" >> $GITHUB_OUTPUT

- uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.tinyevm/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{steps.os.outputs.KERNEL}}-${{ hashFiles('**/Cargo.toml') }}

- name: Build with file system cache
run: |
cargo build --release

- name: Upload binaries to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: target/release/evm-interpreter
overwrite: true
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ revm = {git="https://github.com/bluealloy/revm", branch="main", features=[ "serd
revme = {git="https://github.com/bluealloy/revm", branch="main"}
serde = "1.0.190"
serde_json = "1.0.107"
tempfile = "3.8.1"
walkdir = "2.4.0"
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,36 @@ cargo install --path .

## Usage

### Execute a transaction

```bash
❯ evm-interpreter -h
Usage: evm-interpreter [OPTIONS]
❯ evm-interpreter execute -h
Usage: evm-interpreter execute [OPTIONS]

Options:
--bytecode <BYTECODE> A hex string representing a contract runtime binary code
--input <INPUT> An optional hex encoded transaction data
--pprint If provided, print stack traces to stdout
--output <OUTPUT> If provided, output as JSON to this file
--test-json <TEST_JSON> If provided, use the ethtest JSON file the input
--test-json <TEST_JSON> If provided, use the ethtest JSON file as the input
--limit <LIMIT> Maximum number of test files to run, valid when using with --test-json [default: 10]
-h, --help Print help
-V, --version Print version

```

### Compare with CuEVM

```bash
❯ evm-interpreter compare --executable path_to_cuevm_interpreter --test-json dev-resources/ethtest/GeneralStateTests/VMTests/vmArithmeticTest/arith.json
```


## Examples

### Call contract with no input (zero length transaction)

``` bash
❯ evm-interpreter --bytecode 604260005260206000F3 --pprint
❯ evm-interpreter execute --bytecode 604260005260206000F3 --pprint
Input: Bytes(0x)
Output: Bytes(0x0000000000000000000000000000000000000000000000000000000000000042)
➡️ PC: 0 OPCODE: 0x60 PUSH1
Expand Down
178 changes: 178 additions & 0 deletions src/comparator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use eyre::Result;
use revm::primitives::{
calc_excess_blob_gas, keccak256, Bytecode, Bytes, CreateScheme, Env, TransactTo, U256,
};
use std::{path::Path, process::Command};
use tempfile::Builder;

use crate::{cuevm_test_suite::CuEvmTestSuite, inspector::TraceInspector};

/// Compare the output of CuEVM with the output of revm. Panics if there is a
/// mismatch.
pub fn execute_and_compare(
cuevm_executable: String,
test_json: String,
pprint: bool,
) -> Result<()> {
let output_json = {
let temp_dir = std::env::temp_dir();
let file = Builder::new()
.prefix("mytempfile_")
.suffix(".json")
.tempfile_in(temp_dir)?;
file.into_temp_path()
.canonicalize()?
.as_path()
.to_str()
.unwrap()
.to_string()
};

let out = Command::new(cuevm_executable)
.args(["--input", &test_json])
.args(["--output", &output_json])
.output()?;

if output_json.is_empty() {
Err(eyre::eyre!(
"Output json is empty, cuevm returns stdout: {:?}",
out.stdout
))?;
}

println!("Input: {} CuEVM Output: {}", test_json, output_json);

let path = Path::new(&output_json);
let s = std::fs::read_to_string(path)?;
let suite: CuEvmTestSuite = serde_json::from_str(&s)?;

for (_name, unit) in suite.0 {
// Create database and insert cache
let mut cache_state = revm::CacheState::new(false);
for (address, info) in unit.pre {
let acc_info = revm::primitives::AccountInfo {
balance: info.balance,
code_hash: keccak256(&info.code),
code: Some(Bytecode::new_raw(info.code)),
nonce: info.nonce,
};
cache_state.insert_account_with_storage(address, acc_info, info.storage);
}

let mut env = Env::default();
env.cfg.chain_id = 1;
env.block.number = unit.env.current_number;
env.block.coinbase = unit.env.current_coinbase;
env.block.timestamp = unit.env.current_timestamp;
env.block.gas_limit = unit.env.current_gas_limit;
env.block.basefee = unit.env.current_base_fee.unwrap_or_default();
env.block.difficulty = unit.env.current_difficulty;
env.block.prevrandao = Some(unit.env.current_difficulty.to_be_bytes().into());
if let (Some(parent_blob_gas_used), Some(parent_excess_blob_gas)) = (
unit.env.parent_blob_gas_used,
unit.env.parent_excess_blob_gas,
) {
env.block
.set_blob_excess_gas_and_price(calc_excess_blob_gas(
parent_blob_gas_used.to(),
parent_excess_blob_gas.to(),
));
}

// Run test in post generated by cuevm
for (index, test) in unit.post.into_iter().enumerate() {
env.tx.gas_limit = test.msg.gas_limit.saturating_to();
env.tx.caller = test.msg.sender;
env.tx.gas_price = test.msg.gas_price.unwrap_or_default(); // Note some ethtest has max_fee_per_gas

env.tx.data = test.msg.data.clone();
env.tx.value = test.msg.value;

let to = match test.msg.to {
Some(add) => TransactTo::Call(add),
None => TransactTo::Create(CreateScheme::Create),
};
env.tx.transact_to = to;

let cache = cache_state.clone();
let mut state = revm::db::State::builder()
.with_cached_prestate(cache)
.with_bundle_update()
.build();
let mut evm = revm::new();
evm.database(&mut state);
evm.env = env.clone();

let mut traces = vec![];
let inspector = TraceInspector {
traces: &mut traces,
};

let result = evm.inspect_commit(inspector);
let mut success = false;
let mut output = Bytes::new();
if let Ok(result) = result {
success = result.is_success();
if let Some(o) = result.output() {
output = o.to_owned();
}
}

if pprint {
traces.iter().for_each(|trace| {
trace.pprint();
});
println!(
"ID: {} {} OUTPUT: {}",
index,
if success { "✅" } else { "❌" },
output
);
}

traces.iter().enumerate().skip(1).for_each(|(idx, t)| {
let revm_stack = t.stack.data().clone();
let cuevm_stack = test.traces[idx - 1].stack.data.clone();
compare_stack(&test_json, idx, revm_stack, cuevm_stack).unwrap();
});
}
}

Ok(())
}

fn compare_stack(
test_json: &str,
idx: usize,
expected: Vec<U256>,
actual: Vec<Bytes>,
) -> Result<()> {
macro_rules! err {
() => {
eprintln!("Expected: {:?}", &expected);
eprintln!("Actual: {:?}", &actual);
Err(eyre::eyre!(
"Stack length mismatch at index {} from {}",
idx,
test_json
))?
};
}
if expected.len() != actual.len() {
err!();
}
let actual: Vec<U256> = actual
.iter()
.map(|x| {
let mut padded_array = [0u8; 32];
let xs = x.iter().cloned().collect::<Vec<u8>>();
let len = xs.len();
padded_array[32 - len..].copy_from_slice(&xs);
U256::from_be_bytes(padded_array)
})
.collect();
if actual != expected {
err!();
}
Ok(())
}
Loading