diff --git a/Cargo.lock b/Cargo.lock index 89cf7413d..f9063a4e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2477,6 +2477,22 @@ dependencies = [ "getopts 0.2.15 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "pwasm-run-test" +version = "0.1.0" +dependencies = [ + "clap 2.26.2 (registry+https://github.com/rust-lang/crates.io-index)", + "ethcore-bigint 0.2.1", + "ethcore-logger 1.9.0", + "ethjson 0.1.0", + "rustc-hex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "vm 0.1.0", + "wasm 0.1.0", +] + [[package]] name = "quasi" version = "0.32.0" diff --git a/Cargo.toml b/Cargo.toml index 36d8255fc..17250c32c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,4 +116,4 @@ lto = false panic = "abort" [workspace] -members = ["ethstore/cli", "ethkey/cli", "evmbin", "whisper", "chainspec", "dapps/js-glue"] +members = ["ethstore/cli", "ethkey/cli", "evmbin", "whisper", "chainspec", "dapps/js-glue", "ethcore/wasm/run"] diff --git a/ethcore/wasm/run/Cargo.toml b/ethcore/wasm/run/Cargo.toml new file mode 100644 index 000000000..a739d72e2 --- /dev/null +++ b/ethcore/wasm/run/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "pwasm-run-test" +version = "0.1.0" +authors = ["Parity Technologies "] + +[dependencies] +serde = "1" +serde_json = "1" +serde_derive = "1" +ethcore-bigint = { path = "../../../util/bigint" } +ethjson = { path = "../../../json" } +vm = { path = "../../vm" } +wasm = { path = "../" } +clap = "2.24" +ethcore-logger = { path = "../../../logger" } +rustc-hex = "1" + +[features] +default = ["ethcore-bigint/std"] \ No newline at end of file diff --git a/ethcore/wasm/run/res/sample-fixture.json b/ethcore/wasm/run/res/sample-fixture.json new file mode 100644 index 000000000..4f53eaa38 --- /dev/null +++ b/ethcore/wasm/run/res/sample-fixture.json @@ -0,0 +1,31 @@ +[ + { + "caption": "Sample test", + "wasm": "./res/sample1.wasm", + "address": "0x1000000000000000000000000000000000000001", + "sender": "0x1000000000000000000000000000000000000002", + "value": "0x0000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": 100000, + "payload": "0x", + "asserts": [ + { "Return": "0x01" }, + { "UsedGas": 17 }, + { "HasCall": { "codeAddress": "0x1000000000000000000000000000000000000002" }}, + { "HasStorage": + { + "key": "0x0000000000000000000000000000000000000000000000000000000000000001", + "value": "0x0000000000000000000000000000000000000000000000000000000000000002" + } + } + ] + }, + { + "caption": "Keccak test", + "wasm": "./res/sample2.wasm", + "payload": "0x736f6d657468696e67", + "gasLimit": 100000, + "asserts": [ + { "Return": "0x68371d7e884c168ae2022c82bd837d51837718a7f7dfb7aa3f753074a35e1d87" } + ] + } +] \ No newline at end of file diff --git a/ethcore/wasm/run/res/sample1.wasm b/ethcore/wasm/run/res/sample1.wasm new file mode 100644 index 000000000..6ea0c58cc Binary files /dev/null and b/ethcore/wasm/run/res/sample1.wasm differ diff --git a/ethcore/wasm/run/res/sample2.wasm b/ethcore/wasm/run/res/sample2.wasm new file mode 100644 index 000000000..baf9d17aa Binary files /dev/null and b/ethcore/wasm/run/res/sample2.wasm differ diff --git a/ethcore/wasm/run/src/fixture.rs b/ethcore/wasm/run/src/fixture.rs new file mode 100644 index 000000000..0fd3a08e1 --- /dev/null +++ b/ethcore/wasm/run/src/fixture.rs @@ -0,0 +1,49 @@ +use std::borrow::Cow; +use ethjson::uint::Uint; +use ethjson::hash::{Address, H256}; +use ethjson::bytes::Bytes; + +#[derive(Deserialize)] +pub struct Fixture { + pub caption: Cow<'static, String>, + #[serde(rename="wasm")] + pub wasm_file: Cow<'static, String>, + pub address: Option
, + pub sender: Option
, + pub value: Option, + #[serde(rename="gasLimit")] + pub gas_limit: Option, + pub payload: Option, + pub storage: Option>, + pub asserts: Vec, +} + +#[derive(Deserialize, Debug)] +pub struct StorageEntry { + pub key: Uint, + pub value: Uint, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct CallLocator { + pub sender: Option
, + pub receiver: Option
, + pub value: Option, + pub data: Option, + #[serde(rename="codeAddress")] + pub code_address: Option
, +} + +#[derive(Deserialize, Debug)] +pub struct StorageAssert { + pub key: H256, + pub value: H256, +} + +#[derive(Deserialize, Debug)] +pub enum Assert { + HasCall(CallLocator), + HasStorage(StorageAssert), + UsedGas(u64), + Return(Bytes), +} \ No newline at end of file diff --git a/ethcore/wasm/run/src/main.rs b/ethcore/wasm/run/src/main.rs new file mode 100644 index 000000000..1cb38cdf9 --- /dev/null +++ b/ethcore/wasm/run/src/main.rs @@ -0,0 +1,46 @@ +extern crate serde; +extern crate serde_json; +#[macro_use] extern crate serde_derive; +extern crate ethcore_bigint; +extern crate ethjson; +extern crate wasm; +extern crate vm; +extern crate clap; +extern crate ethcore_logger; +extern crate rustc_hex; + +mod fixture; +mod runner; + +use fixture::Fixture; +use clap::{App, Arg}; +use std::fs; + +fn main() { + ::ethcore_logger::init_log(); + + let matches = App::new("pwasm-run-test") + .arg(Arg::with_name("target") + .index(1) + .required(true) + .multiple(true) + .help("JSON fixture")) + .get_matches(); + + let mut exit_code = 0; + + for target in matches.values_of("target").expect("No target parameter") { + let mut f = fs::File::open(target).expect("Failed to open file"); + let fixtures: Vec = serde_json::from_reader(&mut f).expect("Failed to deserialize json"); + + for fixture in fixtures.into_iter() { + let fails = runner::run_fixture(&fixture); + for fail in fails.iter() { + exit_code = 1; + println!("Failed assert in test \"{}\" ('{}'): {}", fixture.caption.as_ref(), target, fail); + } + } + } + + std::process::exit(exit_code); +} \ No newline at end of file diff --git a/ethcore/wasm/run/src/runner.rs b/ethcore/wasm/run/src/runner.rs new file mode 100644 index 000000000..edfc1ee4b --- /dev/null +++ b/ethcore/wasm/run/src/runner.rs @@ -0,0 +1,204 @@ +use fixture::{Fixture, Assert, CallLocator}; +use wasm::WasmInterpreter; +use vm::{self, Vm, GasLeft, ActionParams, ActionValue}; +use vm::tests::FakeExt; +use std::io::{self, Read}; +use std::{fs, path, fmt}; +use std::sync::Arc; +use ethcore_bigint::prelude::{U256, H256, H160}; +use rustc_hex::ToHex; + +fn load_code>(p: P) -> io::Result> { + let mut result = Vec::new(); + let mut f = fs::File::open(p)?; + f.read_to_end(&mut result)?; + Ok(result) +} + +fn wasm_interpreter() -> WasmInterpreter { + WasmInterpreter::new().expect("wasm interpreter to create without errors") +} + +#[derive(Debug)] +pub enum Fail { + Return { expected: Vec, actual: Vec }, + UsedGas { expected: u64, actual: u64 }, + Runtime(String), + Load(io::Error), + NoCall(CallLocator), + StorageMismatch { key: H256, expected: H256, actual: Option }, +} + +impl Fail { + fn runtime(err: vm::Error) -> Vec { + vec![Fail::Runtime(format!("{}", err))] + } + + fn load(err: io::Error) -> Vec { + vec![Fail::Load(err)] + } +} + +impl fmt::Display for Fail { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::Fail::*; + match *self { + Return { ref expected, ref actual } => + write!( + f, + "Expected to return result: 0x{} ({} bytes), but got 0x{} ({} bytes)", + expected.to_hex(), + expected.len(), + actual.to_hex(), + actual.len() + ), + UsedGas { expected, actual } => + write!(f, "Expected to use gas: {}, but got actual gas used: {}", expected, actual), + + Runtime(ref s) => + write!(f, "WASM Runtime error: {}", s), + + Load(ref e) => + write!(f, "Load i/o error: {}", e), + + NoCall(ref call) => + write!(f, "Call not found: {:?}", call), + + StorageMismatch { ref key, ref expected, actual: Some(ref actual)} => + write!( + f, + "Storage key {} value mismatch, expected {}, got: {}", + key.as_ref().to_vec().to_hex(), + expected.as_ref().to_vec().to_hex(), + actual.as_ref().to_vec().to_hex(), + ), + + StorageMismatch { ref key, ref expected, actual: None} => + write!( + f, + "No expected storage value for key {} found, expected {}", + key.as_ref().to_vec().to_hex(), + expected.as_ref().to_vec().to_hex(), + ), + } + } +} + +pub fn run_fixture(fixture: &Fixture) -> Vec { + let mut params = ActionParams::default(); + + params.code = Some(Arc::new( + match load_code(fixture.wasm_file.as_ref()) { + Ok(code) => code, + Err(e) => { return Fail::load(e); }, + } + )); + + if let Some(ref address) = fixture.address { + params.address = address.clone().into(); + } + + if let Some(gas_limit) = fixture.gas_limit { + params.gas = U256::from(gas_limit); + } + + if let Some(ref data) = fixture.payload { + params.data = Some(data.clone().into()) + } + + if let Some(value) = fixture.value { + params.value = ActionValue::Transfer(value.clone().into()) + } + + let mut ext = FakeExt::new(); + if let Some(ref storage) = fixture.storage { + for storage_entry in storage.iter() { + let key: U256 = storage_entry.key.into(); + let val: U256 = storage_entry.value.into(); + ext.store.insert(key.into(), val.into()); + } + } + + let mut interpreter = wasm_interpreter(); + + let interpreter_return = match interpreter.exec(params, &mut ext) { + Ok(ret) => ret, + Err(e) => { return Fail::runtime(e); } + }; + let (gas_left, result) = match interpreter_return { + GasLeft::Known(gas) => { (gas, Vec::new()) }, + GasLeft::NeedsReturn { gas_left: gas, data: result, apply_state: _apply } => (gas, result.to_vec()), + }; + + let mut fails = Vec::new(); + + for assert in fixture.asserts.iter() { + match *assert { + Assert::Return(ref data) => { + if &data[..] != &result[..] { + fails.push(Fail::Return { expected: (&data[..]).to_vec(), actual: (&result[..]).to_vec() }) + } + }, + Assert::UsedGas(gas) => { + let used_gas = fixture.gas_limit.unwrap_or(0) - gas_left.low_u64(); + if gas != used_gas { + fails.push(Fail::UsedGas { expected: gas, actual: used_gas }); + } + }, + Assert::HasCall(ref locator) => { + let mut found = false; + + for fake_call in ext.calls.iter() { + let mut match_ = true; + if let Some(ref data) = locator.data { + if data.as_ref() != &fake_call.data[..] { match_ = false; } + } + + if let Some(ref code_addr) = locator.code_address { + if fake_call.code_address.unwrap_or(H160::zero()) != code_addr.clone().into() { match_ = false } + } + + if let Some(ref sender) = locator.sender { + if fake_call.sender_address.unwrap_or(H160::zero()) != sender.clone().into() { match_ = false } + } + + if let Some(ref receiver) = locator.receiver { + if fake_call.receive_address.unwrap_or(H160::zero()) != receiver.clone().into() { match_ = false } + } + + if match_ { + found = true; + break; + } + } + + if !found { + fails.push(Fail::NoCall(locator.clone())) + } + }, + Assert::HasStorage(ref storage_entry) => { + let expected_storage_key: H256 = storage_entry.key.clone().into(); + let expected_storage_value: H256 = storage_entry.value.clone().into(); + let val = ext.store.get(&expected_storage_key); + + if let Some(val) = val { + if val != &expected_storage_value { + fails.push(Fail::StorageMismatch { + key: expected_storage_key, + expected: expected_storage_value, + actual: Some(val.clone()) + }) + } + } else { + fails.push(Fail::StorageMismatch { + key: expected_storage_key, + expected: expected_storage_value, + actual: None, + }) + } + + }, + } + } + fails +} \ No newline at end of file