Files
revive/crates/differential/src/lib.rs
T
Cyrill Leutwiler 89ddfb28c8 CI: ignore the broken test (#116)
Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com>
2024-11-21 12:22:15 +01:00

571 lines
19 KiB
Rust

use core::str;
use std::{
collections::BTreeMap,
io::Write,
path::PathBuf,
process::{Command, Stdio},
str::FromStr,
time::Duration,
};
use alloy_genesis::{Genesis, GenesisAccount};
use alloy_primitives::{hex::ToHexExt, Address, Bytes, B256, U256};
use alloy_serde::storage::deserialize_storage_map;
use serde::{Deserialize, Serialize};
use serde_json::{Deserializer, Value};
use tempfile::{NamedTempFile, TempPath};
pub use self::go_duration::parse_go_duration;
mod go_duration;
const GENESIS_JSON: &str = include_str!("../genesis.json");
const EXECUTABLE_NAME: &str = "evm";
const EXECUTABLE_ARGS: [&str; 8] = [
"--log.format=json",
"run",
"--dump",
"--nomemory=false",
"--noreturndata=false",
"--json",
"--codefile",
"-",
];
const EXECUTABLE_ARGS_BENCH: [&str; 6] = [
"run",
"--bench",
"--nomemory=false",
"--noreturndata=false",
"--codefile",
"-",
];
const GAS_USED_MARKER: &str = "EVM gas used:";
const REVERT_MARKER: &str = " error: ";
/// The geth EVM state dump structure
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct StateDump {
pub root: Bytes,
pub accounts: BTreeMap<Address, Account>,
}
impl From<StateDump> for Genesis {
fn from(value: StateDump) -> Self {
let mut genesis: Genesis = serde_json::from_str(GENESIS_JSON).unwrap();
genesis.alloc = value
.accounts
.iter()
.map(|(address, account)| (*address, account.clone().into()))
.collect();
genesis
}
}
/// The geth EVM state dump account structure
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Account {
pub balance: U256,
pub nonce: u64,
pub code: Option<Bytes>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_storage_map"
)]
pub storage: Option<BTreeMap<B256, B256>>,
pub key: Option<B256>,
}
impl From<Account> for GenesisAccount {
fn from(value: Account) -> Self {
GenesisAccount {
balance: value.balance,
nonce: Some(value.nonce),
code: value.code,
storage: value.storage,
private_key: value.key,
}
}
}
/// Contains the output from geth `emv` invocations
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct EvmOutput {
pub output: Bytes,
#[serde(rename = "gasUsed")]
pub gas_used: U256,
pub error: Option<String>,
}
impl EvmOutput {
/// Return if there was no error found.
///
/// Panics if the gas used is zero as this indicates nothing was run.
pub fn run_success(&self) -> bool {
assert_ne!(self.gas_used, U256::ZERO, "nothing was executed: {self:?}");
self.error.is_none()
}
}
/// Contains the full log from geth `emv` invocations
#[derive(Clone, Debug)]
pub struct EvmLog {
pub account_deployed: Option<Address>,
pub output: EvmOutput,
pub state_dump: StateDump,
pub stderr: String,
}
impl EvmLog {
pub const EXECUTION_TIME_MARKER: &'static str = "execution time:";
/// Parse the reported execution time from stderr (requires --bench)
pub fn execution_time(&self) -> Result<Duration, String> {
for line in self.stderr.lines() {
if let Some(value) = line.split("execution time:").nth(1) {
return parse_go_duration(value.trim());
}
}
Err(format!(
"execution time marker '{}' not found in raw EVM log",
Self::EXECUTION_TIME_MARKER
))
}
fn parse_gas_used_from_bench(&mut self) {
for line in self.stderr.lines() {
if let Some(gas_line) = line.split(GAS_USED_MARKER).nth(1) {
let gas_used = gas_line.trim().parse::<u64>().unwrap_or_else(|error| {
panic!("invalid output '{gas_line}' for gas used: {error}")
});
self.output.gas_used = U256::from(gas_used);
}
}
}
}
impl From<&str> for EvmLog {
fn from(value: &str) -> Self {
let mut output = None;
let mut state_dump = None;
for value in Deserializer::from_str(value).into_iter::<Value>() {
let Ok(value) = value else { continue };
if let Ok(value @ EvmOutput { .. }) = serde_json::from_value(value.clone()) {
output = Some(value);
continue;
}
if let Ok(value @ StateDump { .. }) = serde_json::from_value(value) {
state_dump = Some(value);
}
}
if let (Some(output), Some(state_dump)) = (output, state_dump) {
return Self {
account_deployed: None,
output,
state_dump,
stderr: value.into(),
};
}
EvmLog {
account_deployed: None,
output: EvmOutput {
error: value.find(REVERT_MARKER).map(|_| REVERT_MARKER.to_string()),
..Default::default()
},
state_dump: Default::default(),
stderr: Default::default(),
}
}
}
/// Builder for running contracts in geth `evm`
pub struct Evm {
genesis_json: Option<String>,
genesis_path: Option<PathBuf>,
code: Option<Vec<u8>>,
input: Option<Bytes>,
receiver: Option<String>,
sender: String,
value: Option<u128>,
gas: Option<u64>,
create: bool,
bench: bool,
}
impl Default for Evm {
fn default() -> Self {
Self {
genesis_json: Some(GENESIS_JSON.to_string()),
genesis_path: None,
code: None,
input: None,
receiver: None,
sender: Address::default().encode_hex(),
value: None,
gas: None,
create: false,
bench: false,
}
}
}
impl Evm {
/// Create a new EVM with the given `genesis`
pub fn from_genesis(genesis: Genesis) -> Self {
Self::default().genesis_json(genesis)
}
/// Run the `code`
pub fn code_blob(self, blob: Vec<u8>) -> Self {
Self {
code: Some(blob),
..self
}
}
/// Set the calldata
pub fn input(self, bytes: Bytes) -> Self {
Self {
input: (!bytes.is_empty()).then_some(bytes),
..self
}
}
/// Set the create flag
pub fn deploy(self, enable: bool) -> Self {
Self {
create: enable,
..self
}
}
/// Set the transferred value
pub fn value(self, value: u128) -> Self {
Self {
value: Some(value),
..self
}
}
/// Set the gas limit
pub fn gas(self, limit: u64) -> Self {
Self {
gas: Some(limit),
..self
}
}
/// Provide the prestate genesis configuration
pub fn genesis_json(self, genesis: Genesis) -> Self {
let genesis_json = serde_json::to_string(&genesis).expect("state dump should be valid");
// TODO: Investigate
let genesis_json = genesis_json.replace("\"0x0\"", "0").into();
Self {
genesis_json,
genesis_path: None,
..self
}
}
/// Provide a path to the genesis file to be used
pub fn genesis_path(self, path: PathBuf) -> Self {
Self {
genesis_path: Some(path),
genesis_json: None,
..self
}
}
/// Set the callee address
pub fn receiver(self, address: Address) -> Self {
Self {
receiver: Some(address.encode_hex()),
..self
}
}
/// Set the caller address
pub fn sender(self, address: Address) -> Self {
Self {
sender: address.encode_hex(),
..self
}
}
/// Run as a benchmark
pub fn bench(self, flag: bool) -> Self {
Self {
bench: flag,
..self
}
}
/// Calculate the address of the contract account this deploy call would create
pub fn expect_account_created(&self) -> Address {
assert!(self.create, "expected a deploy call");
let sender = Address::from_str(&self.sender).expect("sender address should be valid");
let genesis: Genesis = match (self.genesis_json.as_ref(), self.genesis_path.as_ref()) {
(Some(json), None) => serde_json::from_str(json).unwrap(),
(None, Some(path)) => {
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap()
}
_ => panic!("provided a genesis json and a genesis json path"),
};
let nonce = genesis
.alloc
.get(&sender)
.map(|account| account.nonce.unwrap_or(0))
.unwrap_or(0);
sender.create(nonce)
}
/// Return the path to the genesis file;
/// writes the genesis file into a tmpdir if necessary.
///
/// `TempPath`` will delete on drop, so need to keep it around
fn write_genesis_file(&self, temp_path: &mut Option<TempPath>) -> String {
match (self.genesis_json.as_ref(), self.genesis_path.as_ref()) {
(Some(json), None) => {
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(json.as_bytes()).unwrap();
let path = temp_file.into_temp_path();
*temp_path = Some(path);
temp_path.as_ref().unwrap().display().to_string()
}
(None, Some(path)) => path.display().to_string(),
_ => panic!("provided a genesis json and a genesis json path"),
}
}
/// Run the call in a geth `evm` subprocess.
///
/// Definitively not a hairy plumbing function.
pub fn run(self) -> EvmLog {
let mut temp_path = None;
let genesis_json_path = &self.write_genesis_file(&mut temp_path);
// Static args
let mut command = Command::new(PathBuf::from(EXECUTABLE_NAME));
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if self.bench {
command.args(EXECUTABLE_ARGS_BENCH);
} else {
command.args(EXECUTABLE_ARGS);
};
// Dynamic args
command.args(["--prestate", genesis_json_path]);
command.args(["--sender", &self.sender]);
if let Some(input) = &self.input {
command.args(["--input", hex::encode(input).as_str()]);
}
let account_deployed = if self.create {
command.arg("--create");
self.expect_account_created().into()
} else {
None
};
match (&self.code, &self.receiver) {
(Some(_), None) => {}
(None, Some(address)) => {
command.args(["--receiver", address]);
}
(Some(_), Some(_)) => panic!("code and receiver specified"),
_ => panic!("no code file or receiver specified"),
}
if let Some(gas) = self.gas {
command.args(["--gas", &format!("{gas}")]);
}
if let Some(value) = self.value {
command.args(["--value", &format!("{value}")]);
}
// Run the evm subprocess and assert success return value
let process = command.spawn().unwrap_or_else(|error| {
panic!("{EXECUTABLE_NAME} subprocess spawning error: {error:?}")
});
let buf = vec![];
process
.stdin
.as_ref()
.unwrap_or_else(|| panic!("{EXECUTABLE_NAME} stdin getting error"))
.write_all(self.code.as_ref().unwrap_or(&buf))
.unwrap_or_else(|err| panic!("{EXECUTABLE_NAME} stdin writing error: {err:?}"));
let output = process
.wait_with_output()
.unwrap_or_else(|err| panic!("{EXECUTABLE_NAME} subprocess output error: {err}"));
assert!(
output.status.success(),
"{EXECUTABLE_NAME} command failed: {output:?}",
);
drop(temp_path);
let stdout = str::from_utf8(output.stdout.as_slice())
.unwrap_or_else(|err| panic!("{EXECUTABLE_NAME} stdout failed to parse: {err}"));
let stderr = str::from_utf8(output.stderr.as_slice())
.unwrap_or_else(|err| panic!("{EXECUTABLE_NAME} stderr failed to parse: {err}"));
let mut log: EvmLog = stdout.into();
log.stderr = stderr.into();
if self.bench {
log.parse_gas_used_from_bench();
}
// Set the deployed account
log.account_deployed = account_deployed;
log
}
}
#[cfg(test)]
mod tests {
use std::{str::FromStr, time::Duration};
use alloy_genesis::Genesis;
use alloy_primitives::{Bytes, B256, U256};
use crate::{Evm, EvmLog, EvmOutput, StateDump};
const OUTPUT_JSON_OK: &str = r#"{"output":"0000000000000000000000000000000000000000000000000000000000000000","gasUsed":"0x11d"}"#;
const OUTPUT_JSON_REVERTED: &str =
r#"{"output":"","gasUsed":"0x2d","error":"execution reverted"}"#;
const STATE_DUMP: &str = r#"
{
"root": "eb5d51177cb9049b848ea92f87f9a3f00abfb683d0866c2eddecc5692ad27f86",
"accounts": {
"0x1f2a98889594024BFfdA3311CbE69728d392C06D": {
"balance": "0",
"nonce": 1,
"root": "0x63cfcda8d81a8b1840b1b9722c37f929a4037e53ad1ce6abdef31c0c8bac1f61",
"codeHash": "0xa6e0062c5ba829446695f179b97702a75f7d354e33445d2e928ed00e1a39e88f",
"code": "0x608060405260043610610028575f3560e01c80633fa4f2451461002c578063b144adfb1461004a575b5f80fd5b610034610086565b60405161004191906100c5565b60405180910390f35b348015610055575f80fd5b50610070600480360381019061006b919061013c565b61008d565b60405161007d91906100c5565b60405180910390f35b5f34905090565b5f8173ffffffffffffffffffffffffffffffffffffffff16319050919050565b5f819050919050565b6100bf816100ad565b82525050565b5f6020820190506100d85f8301846100b6565b92915050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61010b826100e2565b9050919050565b61011b81610101565b8114610125575f80fd5b50565b5f8135905061013681610112565b92915050565b5f60208284031215610151576101506100de565b5b5f61015e84828501610128565b9150509291505056fea2646970667358221220a2109c2f05a629fff4640e9f0cf12a698bbea9b0858a4029901e88bf5d1c926964736f6c63430008190033",
"storage": {
"0x0000000000000000000000000000000000000000000000000000000000000000": "02"
},
"address": "0x1f2a98889594024bffda3311cbe69728d392c06d",
"key": "0xcbeeb4463624bc2f332dcfe2b479eddb1c380ec862ee63d9f31b31b854fb7c61"
}
}
}"#;
const EVM_BIN_FIXTURE: &str = "6080604052348015600e575f80fd5b506040516101403803806101408339818101604052810190602e9190607f565b805f806101000a81548160ff0219169083151502179055505060a5565b5f80fd5b5f8115159050919050565b606181604f565b8114606a575f80fd5b50565b5f81519050607981605a565b92915050565b5f602082840312156091576090604b565b5b5f609c84828501606d565b91505092915050565b608f806100b15f395ff3fe6080604052348015600e575f80fd5b50600436106026575f3560e01c8063cde4efa914602a575b5f80fd5b60306032565b005b5f8054906101000a900460ff16155f806101000a81548160ff02191690831515021790555056fea264697066735822122046c92dd2fd612b1ed93d184dad4c49f61c44690722c4a6c7c746ebeb0aadeb4a64736f6c63430008190033";
const EVM_BIN_RUNTIME_FIXTURE: &str = "6080604052348015600e575f80fd5b50600436106026575f3560e01c8063cde4efa914602a575b5f80fd5b60306032565b005b5f8054906101000a900460ff16155f806101000a81548160ff02191690831515021790555056fea264697066735822122046c92dd2fd612b1ed93d184dad4c49f61c44690722c4a6c7c746ebeb0aadeb4a64736f6c63430008190033";
const EVM_BIN_FIXTURE_INPUT: &str =
"0000000000000000000000000000000000000000000000000000000000000001";
const EVM_BIN_RUNTIME_FIXTURE_INPUT: &str = "cde4efa9";
const STDERR_BENCH_OK: &str = r#"EVM gas used: 560071
execution time: 1.460881ms
allocations: 29
allocated bytes: 2558
"#;
const STDERR_BENCH_REVERT: &str = r#"EVM gas used: 69
execution time: 10.11µs
allocations: 43
allocated bytes: 3711"#;
const STDOUT_BENCH_REVERT: &str = r#" error: execution reverted"#;
#[test]
fn parse_evm_output_ok() {
serde_json::from_str::<EvmOutput>(OUTPUT_JSON_OK).unwrap();
}
#[test]
fn parse_evm_output_revert() {
serde_json::from_str::<EvmOutput>(OUTPUT_JSON_REVERTED).unwrap();
}
#[test]
fn parse_evm_output_bench_ok() {
let mut log = EvmLog::from("");
log.stderr = STDERR_BENCH_OK.into();
log.parse_gas_used_from_bench();
assert!(log.output.run_success());
assert_eq!(log.execution_time().unwrap(), Duration::from_nanos(1460881));
}
#[test]
fn parse_evm_output_bench_revert() {
let mut log = EvmLog::from(STDOUT_BENCH_REVERT);
log.stderr = STDERR_BENCH_REVERT.into();
log.parse_gas_used_from_bench();
assert!(!log.output.run_success());
}
#[test]
fn parse_state_dump() {
serde_json::from_str::<StateDump>(STATE_DUMP).unwrap();
}
#[test]
fn evm_log_from_str() {
let log = format!("{OUTPUT_JSON_OK}\n{STATE_DUMP}");
let _ = EvmLog::from(log.as_str());
}
#[test]
fn generate_genesis() {
let log = format!("{OUTPUT_JSON_OK}\n{STATE_DUMP}");
let log = EvmLog::from(log.as_str());
let mut genesis: Genesis = log.state_dump.into();
let storage = genesis
.alloc
.pop_first()
.expect("should have one account in genesis")
.1
.storage
.expect("genesis account should have storage");
let storage_value = storage
.get(&B256::ZERO)
.expect("genesis account should have key 0 occupied");
assert_eq!(*storage_value, B256::from(U256::from(2)));
}
#[test]
fn flipper() {
let log_runtime = Evm::default()
.code_blob(EVM_BIN_RUNTIME_FIXTURE.as_bytes().to_vec())
.input(Bytes::from_str(EVM_BIN_RUNTIME_FIXTURE_INPUT).unwrap())
.run();
assert!(log_runtime.output.run_success());
}
#[test]
fn prestate() {
let log_deploy = Evm::default()
.code_blob(EVM_BIN_FIXTURE.as_bytes().to_vec())
.input(Bytes::from_str(EVM_BIN_FIXTURE_INPUT).unwrap())
.deploy(true)
.run();
assert!(log_deploy.output.run_success());
let address = log_deploy.account_deployed.unwrap();
let genesis: Genesis = log_deploy.state_dump.into();
let log_runtime = Evm::default()
.genesis_json(genesis)
.receiver(address)
.input(Bytes::from_str(EVM_BIN_RUNTIME_FIXTURE_INPUT).unwrap())
.run();
assert!(log_runtime.output.run_success(), "{:?}", log_runtime.output);
}
#[test]
#[ignore] // https://github.com/ethereum/go-ethereum/issues/30778
fn bench_flipper() {
let log_runtime = Evm::default()
.code_blob(EVM_BIN_RUNTIME_FIXTURE.as_bytes().to_vec())
.input(Bytes::from_str(EVM_BIN_RUNTIME_FIXTURE_INPUT).unwrap())
.bench(true)
.run();
assert!(log_runtime.output.run_success());
assert!(log_runtime.execution_time().unwrap() > Duration::from_nanos(0));
}
}