From 5c5f8764a45b7974df556c762f026a8cbc6b9ebd Mon Sep 17 00:00:00 2001 From: Dave Bryson Date: Thu, 3 Apr 2025 13:53:53 -0500 Subject: [PATCH 01/17] fixed signed int issue --- pyproject.toml | 2 +- src/pyevm.rs | 26 +++++++++++++++----------- tests/conftest.py | 7 +++++++ tests/fixtures/SignedInts.json | 1 + tests/test_datatypes.py | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 tests/fixtures/SignedInts.json create mode 100644 tests/test_datatypes.py diff --git a/pyproject.toml b/pyproject.toml index b0b3964..8d9d0f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ sdist-include = ["LICENSE", "README.md"] [project] name = "simular-evm" -version = "0.2.6" +version = "0.2.7" requires-python = ">=3.10" authors = [ { name = "Dave Bryson", email = "davebryson@users.noreply.github.com" }, diff --git a/src/pyevm.rs b/src/pyevm.rs index a789541..ef50e06 100644 --- a/src/pyevm.rs +++ b/src/pyevm.rs @@ -230,23 +230,27 @@ fn walk_list(values: Vec, py: Python<'_>) -> PyObject { .into_py(py) } +// Convert DynSolValue signed and unsigned ints to Python. +// these values originate from Solidity types. +// Goal is to support u/i8 -> u/i256 +fn convert_ints(bytes: [u8; 32], is_signed: bool, py: Python<'_>) -> PyObject { + let signed = if is_signed { 1 } else { 0 }; + unsafe { + let obj = + ffi::_PyLong_FromByteArray(bytes.as_ptr().cast::(), bytes.len(), 1, signed); + PyObject::from_owned_ptr(py, obj) + } +} + +// Transform DynSolValues to Python types. fn base_exctract(dv: DynSolValue, py: Python<'_>) -> PyObject { match dv { DynSolValue::Address(a) => format!("{a:?}").into_py(py), DynSolValue::Bool(a) => a.into_py(py), DynSolValue::String(a) => a.into_py(py), DynSolValue::Tuple(a) => walk_list(a, py), - DynSolValue::Int(a, _) => a.as_i64().into_py(py), - DynSolValue::Uint(a, _) => { - let bytes = a.as_le_bytes(); - // put on your life jacket we're entering 'unsafe' waters - // ...adapted from ruint pyo3 extension - unsafe { - let obj = - ffi::_PyLong_FromByteArray(bytes.as_ptr().cast::(), bytes.len(), 1, 0); - PyObject::from_owned_ptr(py, obj) - } - } + DynSolValue::Int(a, _) => convert_ints(a.to_le_bytes::<32>(), true, py), + DynSolValue::Uint(a, _) => convert_ints(a.to_le_bytes::<32>(), false, py), DynSolValue::Bytes(a) => a.into_py(py), DynSolValue::FixedBytes(a, _) => a.to_vec().into_py(py), DynSolValue::Array(a) => walk_list(a, py), diff --git a/tests/conftest.py b/tests/conftest.py index 2d03841..503d820 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -48,3 +48,10 @@ def block_meta_json(): with open(f"{PATH}/./fixtures/BlockMeta.json") as f: rawabi = f.read() return rawabi + + +@pytest.fixture +def signed_ints_json(): + with open(f"{PATH}/./fixtures/SignedInts.json") as f: + rawabi = f.read() + return rawabi diff --git a/tests/fixtures/SignedInts.json b/tests/fixtures/SignedInts.json new file mode 100644 index 0000000..ed75224 --- /dev/null +++ b/tests/fixtures/SignedInts.json @@ -0,0 +1 @@ +{"abi":[{"type":"function","name":"in_and_out","inputs":[{"name":"value","type":"int256","internalType":"int256"}],"outputs":[{"name":"","type":"int256","internalType":"int256"}],"stateMutability":"pure"}],"bytecode":{"object":"0x608060405234801561000f575f80fd5b5061010f8061001d5f395ff3fe6080604052348015600e575f80fd5b50600436106026575f3560e01c80636ee2e03314602a575b5f80fd5b60406004803603810190603c9190608f565b6054565b604051604b919060c2565b60405180910390f35b5f819050919050565b5f80fd5b5f819050919050565b6071816061565b8114607a575f80fd5b50565b5f81359050608981606a565b92915050565b5f6020828403121560a15760a0605d565b5b5f60ac84828501607d565b91505092915050565b60bc816061565b82525050565b5f60208201905060d35f83018460b5565b9291505056fea2646970667358221220689ef493bdd4f6c5f20643b97ad191ce1462d8cd83851a0c0ce36898ea4ecacf64736f6c63430008150033","sourceMap":"65:176:0:-:0;;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x6080604052348015600e575f80fd5b50600436106026575f3560e01c80636ee2e03314602a575b5f80fd5b60406004803603810190603c9190608f565b6054565b604051604b919060c2565b60405180910390f35b5f819050919050565b5f80fd5b5f819050919050565b6071816061565b8114607a575f80fd5b50565b5f81359050608981606a565b92915050565b5f6020828403121560a15760a0605d565b5b5f60ac84828501607d565b91505092915050565b60bc816061565b82525050565b5f60208201905060d35f83018460b5565b9291505056fea2646970667358221220689ef493bdd4f6c5f20643b97ad191ce1462d8cd83851a0c0ce36898ea4ecacf64736f6c63430008150033","sourceMap":"65:176:0:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;147:92;;;;;;;;;;;;;:::i;:::-;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;202:6;227:5;220:12;;147:92;;;:::o;88:117:1:-;197:1;194;187:12;334:76;370:7;399:5;388:16;;334:76;;;:::o;416:120::-;488:23;505:5;488:23;:::i;:::-;481:5;478:34;468:62;;526:1;523;516:12;468:62;416:120;:::o;542:137::-;587:5;625:6;612:20;603:29;;641:32;667:5;641:32;:::i;:::-;542:137;;;;:::o;685:327::-;743:6;792:2;780:9;771:7;767:23;763:32;760:119;;;798:79;;:::i;:::-;760:119;918:1;943:52;987:7;978:6;967:9;963:22;943:52;:::i;:::-;933:62;;889:116;685:327;;;;:::o;1018:115::-;1103:23;1120:5;1103:23;:::i;:::-;1098:3;1091:36;1018:115;;:::o;1139:218::-;1230:4;1268:2;1257:9;1253:18;1245:26;;1281:69;1347:1;1336:9;1332:17;1323:6;1281:69;:::i;:::-;1139:218;;;;:::o","linkReferences":{}},"methodIdentifiers":{"in_and_out(int256)":"6ee2e033"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.21+commit.d9974bed\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"int256\",\"name\":\"value\",\"type\":\"int256\"}],\"name\":\"in_and_out\",\"outputs\":[{\"internalType\":\"int256\",\"name\":\"\",\"type\":\"int256\"}],\"stateMutability\":\"pure\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/SignedInts.sol\":\"SignedInts\"},\"evmVersion\":\"shanghai\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":ds-test/=lib/forge-std/lib/ds-test/src/\",\":forge-std/=lib/forge-std/src/\",\":solmate/=lib/solmate/src/\"]},\"sources\":{\"src/SignedInts.sol\":{\"keccak256\":\"0xbf0381daedc8cd8aa7dde82cb78c1cdf221e8eac0a2dd0adc0689804d6406c59\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://a5e364031357a11f6676369eb79544b7f6c8e7c49809cd21069e90babec46f43\",\"dweb:/ipfs/QmWQUjM1w1JNPTdH1hduEXFB8deFso8Aqa8S9kQ1Wvq7u2\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.21+commit.d9974bed"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"int256","name":"value","type":"int256"}],"stateMutability":"pure","type":"function","name":"in_and_out","outputs":[{"internalType":"int256","name":"","type":"int256"}]}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["ds-test/=lib/forge-std/lib/ds-test/src/","forge-std/=lib/forge-std/src/","solmate/=lib/solmate/src/"],"optimizer":{"enabled":false,"runs":200},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"src/SignedInts.sol":"SignedInts"},"evmVersion":"shanghai","libraries":{}},"sources":{"src/SignedInts.sol":{"keccak256":"0xbf0381daedc8cd8aa7dde82cb78c1cdf221e8eac0a2dd0adc0689804d6406c59","urls":["bzz-raw://a5e364031357a11f6676369eb79544b7f6c8e7c49809cd21069e90babec46f43","dweb:/ipfs/QmWQUjM1w1JNPTdH1hduEXFB8deFso8Aqa8S9kQ1Wvq7u2"],"license":"Apache-2.0"}},"version":1},"id":0} \ No newline at end of file diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py new file mode 100644 index 0000000..a0d8288 --- /dev/null +++ b/tests/test_datatypes.py @@ -0,0 +1,33 @@ +from simular import ( + contract_from_raw_abi, + create_account, +) + + +def test_signed_ints(evm, alice, signed_ints_json): + create_account(evm, alice) + test_contract = contract_from_raw_abi(evm, signed_ints_json) + assert test_contract.deploy(caller=alice) + + # values + cases = [ + (-128, 127), + (-549755813888, 549755813887), + (-2361183241434822606848, 2361183241434822606847), + ( + -43556142965880123323311949751266331066368, + 43556142965880123323311949751266331066367, + ), + ( + -3138550867693340381917894711603833208051177722232017256448, + 3138550867693340381917894711603833208051177722232017256447, + ), + ( + -57896044618658097711785492504343953926634992332820282019728792003956564819968, + 57896044618658097711785492504343953926634992332820282019728792003956564819967, + ), + ] + + for n, p in cases: + assert n == test_contract.in_and_out.call(n) + assert p == test_contract.in_and_out.call(p) From 2f216c97d7b677d0f0d4f76874b56faa930707f7 Mon Sep 17 00:00:00 2001 From: Dave Bryson Date: Thu, 3 Apr 2025 17:12:02 -0500 Subject: [PATCH 02/17] locked to maturin version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8d9d0f5..dc7dd4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = ["eth-abi>=4.1.0", "eth-utils>=2.2.0"] dependencies = [ "pytest>=7.4.0", "black>=23.7.0", - "maturin>=1.1.0", + "maturin==1.6.0", "sphinx>=7.2.6", "sphinx_rtd_theme>=2.0.0", ] From 33c7b4a3bfa89b2cb06aa4adbeba2b2379a9b631 Mon Sep 17 00:00:00 2001 From: Dave Bryson Date: Fri, 4 Apr 2025 10:51:51 -0500 Subject: [PATCH 03/17] refactor, merge --- Cargo.toml | 37 +++- pyproject.toml | 4 +- src/core/abi.rs | 371 +++++++++++++++++++++++++++++++++ src/core/errors.rs | 41 ++++ src/core/evm.rs | 435 +++++++++++++++++++++++++++++++++++++++ src/core/fork.rs | 123 +++++++++++ src/core/fork_backend.rs | 159 ++++++++++++++ src/core/in_memory_db.rs | 153 ++++++++++++++ src/core/mod.rs | 9 + src/core/snapshot.rs | 33 +++ src/core/storage.rs | 280 +++++++++++++++++++++++++ src/lib.rs | 1 + src/pyabi.rs | 2 +- src/pyevm.rs | 2 +- 14 files changed, 1641 insertions(+), 9 deletions(-) create mode 100644 src/core/abi.rs create mode 100644 src/core/errors.rs create mode 100644 src/core/evm.rs create mode 100644 src/core/fork.rs create mode 100644 src/core/fork_backend.rs create mode 100644 src/core/in_memory_db.rs create mode 100644 src/core/mod.rs create mode 100644 src/core/snapshot.rs create mode 100644 src/core/storage.rs diff --git a/Cargo.toml b/Cargo.toml index 88ccbc3..bd9a2ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simular" -version = "0.2.6" +version = "0.3.0" edition = "2021" authors = ["Dave Bryson"] readme = "README.md" @@ -10,6 +10,7 @@ repository = "https://github.com/simular-fi/simular" homepage = "https://github.com/simular-fi/simular" categories = ["cryptography::cryptocurrencies"] keywords = ["revm", "python", "ethereum", "web3", "abm"] +rust-version = "1.76.0" [lib] name = "simular" @@ -17,16 +18,42 @@ crate-type = ["cdylib", "rlib"] [dependencies] +# helpers anyhow = "1.0.81" -alloy-dyn-abi = "0.7.0" -alloy-primitives = { version = "0.7.0", default-features = false } +thiserror = "1.0.58" serde = "1.0.165" serde_json = "1.0.99" - hex = { version = "0.4.3", features = ["serde"] } + +# Alloy +alloy-dyn-abi = "0.7.0" +alloy-json-abi = "0.7.0" +alloy-primitives = { version = "0.7.0", default-features = false } +alloy-sol-types = { version = "0.7.0", features = ["json"] } + +# EVM +revm = { version = "8.0.0", default-features = false, features = [ + "tokio", + "memory_limit", + "optional_eip3607", + "optional_block_gas_limit", + "optional_no_base_fee", + "arbitrary", +] } + + +# required for forkdb +tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros"] } +ethers-core = { version = "2.0.10", default-features = false } +ethers-providers = "2.0.10" +# need this feature in reqwest to deal with potential self-signed certs +reqwest = { version = "0.11.19", features = ["rustls-tls"] } +# resolve build issues on Ubuntu +openssl = { version = "0.10", features = ["vendored"] } + +# Python wrapper pyo3 = { version = "0.20.0", features = ["multiple-pymethods", "anyhow"] } -simular-core = "0.2.5" # using this to allow cargo test on rust code. See: # https://github.com/PyO3/pyo3/issues/340#issuecomment-461514532 diff --git a/pyproject.toml b/pyproject.toml index dc7dd4f..d54540f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ sdist-include = ["LICENSE", "README.md"] [project] name = "simular-evm" -version = "0.2.7" +version = "0.3.0" requires-python = ">=3.10" authors = [ { name = "Dave Bryson", email = "davebryson@users.noreply.github.com" }, @@ -35,7 +35,7 @@ dependencies = ["eth-abi>=4.1.0", "eth-utils>=2.2.0"] dependencies = [ "pytest>=7.4.0", "black>=23.7.0", - "maturin==1.6.0", + "maturin>=1.6.0", "sphinx>=7.2.6", "sphinx_rtd_theme>=2.0.0", ] diff --git a/src/core/abi.rs b/src/core/abi.rs new file mode 100644 index 0000000..67217c9 --- /dev/null +++ b/src/core/abi.rs @@ -0,0 +1,371 @@ +//! +//! Parse contract ABIs to encode, decode contract calls +//! +use alloy_dyn_abi::{DynSolEvent, DynSolType, DynSolValue, Specifier}; +use alloy_json_abi::{ContractObject, Function, JsonAbi, StateMutability}; +use alloy_primitives::{Bytes, Log, LogData}; +use anyhow::{anyhow, bail, Result}; +use std::collections::BTreeMap; + +type EventMap = BTreeMap>; + +/// +/// Wrapper around pre-processed Events to help extract log information. +/// We flatten the structure of `events` in JsonAbi to make it easier to +/// automatically decode Logs from a `transact/simulate`. +/// +/// EventLog contains `DynSolEvent` to be used to decode log information +#[derive(Debug)] +pub struct EventLog { + /// the event name + pub name: String, + /// the decoder resolved from the original event + pub decoder: DynSolEvent, +} + +impl EventLog { + /// Attempt to decode the log and return the event name and extracted values as + /// DynSolValues. + pub fn decode(&self, log: &LogData) -> Option<(String, DynSolValue)> { + if let Ok(r) = self.decoder.decode_log_data(log, true) { + let v = DynSolValue::Tuple([r.indexed, r.body].concat()); + return Some((self.name.clone(), v)); + } + None + } +} + +pub struct ContractAbi { + /// alloy's json abi object + pub abi: JsonAbi, + /// optional contract bytecode + pub bytecode: Option, + /// Contract event information with a log decoder + pub events_logs: Vec, +} + +// walk through the events in JsonAbi to flatten the +// structure and convert to `EventLog`. +fn convert_events(ev: &EventMap) -> Vec { + ev.iter() + .flat_map(|(k, v)| { + v.iter() + .map(|e| EventLog { + name: k.clone(), + decoder: e.resolve().unwrap(), + }) + .collect::>() + }) + .collect::>() +} + +impl ContractAbi { + /// Parse the `abi` and `bytecode` from a compiled contract's json file. + /// Note: `raw` is un-parsed json. + pub fn from_full_json(raw: &str) -> Self { + let co = + serde_json::from_str::(raw).expect("Abi: failed to parse abi to json"); + if co.abi.is_none() { + panic!("Abi: ABI not found in file") + } + if co.bytecode.is_none() { + panic!("Abi: Bytecode not found in file") + } + let abi = co.abi.unwrap(); + let evts = convert_events(&abi.events); + Self { + abi, + bytecode: co.bytecode, + events_logs: evts, + } + } + + /// Parse the `abi` and `bytecode` + /// Note: `raw` is un-parsed json. + pub fn from_abi_bytecode(raw: &str, bytecode: Option>) -> Self { + let abi = serde_json::from_str::(raw).expect("Abi: failed to parse abi"); + let evts = convert_events(&abi.events); + Self { + abi, + bytecode: bytecode.map(Bytes::from), + events_logs: evts, + } + } + + /// Parse an ABI (without bytecode) from a `Vec` of contract function definitions. + /// See [human readable abi](https://docs.ethers.org/v5/api/utils/abi/formats/#abi-formats--human-readable-abi) + pub fn from_human_readable(input: Vec<&str>) -> Self { + let abi = JsonAbi::parse(input).expect("Abi: Invalid solidity function(s) format"); + let evts = convert_events(&abi.events); + Self { + abi, + bytecode: None, + events_logs: evts, + } + } + + /// Extract and decode logs from emitted events + pub fn extract_logs(&self, logs: Vec) -> Vec<(String, DynSolValue)> { + let mut results: Vec<(String, DynSolValue)> = Vec::new(); + for log in logs { + for e in &self.events_logs { + if let Some(p) = e.decode(&log.data) { + results.push(p); + } + } + } + results + } + + /// Is there a function with the given name? + pub fn has_function(&self, name: &str) -> bool { + self.abi.functions.contains_key(name) + } + + /// Does the ABI have a fallback? + pub fn has_fallback(&self) -> bool { + self.abi.fallback.is_some() + } + + /// Does the ABI have a receive? + pub fn has_receive(&self) -> bool { + self.abi.receive.is_some() + } + + /// Return the contract bytecode as a Vec + pub fn bytecode(&self) -> Option> { + self.bytecode.as_ref().map(|b| b.to_vec()) + } + + /// Encode the information needed to create a contract. This will + /// concatenate the contract bytecode with any arguments required by + /// the constructor. Note: `args` is a string of input arguments. See + /// `encode_function` for more information. + pub fn encode_constructor(&self, args: &str) -> Result<(Vec, bool)> { + let bytecode = match self.bytecode() { + Some(b) => b, + _ => bail!("Abi: Missing contract bytecode!"), + }; + + let constructor = match &self.abi.constructor { + Some(c) => c, + _ => return Ok((bytecode, false)), + }; + + let types = constructor + .inputs + .iter() + .map(|i| i.resolve().unwrap()) + .collect::>(); + + let ty = DynSolType::Tuple(types); + let dynavalues = ty.coerce_str(args).map_err(|_| { + anyhow!("Abi: Error coercing the arguments for the constructor. Check the input argument(s)") + })?; + let encoded_args = dynavalues.abi_encode_params(); + let is_payable = matches!(constructor.state_mutability, StateMutability::Payable); + + Ok(([bytecode, encoded_args].concat(), is_payable)) + } + + fn extract(funcs: &Function, args: &str) -> Result { + let types = funcs + .inputs + .iter() + .map(|i| i.resolve().unwrap()) + .collect::>(); + let ty = DynSolType::Tuple(types); + ty.coerce_str(args).map_err(|_| { + anyhow!( + "Abi: Error coercing the arguments for the function call. Check the input argument(s)" + ) + }) + } + + /// Encode function information for use in a transaction. Note: `args` is a string + /// of input parameters that are parsed by alloy `DynSolType`'s and converted into + /// `DynSolValue`s. See [DynSolType.coerce_str()](https://docs.rs/alloy-dyn-abi/latest/alloy_dyn_abi/enum.DynSolType.html#method.coerce_str) + /// + /// - `name` is the name of the function + /// - `args` string of input arguments + /// + /// ## Example + /// + /// `"(1, hello, (0x11111111111111111111111111111, 5))"` + /// + /// is parsed into an alloy `DynSolValue` ...tuple, U256, etc... + /// + /// Returns a tuple with: + /// - encoded function and args + /// - whether the function is payable + /// - and the output `DynSolType` that can be used to decode the result of the call + pub fn encode_function( + &self, + name: &str, + args: &str, + ) -> anyhow::Result<(Vec, bool, Option)> { + let funcs = match self.abi.function(name) { + Some(funcs) => funcs, + _ => bail!("Abi: Function {} not found in the ABI!", name), + }; + + // find the first function that matches the input args + for f in funcs { + let result = Self::extract(f, args); + let is_payable = matches!(f.state_mutability, StateMutability::Payable); + if result.is_ok() { + // Get the return type decoder, if any... + let ty = match f.outputs.len() { + 0 => None, + 1 => f.outputs.first().unwrap().clone().resolve().ok(), + _ => { + let t = f + .outputs + .iter() + .map(|i| i.resolve().unwrap()) + .collect::>(); + Some(DynSolType::Tuple(t)) + } + }; + + let selector = f.selector().to_vec(); + let encoded_args = result.unwrap().abi_encode_params(); + let all = [selector, encoded_args].concat(); + + return Ok((all, is_payable, ty)); + } + } + + // if we get here, it means we didn't find a function that + // matched the input arguments + Err(anyhow::anyhow!( + "Abi: Arguments to the function do not match what is expected" + )) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use alloy_primitives::{b256, bytes, Address, LogData}; + + #[test] + fn check_constructor_encoding() { + let input = vec!["constructor()"]; + let mut abi = ContractAbi::from_human_readable(input); + // short-circuit internal check... + abi.bytecode = Some(b"hello".into()); + + assert!(abi.encode_constructor("()").is_ok()); + assert!(abi.encode_constructor("(1234)").is_err()); + } + + #[test] + fn encoding_function_decoder_types() { + let tc = ContractAbi::from_human_readable(vec![ + "function a()", + "function b() (uint256)", + "function c() (bool, address, uint256)", + ]); + + let (_, _, r1) = tc.encode_function("a", "()").unwrap(); + let (_, _, r2) = tc.encode_function("b", "()").unwrap(); + let (_, _, r3) = tc.encode_function("c", "()").unwrap(); + + assert_eq!(None, r1); + assert_eq!(Some(DynSolType::Uint(256)), r2); + assert_eq!( + Some(DynSolType::Tuple(vec![ + DynSolType::Bool, + DynSolType::Address, + DynSolType::Uint(256) + ])), + r3 + ); + } + + #[test] + fn encoding_functions() { + let hello_world = vec!["function hello(tuple(uint256, address, uint160)) (bool)"]; + let hw = ContractAbi::from_human_readable(hello_world); + assert!(hw.has_function("hello")); + + let addy = Address::with_last_byte(24); + + assert!(hw.encode_function("bob", "()").is_err()); + assert!(hw.encode_function("hello", "(1,2").is_err()); + + let (_, is_payable, dtype) = hw + .encode_function("hello", &format!("(({}, {}, {}))", 10, addy.to_string(), 1)) + .unwrap(); + + assert!(!is_payable); + assert_eq!(dtype, Some(DynSolType::Bool)); + } + + #[test] + fn encoding_overloaded_functions() { + let overit = vec![ + "function one() (bool)", + "function one(uint256)", + "function one(address, (uint64, uint64)) (address)", + ]; + let abi = ContractAbi::from_human_readable(overit); + let addy = Address::with_last_byte(24); + + let (_, _, otype) = abi + .encode_function("one", &format!("({},({},{}))", addy.to_string(), 10, 11)) + .unwrap(); + + assert_eq!(Some(DynSolType::Address), otype); + } + + #[test] + fn test_flatten_event_structure() { + // mint signature: 0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885 + // burn signature: 0xcc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5 + let sample = ContractAbi::from_human_readable(vec![ + "event Transfer(address indexed from,address indexed to,uint256 amount)", + "event Transfer(address indexed from) anonymous", + "event Mint(address indexed recip,uint256 amount)", + "event Burn(address indexed recip,uint256 amount)", + ]); + + assert_eq!(4, sample.events_logs.len()); + + let transfer = LogData::new_unchecked( + vec![ + b256!("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), + b256!("000000000000000000000000c2e9f25be6257c210d7adf0d4cd6e3e881ba25f8"), + b256!("0000000000000000000000002b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b"), + ], + bytes!("0000000000000000000000000000000000000000000000000000000000000005"), + ); + + let burn = LogData::new_unchecked( + vec![ + b256!("cc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5"), + b256!("000000000000000000000000c2e9f25be6257c210d7adf0d4cd6e3e881ba25f8"), + ], + bytes!("0000000000000000000000000000000000000000000000000000000000000005"), + ); + + let log_address = Address::repeat_byte(14); + let logs = vec![ + Log { + address: log_address, + data: transfer, + }, + Log { + address: log_address, + data: burn, + }, + ]; + + let results = sample.extract_logs(logs); + assert_eq!(2, results.len()); + + //println!("{:?}", results); + } +} diff --git a/src/core/errors.rs b/src/core/errors.rs new file mode 100644 index 0000000..263789e --- /dev/null +++ b/src/core/errors.rs @@ -0,0 +1,41 @@ +//! +//! Database errors +//! +use alloy_primitives::{Address, U256}; +use revm::primitives::EVMError; +use revm::primitives::B256; +use thiserror::Error; + +use std::convert::Infallible; + +/// Wrapper for Database errors +#[derive(Error, Debug)] +pub enum DatabaseError { + //#[error("missing AccountInfo {0}")] + //MissingAccount(Address), + #[error("code should already be loaded: {0}")] + MissingCode(B256), + #[error("failed to get account for {0}")] + GetAccount(Address), + #[error("failed to get storage for {0} at {1}")] + GetStorage(Address, U256), + #[error("failed to get block hash for {0}")] + GetBlockHash(U256), + #[error("{0}")] + Other(String), +} + +impl From> for DatabaseError { + fn from(err: EVMError) -> Self { + match err { + EVMError::Database(err) => err, + err => DatabaseError::Other(err.to_string()), + } + } +} + +impl From for DatabaseError { + fn from(value: Infallible) -> Self { + match value {} + } +} diff --git a/src/core/evm.rs b/src/core/evm.rs new file mode 100644 index 0000000..73884bd --- /dev/null +++ b/src/core/evm.rs @@ -0,0 +1,435 @@ +//! +//! An API to interact with an embedded Ethereum Virtual Machine. +//! +//! This is wrapper around [REVM](https://docs.rs/revm/latest/revm/index.html). The implementation +//! is a simplfied version of [Foundry's Executor](https://github.com/foundry-rs/foundry) +//! + +use alloy_primitives::{Address, Bytes, U256}; +use alloy_sol_types::decode_revert_reason; +use anyhow::{anyhow, bail, Result}; +use revm::{ + db::{DatabaseCommit, DatabaseRef}, + primitives::{ + Account, AccountInfo, BlockEnv, Env, EnvWithHandlerCfg, ExecutionResult, HashMap as Map, + Log, Output, ResultAndState, TransactTo, TxEnv, + }, +}; + +use crate::{core::snapshot::SnapShot, core::storage::CreateFork, core::storage::StorageBackend}; + +/// type alias for a `revm` hashmap of `Address` => `Account` +type StateChangeSet = Map; + +/// EVM that supports both in-memory and forked storage. +pub struct BaseEvm { + backend: StorageBackend, + env: EnvWithHandlerCfg, +} + +/// Create an EVM with the in-memory database +impl Default for BaseEvm { + fn default() -> Self { + BaseEvm::new(None) + } +} + +impl BaseEvm { + /// Create an instance of the EVM. If fork is None it will use the in-memory database. + /// Otherwise it will create a forked database. + pub fn new(fork: Option) -> Self { + let env = EnvWithHandlerCfg::default(); + let backend = StorageBackend::new(fork); + Self { env, backend } + } + + /// Create an instance of the EVM and load it's state from the `SnapShot`. This + /// will use the in-memory database. + pub fn new_from_snapshot(snap: SnapShot) -> Self { + let env = EnvWithHandlerCfg::default(); + let mut backend = StorageBackend::default(); + backend.load_snapshot(snap); + Self { env, backend } + } + + /// Create an account for the given `user` with an optional balance (`amount`). + /// This will overwrite an account if it already exists. + pub fn create_account(&mut self, user: Address, amount: Option) -> Result<()> { + let mut info = AccountInfo::default(); + if let Some(amnt) = amount { + info.balance = amnt; + } + self.backend.insert_account_info(user, info); + Ok(()) + } + + /// Return the balance for the `caller`'s account. + pub fn get_balance(&mut self, caller: Address) -> Result { + Ok(self + .backend + .basic_ref(caller)? + .map(|acc| acc.balance) + .unwrap_or_default()) + } + + /* + /// Set the balance for the given `address` with the given `amount` + pub fn set_balance(&mut self, address: Address, amount: U256) -> Result<&mut Self> { + let mut account = self.backend.basic_ref(address)?.unwrap_or_default(); + account.balance = amount; + + self.backend.insert_account_info(address, account); + Ok(self) + } + */ + + /// Create a snapshot of the current database. This can be used to reload state. + pub fn create_snapshot(&self) -> Result { + self.backend.create_snapshot() + } + + /// Deploy a contract returning the contract's address. + /// If `value` is specified, the constructor must be `payable`. + pub fn deploy(&mut self, caller: Address, data: Vec, value: U256) -> Result
{ + let mut env = self.build_env(Some(caller), TransactTo::create(), data.into(), value); + let result = self.backend.run_transact(&mut env)?; + let mut call_results = process_call_result(result)?; + self.commit(&mut call_results); + + match call_results.address { + Some(addr) => Ok(addr), + _ => Err(anyhow!("deploy did not return an Address!")), + } + } + + /// Transfer `value` from `caller` -> `to` + pub fn transfer(&mut self, caller: Address, to: Address, value: U256) -> Result<()> { + let _ = self.transact_commit(caller, to, vec![], value)?; + Ok(()) + } + + /* TODO Remove? + /// Same as `transact_commit`, but supports [alloy's sol types](https://docs.rs/alloy-sol-types/latest/alloy_sol_types/index.html). + pub fn transact_commit_sol( + &mut self, + caller: Address, + to: Address, + args: T, + value: U256, + ) -> Result<::Return> { + let data = args.abi_encode(); + let result = self.transact_commit(caller, to, data, value)?; + T::abi_decode_returns(&result.result, true) + .map_err(|e| anyhow!("transact commit sol error: {:?}", e)) + } + */ + + /// Write call to a contact. Send a transaction where any state changes are persisted to the underlying database. + pub fn transact_commit( + &mut self, + caller: Address, + to: Address, + data: Vec, + value: U256, + ) -> Result { + let mut env = self.build_env(Some(caller), TransactTo::call(to), data.into(), value); + let result = self.backend.run_transact(&mut env)?; + let mut call_results = process_call_result(result)?; + self.commit(&mut call_results); + + Ok(call_results) + } + + /* TODO remove + /// Same as `transact_call` but supports [alloy's sol types](https://docs.rs/alloy-sol-types/latest/alloy_sol_types/index.html). + pub fn transact_call_sol( + &mut self, + to: Address, + args: T, + value: U256, + ) -> Result<::Return> { + let data = args.abi_encode(); + let result = self.transact_call(to, data, value)?; + T::abi_decode_returns(&result.result, true) + .map_err(|e| anyhow!("transact call sol error: {:?}", e)) + } + */ + + /// Read call to a contract. Send a transaction but any state changes are NOT persisted to the + /// database. + pub fn transact_call(&mut self, to: Address, data: Vec, value: U256) -> Result { + let mut env = self.build_env(None, TransactTo::call(to), data.into(), value); + let result = self.backend.run_transact(&mut env)?; + process_call_result(result) + } + + /// Simulate a `transact_commit` without actually committing/changing state. + pub fn simulate( + &mut self, + caller: Address, + to: Address, + data: Vec, + value: U256, + ) -> Result { + let mut env = self.build_env(Some(caller), TransactTo::call(to), data.into(), value); + let result = self.backend.run_transact(&mut env)?; + process_call_result(result) + } + + /// Advance `block.number` and `block.timestamp`. Set `interval` to the + /// amount of time in seconds you want to advance the timestamp. Block number + /// will be automatically incremented. + /// + /// Must be manually called. + pub fn update_block(&mut self, interval: u64) { + self.backend.update_block_info(interval); + } + + fn build_env( + &self, + caller: Option
, + transact_to: TransactTo, + data: Bytes, + value: U256, + ) -> EnvWithHandlerCfg { + let blkn = self.backend.block_number; + let ts = self.backend.timestamp; + + let env = Env { + cfg: self.env.cfg.clone(), + block: BlockEnv { + basefee: U256::ZERO, + timestamp: U256::from(ts), + number: U256::from(blkn), + ..self.env.block.clone() + }, + tx: TxEnv { + caller: caller.unwrap_or(Address::ZERO), + transact_to, + data, + value, + gas_price: U256::ZERO, + gas_priority_fee: None, + ..self.env.tx.clone() + }, + }; + + EnvWithHandlerCfg::new_with_spec_id(Box::new(env), self.env.handler_cfg.spec_id) + } + + fn commit(&mut self, result: &mut CallResult) { + if let Some(changes) = &result.state_changeset { + self.backend.commit(changes.clone()); + } + } +} + +/// Container for the results of a transaction +pub struct CallResult { + /// The raw result of the call. + pub result: Bytes, + /// An address if the call is a TransactTo::create (deploy) + pub address: Option
, + /// The gas used for the call + pub gas_used: u64, + /// Refunded gas + pub gas_refunded: u64, + /// The logs emitted during the call + pub logs: Vec, + /// Changes made to the database + pub state_changeset: Option, +} + +fn process_call_result(result: ResultAndState) -> Result { + let ResultAndState { + result: exec_result, + state: state_changeset, + } = result; + + let (gas_refunded, gas_used, out, logs) = match exec_result { + ExecutionResult::Success { + gas_used, + gas_refunded, + output, + logs, + .. + } => (gas_refunded, gas_used, output, logs), + ExecutionResult::Revert { gas_used, output } => match decode_revert_reason(&output) { + Some(reason) => bail!("Reverted: {:?}. Gas used: {:?}", reason, gas_used), + _ => bail!("Reverted with no reason. Gas used: {:?}", gas_used), + }, + ExecutionResult::Halt { reason, gas_used } => { + bail!("Halted: {:?}. Gas used: {:?}", reason, gas_used) + } + }; + + match out { + Output::Call(result) => Ok(CallResult { + result, + gas_used, + gas_refunded, + logs, + address: None, + state_changeset: Some(state_changeset), + }), + Output::Create(data, address) => Ok(CallResult { + result: data.clone(), + address, + gas_used, + logs, + gas_refunded, + state_changeset: Some(state_changeset), + }), + } +} + +#[cfg(test)] +mod tests { + use crate::core::abi::ContractAbi; + use crate::core::evm::BaseEvm; + use alloy_dyn_abi::DynSolValue; + use alloy_primitives::{Address, U256}; + + const BYTECODE: &str = "608060405260405161032c38038061032c8339810160408190526100\ + 229161003c565b600155600080546001600160a01b03191633179055610055565b6000602\ + 0828403121561004e57600080fd5b5051919050565b6102c8806100646000396000f3fe60\ + 80604052600436106100555760003560e01c80633fa4f2451461005a57806361fa423b146\ + 100835780637cf5dab0146100b35780638da5cb5b146100e8578063d09de08a1461012057\ + 8063d0e30db014610135575b600080fd5b34801561006657600080fd5b506100706001548\ + 1565b6040519081526020015b60405180910390f35b34801561008f57600080fd5b506100\ + a361009e36600461020a565b610137565b604051901515815260200161007a565b3480156\ + 100bf57600080fd5b506100d36100ce366004610222565b6101c8565b6040805192835260\ + 208301919091520161007a565b3480156100f457600080fd5b50600054610108906001600\ + 160a01b031681565b6040516001600160a01b03909116815260200161007a565b34801561\ + 012c57600080fd5b506100706101ec565b005b600080546001600160a01b0316331461018\ + e5760405162461bcd60e51b81526020600482015260156024820152743737ba103a343290\ + 31bab93932b73a1037bbb732b960591b604482015260640160405180910390fd5b61019b6\ + 02083018361023b565b600080546001600160a01b0319166001600160a01b039290921691\ + 90911790555060200135600190815590565b60008082600160008282546101dd919061026\ + b565b90915550506001549293915050565b6001805460009180836101ff828561026b565b\ + 909155509092915050565b60006040828403121561021c57600080fd5b50919050565b600\ + 06020828403121561023457600080fd5b5035919050565b60006020828403121561024d57\ + 600080fd5b81356001600160a01b038116811461026457600080fd5b9392505050565b808\ + 2018082111561028c57634e487b7160e01b600052601160045260246000fd5b9291505056\ + fea264697066735822122073a633ec59ee8e261bbdfefdc6d54f1d47dd6ccd6dcab4aa1eb\ + 37b62d24b4c1b64736f6c63430008140033"; + + #[test] + fn balances() { + let zero = U256::from(0); + //let one_eth = U256::from(1e18); + + let mut evm = BaseEvm::default(); + let bob = Address::repeat_byte(23); + + evm.create_account(bob, None).unwrap(); + assert!(evm.get_balance(bob).unwrap() == zero); + + //evm.set_balance(bob, one_eth).unwrap(); + //assert!(evm.get_balance(bob).unwrap() == one_eth); + } + + #[test] + fn simple_transfers() { + let one_eth = U256::from(1e18); + //let addresses = generate_random_addresses(2); + let bob = Address::repeat_byte(23); + let alice = Address::repeat_byte(24); + + let mut evm = BaseEvm::new(None); + evm.create_account(bob, Some(U256::from(2e18))).unwrap(); + evm.create_account(alice, None).unwrap(); + + assert!(evm.transfer(alice, bob, one_eth).is_err()); // alice has nothing to transfer...yet + assert!(evm.transfer(bob, alice, one_eth).is_ok()); + + assert!(evm.get_balance(bob).unwrap() == one_eth); + assert!(evm.get_balance(alice).unwrap() == one_eth); + + let s = evm.create_snapshot(); + println!("{:?}", s); + } + + #[test] + fn no_sol_test_contract() { + let contract_bytecode = hex::decode(BYTECODE).expect("failed to decode bytecode"); + + let zero = U256::from(0); + let owner = Address::repeat_byte(12); + let mut evm = BaseEvm::default(); + evm.create_account(owner, Some(U256::from(1e18))).unwrap(); + + let mut test_contract_abi = ContractAbi::from_human_readable(vec![ + "constructor(uint256)", + "function owner() (address)", + "function value() (uint256)", + "function increment() (uint256)", + "function increment(uint256) (uint256, uint256)", + ]); + test_contract_abi.bytecode = Some(contract_bytecode.into()); + + let (args, _) = test_contract_abi.encode_constructor("(1)").unwrap(); + let contract_address = evm.deploy(owner, args, U256::from(0)).unwrap(); + + // Check owner call + let (enc_owner_call, _, de1) = test_contract_abi.encode_function("owner", "()").unwrap(); + let o1 = evm + .transact_call(contract_address, enc_owner_call, zero) + .unwrap(); + assert!(DynSolValue::Address(owner) == de1.unwrap().abi_decode(&o1.result).unwrap()); + + // do increment() + let (enc_inc_0, _, de2) = test_contract_abi + .encode_function("increment", "()") + .unwrap(); + let o2 = evm + .transact_commit(owner, contract_address, enc_inc_0, zero) + .unwrap(); + assert!( + DynSolValue::Uint(U256::from(1), 256) == de2.unwrap().abi_decode(&o2.result).unwrap() + ); + + // check the value + let (enc_value_call, _, de3) = test_contract_abi.encode_function("value", "()").unwrap(); + let o3 = evm + .transact_call(contract_address, enc_value_call, zero) + .unwrap(); + assert!( + DynSolValue::Uint(U256::from(2), 256) == de3.unwrap().abi_decode(&o3.result).unwrap() + ); + + // do increment(value) + let (enc_inc_1, _, de4) = test_contract_abi + .encode_function("increment", "(2)") + .unwrap(); + let o4 = evm + .transact_commit(owner, contract_address, enc_inc_1, zero) + .unwrap(); + assert!( + DynSolValue::Tuple(vec![ + DynSolValue::Uint(U256::from(2), 256), + DynSolValue::Uint(U256::from(4), 256) + ]) == de4.unwrap().abi_decode(&o4.result).unwrap() + ); + + // simulate increment + let (enc_inc_sim, _, des) = test_contract_abi + .encode_function("increment", "()") + .unwrap(); + let os = evm + .simulate(owner, contract_address, enc_inc_sim, zero) + .unwrap(); + assert!( + DynSolValue::Uint(U256::from(4), 256) == des.unwrap().abi_decode(&os.result).unwrap() + ); + + // make sure value didn't change from 'simulate' + let (enc_value_call1, _, de5) = test_contract_abi.encode_function("value", "()").unwrap(); + let o5 = evm + .transact_call(contract_address, enc_value_call1, zero) + .unwrap(); + assert!( + DynSolValue::Uint(U256::from(4), 256) == de5.unwrap().abi_decode(&o5.result).unwrap() + ); + } +} diff --git a/src/core/fork.rs b/src/core/fork.rs new file mode 100644 index 0000000..823d71b --- /dev/null +++ b/src/core/fork.rs @@ -0,0 +1,123 @@ +// + +use crate::core::{ + errors::DatabaseError, + fork_backend::ForkBackend, + snapshot::{SnapShot, SnapShotAccountRecord, SnapShotSource}, +}; +use alloy_primitives::U256; +use revm::db::{CacheDB, DatabaseRef}; +use revm::primitives::Address; +use revm::primitives::{Account, AccountInfo, Bytecode, HashMap as Map, B256}; +use revm::{Database, DatabaseCommit}; + +#[derive(Clone, Debug)] +pub struct Fork { + pub db: CacheDB, + pub block_number: u64, + pub timestamp: u64, +} + +impl Fork { + pub fn new(url: &str, starting_block_number: Option) -> Self { + let backend = ForkBackend::new(url, starting_block_number); + let block_number = backend.block_number; + let timestamp = backend.timestamp; + Self { + db: CacheDB::new(backend), + block_number, + timestamp, + } + } + + pub fn database(&self) -> &CacheDB { + &self.db + } + + pub fn database_mut(&mut self) -> &mut CacheDB { + &mut self.db + } + + pub fn create_snapshot(&self, block_num: u64, timestamp: u64) -> anyhow::Result { + let accounts = self + .database() + .accounts + .clone() + .into_iter() + .map( + |(k, v)| -> anyhow::Result<(Address, SnapShotAccountRecord)> { + let code = if let Some(code) = v.info.code { + code + } else { + self.database().code_by_hash_ref(v.info.code_hash)? + } + .to_checked(); + Ok(( + k, + SnapShotAccountRecord { + nonce: v.info.nonce, + balance: v.info.balance, + code: code.original_bytes(), + storage: v.storage.into_iter().collect(), + }, + )) + }, + ) + .collect::>()?; + Ok(SnapShot { + block_num, + timestamp, + source: SnapShotSource::Fork, + accounts, + }) + } +} + +impl Database for Fork { + type Error = DatabaseError; + + fn basic(&mut self, address: Address) -> Result, Self::Error> { + // Note: this will always return Some, since the `SharedBackend` will always load the + // account, this differs from `::basic`, See also + // [MemDb::ensure_loaded](crate::backend::MemDb::ensure_loaded) + Database::basic(&mut self.db, address) + } + + fn code_by_hash(&mut self, code_hash: B256) -> Result { + Database::code_by_hash(&mut self.db, code_hash) + } + + fn storage(&mut self, address: Address, index: U256) -> Result { + Database::storage(&mut self.db, address, index) + } + + fn block_hash(&mut self, number: U256) -> Result { + Database::block_hash(&mut self.db, number) + } +} + +impl DatabaseRef for Fork { + type Error = DatabaseError; + + fn basic_ref(&self, address: Address) -> Result, Self::Error> { + self.db.basic_ref(address) + } + + fn code_by_hash_ref(&self, code_hash: B256) -> Result { + self.db.code_by_hash_ref(code_hash) + } + + fn storage_ref(&self, address: Address, index: U256) -> Result { + DatabaseRef::storage_ref(&self.db, address, index) + } + + fn block_hash_ref(&self, number: U256) -> Result { + self.db.block_hash_ref(number) + } +} + +impl DatabaseCommit for Fork { + fn commit(&mut self, changes: Map) { + self.database_mut().commit(changes) + } +} diff --git a/src/core/fork_backend.rs b/src/core/fork_backend.rs new file mode 100644 index 0000000..f5962bc --- /dev/null +++ b/src/core/fork_backend.rs @@ -0,0 +1,159 @@ +use alloy_primitives::{Address, U256}; +use anyhow::Result; +use ethers_core::types::{Block, BlockId, BlockNumber, TxHash, H160, H256, U64}; +use ethers_providers::{Http, Middleware, Provider, ProviderError}; +use revm::{ + primitives::{AccountInfo, Bytecode, B256, KECCAK_EMPTY}, + DatabaseRef, +}; +use std::sync::Arc; +use tokio::runtime::{Builder, Handle, RuntimeFlavor}; + +use crate::core::errors::DatabaseError; + +pub type HttpProvider = Provider; + +#[derive(Clone, Debug)] +pub struct ForkBackend { + provider: Arc, + pub block_number: u64, + pub timestamp: u64, +} + +impl ForkBackend { + pub fn new(url: &str, starting_block_number: Option) -> Self { + let client = + Provider::::try_from(url).expect("ForkBackend: failed to load HTTP provider"); + let provider = Arc::new(client); + + let blockid = if let Some(bn) = starting_block_number { + BlockId::from(U64::from(bn)) + } else { + BlockId::from(BlockNumber::Latest) + }; + + let blk = match Self::block_on(provider.get_block(blockid)) { + Ok(Some(b)) => b, + _ => panic!("ForkBackend: failed to load block information"), + }; + + let block_number = blk + .number + .expect("ForkBackend: Got 'pending' block number") + .as_u64(); + let timestamp = blk.timestamp.as_u64(); + /* + let block_number = if let Some(bn) = starting_block_number { + bn + } else { + Self::block_on(provider.get_block_number()) + .expect("ForkBackend: failed to load latest blocknumber from remote") + .as_u64() + }; + */ + + Self { + provider, + block_number, + timestamp, + } + } + + // adapted from revm ethersdb + #[inline] + fn block_on(f: F) -> F::Output + where + F: core::future::Future + Send, + F::Output: Send, + { + match Handle::try_current() { + Ok(handle) => match handle.runtime_flavor() { + RuntimeFlavor::CurrentThread => std::thread::scope(move |s| { + s.spawn(move || { + Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(f) + }) + .join() + .unwrap() + }), + _ => tokio::task::block_in_place(move || handle.block_on(f)), + }, + Err(_) => Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(f), + } + } + + fn fetch_basic_from_fork(&self, address: Address) -> Result { + let add = H160::from(address.0 .0); + let bn: Option = Some(BlockId::from(self.block_number)); + + let f = async { + let nonce = self.provider.get_transaction_count(add, bn); + let balance = self.provider.get_balance(add, bn); + let code = self.provider.get_code(add, bn); + tokio::join!(nonce, balance, code) + }; + let (nonce, balance, code) = Self::block_on(f); + + let balance = U256::from_limbs(balance?.0); + let nonce = nonce?.as_u64(); + let bytecode = Bytecode::new_raw(code?.0.into()); + let code_hash = bytecode.hash_slow(); + Ok(AccountInfo::new(balance, nonce, code_hash, bytecode)) + } + + fn fetch_storage_from_fork( + &self, + address: Address, + index: U256, + ) -> Result { + let add = H160::from(address.0 .0); + let bn: Option = Some(BlockId::from(self.block_number)); + + let index = H256::from(index.to_be_bytes()); + let slot_value: H256 = Self::block_on(self.provider.get_storage_at(add, index, bn))?; + Ok(U256::from_be_bytes(slot_value.to_fixed_bytes())) + } + + fn fetch_blockhash_from_fork(&self, number: U256) -> Result { + if number > U256::from(u64::MAX) { + return Ok(KECCAK_EMPTY); + } + // We know number <= u64::MAX so unwrap is safe + let number = U64::from(u64::try_from(number).unwrap()); + let block: Option> = + Self::block_on(self.provider.get_block(BlockId::from(number)))?; + Ok(B256::new(block.unwrap().hash.unwrap().0)) + } +} + +impl DatabaseRef for ForkBackend { + type Error = DatabaseError; + + fn basic_ref(&self, address: Address) -> Result, Self::Error> { + match self.fetch_basic_from_fork(address) { + Ok(addr) => Ok(Some(addr)), + Err(_err) => Err(DatabaseError::GetAccount(address)), + } + } + + fn code_by_hash_ref(&self, hash: B256) -> Result { + Err(DatabaseError::MissingCode(hash)) + } + + fn storage_ref(&self, address: Address, index: U256) -> Result { + self.fetch_storage_from_fork(address, index) + .map_err(|_err| DatabaseError::GetStorage(address, index)) + } + + fn block_hash_ref(&self, number: U256) -> Result { + self.fetch_blockhash_from_fork(number) + .map_err(|_err| DatabaseError::GetBlockHash(number)) + } +} diff --git a/src/core/in_memory_db.rs b/src/core/in_memory_db.rs new file mode 100644 index 0000000..223761b --- /dev/null +++ b/src/core/in_memory_db.rs @@ -0,0 +1,153 @@ +//! The in memory DB +//! ADAPTED FROM Foundry-rs +//! https://github.com/foundry-rs/foundry/blob/master/crates/evm/core/src/backend/in_memory_db.rs +//! +use crate::core::{ + errors::DatabaseError, + snapshot::{SnapShot, SnapShotAccountRecord, SnapShotSource}, +}; +use alloy_primitives::{Address, B256, U256}; +use revm::{ + db::{CacheDB, DatabaseRef, EmptyDB}, + primitives::{Account, AccountInfo, Bytecode, HashMap as Map}, + Database, DatabaseCommit, +}; + +/// +/// This acts like a wrapper type for [InMemoryDB] but is capable of creating/applying snapshots +#[derive(Debug)] +pub struct MemDb { + pub db: CacheDB, +} + +impl Default for MemDb { + fn default() -> Self { + Self { + db: CacheDB::new(Default::default()), + } + } +} + +impl MemDb { + pub fn create_snapshot(&self, block_num: u64, timestamp: u64) -> anyhow::Result { + let accounts = self + .db + .accounts + .clone() + .into_iter() + .map( + |(k, v)| -> anyhow::Result<(Address, SnapShotAccountRecord)> { + let code = if let Some(code) = v.info.code { + code + } else { + self.db.code_by_hash_ref(v.info.code_hash)? + } + .to_checked(); + Ok(( + k, + SnapShotAccountRecord { + nonce: v.info.nonce, + balance: v.info.balance, + code: code.original_bytes(), + storage: v.storage.into_iter().collect(), + }, + )) + }, + ) + .collect::>()?; + Ok(SnapShot { + block_num, + timestamp, + source: SnapShotSource::Memory, + accounts, + }) + } +} + +impl DatabaseRef for MemDb { + type Error = DatabaseError; + fn basic_ref(&self, address: Address) -> Result, Self::Error> { + DatabaseRef::basic_ref(&self.db, address) + } + + fn code_by_hash_ref(&self, code_hash: B256) -> Result { + DatabaseRef::code_by_hash_ref(&self.db, code_hash) + } + + fn storage_ref(&self, address: Address, index: U256) -> Result { + DatabaseRef::storage_ref(&self.db, address, index) + } + + fn block_hash_ref(&self, number: U256) -> Result { + DatabaseRef::block_hash_ref(&self.db, number) + } +} + +impl Database for MemDb { + type Error = DatabaseError; + + fn basic(&mut self, address: Address) -> Result, Self::Error> { + // Note: this will always return `Some(AccountInfo)`, See `EmptyDBWrapper` + Database::basic(&mut self.db, address) + } + + fn code_by_hash(&mut self, code_hash: B256) -> Result { + Database::code_by_hash(&mut self.db, code_hash) + } + + fn storage(&mut self, address: Address, index: U256) -> Result { + Database::storage(&mut self.db, address, index) + } + + fn block_hash(&mut self, number: U256) -> Result { + Database::block_hash(&mut self.db, number) + } +} + +impl DatabaseCommit for MemDb { + fn commit(&mut self, changes: Map) { + DatabaseCommit::commit(&mut self.db, changes) + } +} + +/// *documentation from foundry-rs* +/// +/// An empty database that always returns default values when queried. +/// +/// This is just a simple wrapper for `revm::EmptyDB` but implements `DatabaseError` instead, this +/// way we can unify all different `Database` impls +/// +/// This will also _always_ return `Some(AccountInfo)`: +/// +/// The [`Database`](revm::Database) implementation for `CacheDB` manages an `AccountState` for the +/// `DbAccount`, this will be set to `AccountState::NotExisting` if the account does not exist yet. +/// This is because there's a distinction between "non-existing" and "empty", +/// see . +/// If an account is `NotExisting`, `Database::basic_ref` will always return `None` for the +/// requested `AccountInfo`. +/// +/// To prevent this, we ensure that a missing account is never marked as `NotExisting` by always +/// returning `Some` with this type, which will then insert a default [`AccountInfo`] instead +/// of one marked as `AccountState::NotExisting`. +#[derive(Clone, Debug, Default)] +pub struct EmptyDBWrapper(EmptyDB); + +impl DatabaseRef for EmptyDBWrapper { + type Error = DatabaseError; + + fn basic_ref(&self, _address: Address) -> Result, Self::Error> { + // Note: this will always return `Some(AccountInfo)`, for the reason explained above + Ok(Some(AccountInfo::default())) + } + + fn code_by_hash_ref(&self, code_hash: B256) -> Result { + Ok(self.0.code_by_hash_ref(code_hash)?) + } + fn storage_ref(&self, address: Address, index: U256) -> Result { + Ok(self.0.storage_ref(address, index)?) + } + + fn block_hash_ref(&self, number: U256) -> Result { + Ok(self.0.block_hash_ref(number)?) + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..a20d4a3 --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,9 @@ +pub mod abi; +pub mod evm; + +pub mod errors; +pub mod fork; +pub mod fork_backend; +pub mod in_memory_db; +pub mod snapshot; +pub mod storage; diff --git a/src/core/snapshot.rs b/src/core/snapshot.rs new file mode 100644 index 0000000..6554cbf --- /dev/null +++ b/src/core/snapshot.rs @@ -0,0 +1,33 @@ +//! +//! Containers for serializing EVM state information +//! +use revm::primitives::{Address, Bytes, U256}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +/// Source of the snapshop. Either from a fork or the local in-memory database. +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub enum SnapShotSource { + Memory, + #[default] + Fork, +} + +/// A single AccountRecord and it's associated storage. `SnapShot` stores +/// a map of Accounts. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SnapShotAccountRecord { + pub nonce: u64, + pub balance: U256, + pub code: Bytes, + pub storage: BTreeMap, +} + +/// The high-level objects containing all the snapshot information. +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct SnapShot { + pub source: SnapShotSource, + pub block_num: u64, + pub timestamp: u64, + pub accounts: BTreeMap, +} diff --git a/src/core/storage.rs b/src/core/storage.rs new file mode 100644 index 0000000..6057a94 --- /dev/null +++ b/src/core/storage.rs @@ -0,0 +1,280 @@ +//! +//! Provides access to EVM storage +//! + +use alloy_primitives::{Address, U256}; +use anyhow::{anyhow, Result}; +use revm::{ + interpreter::primitives::EnvWithHandlerCfg, + primitives::{ + Account, AccountInfo, Bytecode, HashMap as Map, ResultAndState, B256, KECCAK_EMPTY, + }, + Database, DatabaseCommit, DatabaseRef, EvmBuilder, +}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::core::{errors::DatabaseError, snapshot::SnapShot}; +use crate::core::{fork::Fork, in_memory_db::MemDb}; + +/// Information related to creating a fork +#[derive(Clone, Debug)] +pub struct CreateFork { + /// the url of the RPC endpoint + pub url: String, + /// optional block number of the fork. If none, it will use the latest block. + pub blocknumber: Option, +} + +/* +impl CreateFork { + /// Fork at the given URL and block number + pub fn new(url: String, blocknumber: Option) -> Self { + Self { url, blocknumber } + } + + /// For at the given URL and use the latest block available + pub fn latest_block(url: String) -> Self { + Self { + url, + blocknumber: None, + } + } +} +*/ + +// Used by the EVM to access storage. This can either be an in-memory only db or a forked db. +// The EVM delegates transact() and transact_commit to this module +// +// This is based heavily on Foundry's approach. +pub struct StorageBackend { + mem_db: MemDb, // impl wrapper to handle DbErrors + forkdb: Option, + pub block_number: u64, // used to record in the snapshot... + pub timestamp: u64, +} + +impl Default for StorageBackend { + fn default() -> Self { + StorageBackend::new(None) + } +} + +impl StorageBackend { + pub fn new(fork: Option) -> Self { + if let Some(fork) = fork { + let backend = Fork::new(&fork.url, fork.blocknumber); + let block_number = backend.block_number; + let timestamp = backend.timestamp; + Self { + mem_db: MemDb::default(), + forkdb: Some(backend), + block_number, + timestamp, + } + } else { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("StorageBackend: failed to get unix epoch time") + .as_secs(); + Self { + mem_db: MemDb::default(), + forkdb: None, + block_number: 1, + timestamp, + } + } + } + + pub fn insert_account_info(&mut self, address: Address, info: AccountInfo) { + if let Some(fork) = self.forkdb.as_mut() { + fork.database_mut().insert_account_info(address, info) + } else { + // use mem... + self.mem_db.db.insert_account_info(address, info) + } + } + + /* + pub fn insert_account_storage( + &mut self, + address: Address, + slot: U256, + value: U256, + ) -> Result<(), DatabaseError> { + let ret = if let Some(fork) = self.forkdb.as_mut() { + fork.database_mut() + .insert_account_storage(address, slot, value) + } else { + self.mem_db.db.insert_account_storage(address, slot, value) + }; + ret + } + + pub fn replace_account_storage( + &mut self, + address: Address, + storage: Map, + ) -> Result<(), DatabaseError> { + if let Some(fork) = self.forkdb.as_mut() { + fork.database_mut() + .replace_account_storage(address, storage) + } else { + self.mem_db.db.replace_account_storage(address, storage) + } + } + */ + + pub fn run_transact(&mut self, env: &mut EnvWithHandlerCfg) -> Result { + let mut evm = create_evm(self, env.clone()); + let res = evm + .transact() + .map_err(|e| anyhow!("backend failed while executing transaction: {:?}", e))?; + env.env = evm.context.evm.inner.env; + + Ok(res) + } + + /// Create a snapshot of the current state, delegates + /// to the current backend database. + pub fn create_snapshot(&self) -> Result { + if let Some(fork) = self.forkdb.as_ref() { + fork.create_snapshot(self.block_number, self.timestamp) + } else { + self.mem_db + .create_snapshot(self.block_number, self.timestamp) + } + } + + /// Load a snapshot into an in-memory database + pub fn load_snapshot(&mut self, snapshot: SnapShot) { + self.block_number = snapshot.block_num; + self.timestamp = snapshot.timestamp; + + for (addr, account) in snapshot.accounts.into_iter() { + // note: this will populate both 'accounts' and 'contracts' + self.mem_db.db.insert_account_info( + addr, + AccountInfo { + balance: account.balance, + nonce: account.nonce, + code_hash: KECCAK_EMPTY, + code: if account.code.0.is_empty() { + None + } else { + Some( + Bytecode::new_raw(alloy_primitives::Bytes(account.code.0)).to_checked(), + ) + }, + }, + ); + + // ... but we still need to load the account storage map + for (k, v) in account.storage.into_iter() { + self.mem_db + .db + .accounts + .entry(addr) + .or_default() + .storage + .insert(k, v); + } + } + } + + /// See EVM update_block + pub fn update_block_info(&mut self, interval: u64) { + self.block_number += 1; + self.timestamp += interval; + } +} + +impl DatabaseRef for StorageBackend { + type Error = DatabaseError; + + fn basic_ref(&self, address: Address) -> Result, Self::Error> { + if let Some(db) = self.forkdb.as_ref() { + db.basic_ref(address) + } else { + Ok(self.mem_db.basic_ref(address)?) + } + } + + fn code_by_hash_ref(&self, code_hash: B256) -> Result { + if let Some(db) = self.forkdb.as_ref() { + db.code_by_hash_ref(code_hash) + } else { + Ok(self.mem_db.code_by_hash_ref(code_hash)?) + } + } + + fn storage_ref(&self, address: Address, index: U256) -> Result { + if let Some(db) = self.forkdb.as_ref() { + DatabaseRef::storage_ref(db, address, index) + } else { + Ok(DatabaseRef::storage_ref(&self.mem_db, address, index)?) + } + } + + fn block_hash_ref(&self, number: U256) -> Result { + if let Some(db) = self.forkdb.as_ref() { + db.block_hash_ref(number) + } else { + Ok(self.mem_db.block_hash_ref(number)?) + } + } +} + +impl Database for StorageBackend { + type Error = DatabaseError; + fn basic(&mut self, address: Address) -> Result, Self::Error> { + if let Some(db) = self.forkdb.as_mut() { + db.basic(address) + } else { + Ok(self.mem_db.basic(address)?) + } + } + + fn code_by_hash(&mut self, code_hash: B256) -> Result { + if let Some(db) = self.forkdb.as_mut() { + db.code_by_hash(code_hash) + } else { + Ok(self.mem_db.code_by_hash(code_hash)?) + } + } + + fn storage(&mut self, address: Address, index: U256) -> Result { + if let Some(db) = self.forkdb.as_mut() { + Database::storage(db, address, index) + } else { + Ok(Database::storage(&mut self.mem_db, address, index)?) + } + } + + fn block_hash(&mut self, number: U256) -> Result { + if let Some(db) = self.forkdb.as_mut() { + db.block_hash(number) + } else { + Ok(self.mem_db.block_hash(number)?) + } + } +} + +impl DatabaseCommit for StorageBackend { + fn commit(&mut self, changes: Map) { + if let Some(db) = self.forkdb.as_mut() { + db.commit(changes) + } else { + self.mem_db.commit(changes) + } + } +} + +fn create_evm<'a, DB: Database>( + db: DB, + env: revm::primitives::EnvWithHandlerCfg, +) -> revm::Evm<'a, (), DB> { + EvmBuilder::default() + .with_db(db) + .with_env(env.env.clone()) + .build() +} diff --git a/src/lib.rs b/src/lib.rs index ab49202..f217a68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +mod core; mod pyabi; mod pyevm; diff --git a/src/pyabi.rs b/src/pyabi.rs index 9f0220e..367a83a 100644 --- a/src/pyabi.rs +++ b/src/pyabi.rs @@ -4,7 +4,7 @@ use alloy_dyn_abi::DynSolType; use pyo3::prelude::*; -use simular_core::ContractAbi; +use crate::core::abi::ContractAbi; /// Can load and parse ABI information. Used in `Contract.py` to /// process function calls. diff --git a/src/pyevm.rs b/src/pyevm.rs index ef50e06..721c516 100644 --- a/src/pyevm.rs +++ b/src/pyevm.rs @@ -1,9 +1,9 @@ +use crate::core::{evm::BaseEvm, evm::CallResult, snapshot::SnapShot, storage::CreateFork}; use alloy_dyn_abi::DynSolValue; use alloy_primitives::U256; use anyhow::{anyhow, Result}; use core::ffi::c_uchar; use pyo3::{ffi, prelude::*}; -use simular_core::{evm::CallResult, BaseEvm, CreateFork, SnapShot}; use std::collections::HashMap; use crate::{ From 756cbd3d296cdd87d54646b9cc6f0ae7e541b6fa Mon Sep 17 00:00:00 2001 From: Dave Bryson Date: Fri, 4 Apr 2025 14:23:40 -0500 Subject: [PATCH 04/17] fix python version --- .github/workflows/build-release.yml | 9 +++++++++ pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 34dbfed..625abb3 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -50,6 +50,9 @@ jobs: needs: [check_main] steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' - name: Build wheels uses: PyO3/maturin-action@v1 with: @@ -70,6 +73,9 @@ jobs: needs: [check_main] steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' - name: Build wheels uses: PyO3/maturin-action@v1 with: @@ -87,6 +93,9 @@ jobs: needs: [check_main] steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' - name: Build sdist uses: PyO3/maturin-action@v1 with: diff --git a/pyproject.toml b/pyproject.toml index d54540f..9e0259d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ sdist-include = ["LICENSE", "README.md"] [project] name = "simular-evm" version = "0.3.0" -requires-python = ">=3.10" +requires-python = ">=3.10,<3.12" authors = [ { name = "Dave Bryson", email = "davebryson@users.noreply.github.com" }, ] From 2400b80f55c17df53420150e4281fb1d214aa981 Mon Sep 17 00:00:00 2001 From: Dave Bryson Date: Fri, 4 Apr 2025 15:00:15 -0500 Subject: [PATCH 05/17] CI stuff --- .github/workflows/build-linux.yml | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/build-linux.yml diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml new file mode 100644 index 0000000..b481fb2 --- /dev/null +++ b/.github/workflows/build-linux.yml @@ -0,0 +1,34 @@ +name: build-linux + +on: + workflow_dispatch + +jobs: + check_main: + runs-on: ubuntu-latest + steps: + - name: Release from main + if: github.ref == 'refs/heads/master' + run: echo '::notice ::Building and uploading release' + - name: No release + if: github.ref != 'refs/heads/master' + run: echo '::warning ::Release can only be run from the main branch!' && exit 1 + + linux: + runs-on: ubuntu-latest + strategy: + matrix: + target: [x86_64, aarch64, armv7] + needs: [check_main] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + manylinux: auto \ No newline at end of file From 64d9fb61d50efc87cdc7e56a9325ac3bbf91b853 Mon Sep 17 00:00:00 2001 From: Dave Bryson Date: Fri, 4 Apr 2025 15:04:47 -0500 Subject: [PATCH 06/17] CI stuff --- .github/workflows/build-linux.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index b481fb2..fc6a4b3 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -31,4 +31,7 @@ jobs: target: ${{ matrix.target }} args: --release --out dist --find-interpreter sccache: 'true' - manylinux: auto \ No newline at end of file + manylinux: auto + before-script-linux: | + sudo apt-get update + sudo apt-get install --yes --upgrade build-essential cmake protobuf-compiler libssl-dev glibc-source musl-tools \ No newline at end of file From e39bf9fa6b5d6b1f0f5bf20c4cecf479dc1e10cc Mon Sep 17 00:00:00 2001 From: Dave Bryson Date: Fri, 4 Apr 2025 17:56:32 -0500 Subject: [PATCH 07/17] upgrade pyo3 --- Cargo.toml | 2 +- pyproject.toml | 1 + src/lib.rs | 11 ++++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bd9a2ff..be88456 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ reqwest = { version = "0.11.19", features = ["rustls-tls"] } openssl = { version = "0.10", features = ["vendored"] } # Python wrapper -pyo3 = { version = "0.20.0", features = ["multiple-pymethods", "anyhow"] } +pyo3 = { version = "0.24.1", features = ["multiple-pymethods", "anyhow"] } # using this to allow cargo test on rust code. See: diff --git a/pyproject.toml b/pyproject.toml index 9e0259d..000666b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ description = "smart-contract api and embedded ethereum virtual machine" classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Rust", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", diff --git a/src/lib.rs b/src/lib.rs index f217a68..07a4512 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,9 +15,18 @@ pub fn str_to_address(caller: &str) -> Result
{ } #[pymodule] -fn simular(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn simular(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; Ok(()) } + +/* +#[pymodule] +fn simular(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +}*/ From 299c28aea311f0becffb2ad3bf78678319e0d0d2 Mon Sep 17 00:00:00 2001 From: Dave Bryson Date: Sat, 5 Apr 2025 08:29:15 -0500 Subject: [PATCH 08/17] upgraded pyo3 --- src/pyabi.rs | 12 +++++++++--- src/pyevm.rs | 31 +++++++++++++++++++------------ 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/pyabi.rs b/src/pyabi.rs index 367a83a..51f4634 100644 --- a/src/pyabi.rs +++ b/src/pyabi.rs @@ -2,7 +2,8 @@ //! Python wrapper for `simular-core::ContractAbi` //! use alloy_dyn_abi::DynSolType; -use pyo3::prelude::*; +use pyo3::{prelude::*, pybacked::PyBackedStr}; +use std::str; use crate::core::abi::ContractAbi; @@ -34,8 +35,13 @@ impl PyAbi { /// `["function hello() (uint256)"]` creates the function `hello` that /// can be encoded/decoded for calls to the Evm. #[staticmethod] - pub fn from_human_readable(values: Vec<&str>) -> Self { - Self(ContractAbi::from_human_readable(values)) + pub fn from_human_readable(values: Vec) -> Self { + // convert PyBackStr into &str + let mapped: Vec<&str> = values + .iter() + .map(|i| str::from_utf8(i.as_bytes()).unwrap()) + .collect(); + Self(ContractAbi::from_human_readable(mapped)) } /// Does the ABI contain the function `name` diff --git a/src/pyevm.rs b/src/pyevm.rs index 721c516..2dc5387 100644 --- a/src/pyevm.rs +++ b/src/pyevm.rs @@ -3,7 +3,7 @@ use alloy_dyn_abi::DynSolValue; use alloy_primitives::U256; use anyhow::{anyhow, Result}; use core::ffi::c_uchar; -use pyo3::{ffi, prelude::*}; +use pyo3::{ffi, prelude::*, IntoPyObjectExt}; use std::collections::HashMap; use crate::{ @@ -64,6 +64,7 @@ impl PyEvm { } /// Create account with an initial balance + #[pyo3(signature = (address, balance=None))] pub fn create_account(&mut self, address: &str, balance: Option) -> Result<()> { let caller = str_to_address(address)?; let value = balance.map(U256::from); @@ -168,6 +169,7 @@ impl PyEvm { /// number will automatically increment. /// /// When using a fork the initial block.number/timestamp will come from the snapshot. + #[pyo3(signature = (interval=None))] pub fn advance_block(&mut self, interval: Option) { let it = interval.unwrap_or(DEFAULT_BLOCK_INTERVAL); self.0.update_block(it); @@ -184,7 +186,7 @@ fn process_results( if let Some(de) = decoder.0 { let dynvalues = de.abi_decode(&output.result).unwrap(); let d = DynSolMap(dynvalues.clone()); - Some(d.into_py(py)) + Some(d.into_py_any(py).unwrap()) } else { None } @@ -209,7 +211,7 @@ fn process_results_and_events( let mut map = HashMap::::new(); for (k, v) in raw_events { let d = DynSolMap(v); - map.insert(k, d.into_py(py)); + map.insert(k, d.into_py_any(py).unwrap()); } Some(map) } else { @@ -227,7 +229,8 @@ fn walk_list(values: Vec, py: Python<'_>) -> PyObject { .into_iter() .map(|dv| base_exctract(dv, py)) .collect::>() - .into_py(py) + .into_py_any(py) + .unwrap() } // Convert DynSolValue signed and unsigned ints to Python. @@ -245,14 +248,14 @@ fn convert_ints(bytes: [u8; 32], is_signed: bool, py: Python<'_>) -> PyObject { // Transform DynSolValues to Python types. fn base_exctract(dv: DynSolValue, py: Python<'_>) -> PyObject { match dv { - DynSolValue::Address(a) => format!("{a:?}").into_py(py), - DynSolValue::Bool(a) => a.into_py(py), - DynSolValue::String(a) => a.into_py(py), + DynSolValue::Address(a) => format!("{a:?}").into_pyobject(py).unwrap().into(), + DynSolValue::Bool(a) => a.into_py_any(py).unwrap(), + DynSolValue::String(a) => a.into_pyobject(py).unwrap().into(), DynSolValue::Tuple(a) => walk_list(a, py), DynSolValue::Int(a, _) => convert_ints(a.to_le_bytes::<32>(), true, py), DynSolValue::Uint(a, _) => convert_ints(a.to_le_bytes::<32>(), false, py), - DynSolValue::Bytes(a) => a.into_py(py), - DynSolValue::FixedBytes(a, _) => a.to_vec().into_py(py), + DynSolValue::Bytes(a) => a.to_vec().into_py_any(py).unwrap(), + DynSolValue::FixedBytes(a, _) => a.to_vec().into_py_any(py).unwrap(), DynSolValue::Array(a) => walk_list(a, py), DynSolValue::FixedArray(a) => walk_list(a, py), _ => unimplemented!(), @@ -261,8 +264,12 @@ fn base_exctract(dv: DynSolValue, py: Python<'_>) -> PyObject { pub struct DynSolMap(DynSolValue); -impl IntoPy for DynSolMap { - fn into_py(self, py: Python<'_>) -> PyObject { - base_exctract(self.0, py) +impl<'py> IntoPyObject<'py> for DynSolMap { + type Target = PyAny; // the Python type + type Output = Bound<'py, Self::Target>; + type Error = std::convert::Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(base_exctract(self.0, py).into_bound(py)) } } From 3f6acf5bad8734bf34b9ab45af10829dba0fd035 Mon Sep 17 00:00:00 2001 From: Dave Bryson Date: Sat, 5 Apr 2025 08:35:09 -0500 Subject: [PATCH 09/17] upgraded pyo3 --- .github/workflows/build-linux.yml | 36 +++++++++++++++++-------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index fc6a4b3..bb29896 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -4,34 +4,38 @@ on: workflow_dispatch jobs: - check_main: - runs-on: ubuntu-latest + check_pyo3: + runs-on: ubuntu-22.04 steps: - name: Release from main - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/uppy03' run: echo '::notice ::Building and uploading release' - name: No release - if: github.ref != 'refs/heads/master' - run: echo '::warning ::Release can only be run from the main branch!' && exit 1 - + if: github.ref != 'refs/heads/uppy03' + run: echo '::warning ::test only be run from the uppy03 branch!' && exit 1 + linux: - runs-on: ubuntu-latest + runs-on: ${{ matrix.platform.runner }} strategy: matrix: - target: [x86_64, aarch64, armv7] - needs: [check_main] + platform: + - runner: ubuntu-22.04 + target: x86_64 + - runner: ubuntu-22.04 + target: x86 + - runner: ubuntu-22.04 + target: aarch64 + - runner: ubuntu-22.04 + target: armv7 steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: 3.x - name: Build wheels uses: PyO3/maturin-action@v1 with: - target: ${{ matrix.target }} + target: ${{ matrix.platform.target }} args: --release --out dist --find-interpreter - sccache: 'true' + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} manylinux: auto - before-script-linux: | - sudo apt-get update - sudo apt-get install --yes --upgrade build-essential cmake protobuf-compiler libssl-dev glibc-source musl-tools \ No newline at end of file From 279298a50a200645aba8960f495a40d0cd894918 Mon Sep 17 00:00:00 2001 From: Dave Bryson Date: Sat, 5 Apr 2025 08:40:00 -0500 Subject: [PATCH 10/17] upgraded pyo3 --- .github/workflows/build-linux.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index bb29896..51d1029 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -21,12 +21,11 @@ jobs: platform: - runner: ubuntu-22.04 target: x86_64 - - runner: ubuntu-22.04 - target: x86 - runner: ubuntu-22.04 target: aarch64 - runner: ubuntu-22.04 target: armv7 + needs: [check_pyo3] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 From 5fcf590d4f6a331277e97e049f7347fa8a1ee55e Mon Sep 17 00:00:00 2001 From: Dave Bryson Date: Sat, 5 Apr 2025 09:07:49 -0500 Subject: [PATCH 11/17] upgraded pyo3 --- .github/workflows/build-linux.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 51d1029..0b47d54 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -38,3 +38,6 @@ jobs: args: --release --out dist --find-interpreter sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} manylinux: auto + before-script-linux: | + sudo apt-get update + sudo apt-get install --yes --upgrade build-essential cmake protobuf-compiler libssl-dev glibc-source musl-tools From 3dca65244d2d13d7d3073f2ffb541120b60cf393 Mon Sep 17 00:00:00 2001 From: Dave Bryson Date: Sat, 5 Apr 2025 09:14:18 -0500 Subject: [PATCH 12/17] upgraded pyo3 --- .github/workflows/build-linux.yml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 0b47d54..811fde3 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -15,16 +15,10 @@ jobs: run: echo '::warning ::test only be run from the uppy03 branch!' && exit 1 linux: - runs-on: ${{ matrix.platform.runner }} + runs-on: ubuntu-latest strategy: matrix: - platform: - - runner: ubuntu-22.04 - target: x86_64 - - runner: ubuntu-22.04 - target: aarch64 - - runner: ubuntu-22.04 - target: armv7 + target: [x86_64, aarch64, armv7] needs: [check_pyo3] steps: - uses: actions/checkout@v4 @@ -34,10 +28,11 @@ jobs: - name: Build wheels uses: PyO3/maturin-action@v1 with: - target: ${{ matrix.platform.target }} + target: ${{ matrix.target }} args: --release --out dist --find-interpreter sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - manylinux: auto + manylinux: manylinux_2_28 + container: "ghcr.io/rust-cross/manylinux_2_28-cross:${{ matrix.target }}" before-script-linux: | sudo apt-get update sudo apt-get install --yes --upgrade build-essential cmake protobuf-compiler libssl-dev glibc-source musl-tools From 05add6e55887f0f26563ffe8ec3d01094e6a8ba1 Mon Sep 17 00:00:00 2001 From: Dave Bryson Date: Sat, 5 Apr 2025 09:23:14 -0500 Subject: [PATCH 13/17] upgraded pyo3 windows --- .github/workflows/build-linux.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 811fde3..aad62ee 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -5,7 +5,7 @@ on: jobs: check_pyo3: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - name: Release from main if: github.ref == 'refs/heads/uppy03' @@ -36,3 +36,21 @@ jobs: before-script-linux: | sudo apt-get update sudo apt-get install --yes --upgrade build-essential cmake protobuf-compiler libssl-dev glibc-source musl-tools + + windows: + runs-on: windows-latest + strategy: + matrix: + target: [x64, x86] + needs: [check_pyo3] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} From 2411d4974834894e71f1338f15edf498d25590be Mon Sep 17 00:00:00 2001 From: Dave Bryson Date: Sat, 5 Apr 2025 09:59:29 -0500 Subject: [PATCH 14/17] mac build --- .github/workflows/build-mac.yml | 33 +++++++++++++++++++++++++++++++++ Makefile | 2 +- pyproject.toml | 10 ++++------ 3 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/build-mac.yml diff --git a/.github/workflows/build-mac.yml b/.github/workflows/build-mac.yml new file mode 100644 index 0000000..918b50d --- /dev/null +++ b/.github/workflows/build-mac.yml @@ -0,0 +1,33 @@ +name: build-mac + +on: + workflow_dispatch + +jobs: + check_pyo3: + runs-on: ubuntu-latest + steps: + - name: Release from main + if: github.ref == 'refs/heads/uppy03' + run: echo '::notice ::Building and uploading release' + - name: No release + if: github.ref != 'refs/heads/uppy03' + run: echo '::warning ::test only be run from the uppy03 branch!' && exit 1 + + macos: + runs-on: macos-latest + strategy: + matrix: + target: [x86_64, aarch64] + needs: [check_pyo3] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} diff --git a/Makefile b/Makefile index 90f96f2..170fdc0 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ build: # this builds the production release prod_release: - hatch run dev:maturin develop --release + hatch run dev:maturin build test: build hatch run dev:pytest tests/* diff --git a/pyproject.toml b/pyproject.toml index 000666b..634be8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,9 +16,6 @@ authors = [ ] license = "Apache-2.0" readme = "README.md" -homepage = "https://github.com/simular-fi/simular" -repository = "https://github.com/simular-fi/simular" -documentation = "https://simular.readthedocs.io/en/latest/" keywords = ["agent-based modeling", "ethereum", "solidity", "simulation"] description = "smart-contract api and embedded ethereum virtual machine" classifiers = [ @@ -45,6 +42,7 @@ dependencies = [ docs = "sphinx-build -M html docs docs/build" [project.urls] -Source = "https://github.com/simular-fi/simular" -Documentation = "https://simular.readthedocs.io/en/latest/" -Issues = "https://github.com/simular-fi/simular/issues" +homepage = "https://github.com/simular-fi/simular" +repository = "https://github.com/simular-fi/simular" +documentation = "https://simular.readthedocs.io/en/latest/" +issues = "https://github.com/simular-fi/simular/issues" From dcfdc04a1dc451cf0dba8b2dbd147b628012cabb Mon Sep 17 00:00:00 2001 From: Dave Bryson Date: Sat, 5 Apr 2025 10:08:27 -0500 Subject: [PATCH 15/17] mac build test --- .github/workflows/build-linux.yml | 29 +------- .github/workflows/build-mac.yml | 33 --------- .github/workflows/build-publish.yml | 103 ++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 59 deletions(-) delete mode 100644 .github/workflows/build-mac.yml create mode 100644 .github/workflows/build-publish.yml diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index aad62ee..aea4d8e 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -14,34 +14,11 @@ jobs: if: github.ref != 'refs/heads/uppy03' run: echo '::warning ::test only be run from the uppy03 branch!' && exit 1 - linux: - runs-on: ubuntu-latest - strategy: - matrix: - target: [x86_64, aarch64, armv7] - needs: [check_pyo3] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - manylinux: manylinux_2_28 - container: "ghcr.io/rust-cross/manylinux_2_28-cross:${{ matrix.target }}" - before-script-linux: | - sudo apt-get update - sudo apt-get install --yes --upgrade build-essential cmake protobuf-compiler libssl-dev glibc-source musl-tools - - windows: - runs-on: windows-latest + macos: + runs-on: macos-latest strategy: matrix: - target: [x64, x86] + target: [x86_64, aarch64] needs: [check_pyo3] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/build-mac.yml b/.github/workflows/build-mac.yml deleted file mode 100644 index 918b50d..0000000 --- a/.github/workflows/build-mac.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: build-mac - -on: - workflow_dispatch - -jobs: - check_pyo3: - runs-on: ubuntu-latest - steps: - - name: Release from main - if: github.ref == 'refs/heads/uppy03' - run: echo '::notice ::Building and uploading release' - - name: No release - if: github.ref != 'refs/heads/uppy03' - run: echo '::warning ::test only be run from the uppy03 branch!' && exit 1 - - macos: - runs-on: macos-latest - strategy: - matrix: - target: [x86_64, aarch64] - needs: [check_pyo3] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} diff --git a/.github/workflows/build-publish.yml b/.github/workflows/build-publish.yml new file mode 100644 index 0000000..b40294c --- /dev/null +++ b/.github/workflows/build-publish.yml @@ -0,0 +1,103 @@ +name: build-publish + +on: + workflow_dispatch + +jobs: + check_main: + runs-on: ubuntu-latest + steps: + - name: Release from main + if: github.ref == 'refs/heads/master' + run: echo '::notice ::Building and uploading release' + - name: No release + if: github.ref != 'refs/heads/master' + run: echo '::warning ::Release can only be run from the main branch!' && exit 1 + + linux: + runs-on: ubuntu-latest + strategy: + matrix: + target: [x86_64, aarch64, armv7] + needs: [check_main] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: manylinux_2_28 + container: "ghcr.io/rust-cross/manylinux_2_28-cross:${{ matrix.target }}" + before-script-linux: | + sudo apt-get update + sudo apt-get install --yes --upgrade build-essential cmake protobuf-compiler libssl-dev glibc-source musl-tools + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-linux-${{ matrix.target }} + path: dist + + windows: + runs-on: windows-latest + strategy: + matrix: + target: [x64, x86] + needs: [check_main] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-windows-${{ matrix.target }} + path: dist + + sdist: + runs-on: ubuntu-latest + needs: [check_main] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + - name: Upload sdist + uses: actions/upload-artifact@v4 + with: + name: wheels-sdist + path: dist + + publish_pypi: + runs-on: ubuntu-latest + needs: [linux, windows, macos, sdist] + environment: + name: pypi + url: https://pypi.org/p/simular-evm + permissions: + id-token: write + steps: + - name: Download dists + uses: actions/download-artifact@v4 + with: + pattern: wheels-* + merge-multiple: true + path: dist/ + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@v1.8.14 From 594b7da7aef4bcee9152abbf5ebb7a6543e9b89d Mon Sep 17 00:00:00 2001 From: Dave Bryson Date: Sat, 5 Apr 2025 10:17:55 -0500 Subject: [PATCH 16/17] upgrades, workflow fixes --- .github/workflows/build-linux.yml | 33 -------- .github/workflows/build-publish.yml | 25 +++++- .github/workflows/build-release.yml | 126 ---------------------------- 3 files changed, 24 insertions(+), 160 deletions(-) delete mode 100644 .github/workflows/build-linux.yml delete mode 100644 .github/workflows/build-release.yml diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml deleted file mode 100644 index aea4d8e..0000000 --- a/.github/workflows/build-linux.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: build-linux - -on: - workflow_dispatch - -jobs: - check_pyo3: - runs-on: ubuntu-latest - steps: - - name: Release from main - if: github.ref == 'refs/heads/uppy03' - run: echo '::notice ::Building and uploading release' - - name: No release - if: github.ref != 'refs/heads/uppy03' - run: echo '::warning ::test only be run from the uppy03 branch!' && exit 1 - - macos: - runs-on: macos-latest - strategy: - matrix: - target: [x86_64, aarch64] - needs: [check_pyo3] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} diff --git a/.github/workflows/build-publish.yml b/.github/workflows/build-publish.yml index b40294c..aa95084 100644 --- a/.github/workflows/build-publish.yml +++ b/.github/workflows/build-publish.yml @@ -12,7 +12,30 @@ jobs: run: echo '::notice ::Building and uploading release' - name: No release if: github.ref != 'refs/heads/master' - run: echo '::warning ::Release can only be run from the main branch!' && exit 1 + run: echo '::warning ::Release can only be run from the master branch!' && exit 1 + + macos: + runs-on: macos-latest + strategy: + matrix: + target: [x86_64, aarch64] + needs: [check_main] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-macos-${{ matrix.target }} + path: dist linux: runs-on: ubuntu-latest diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml deleted file mode 100644 index 625abb3..0000000 --- a/.github/workflows/build-release.yml +++ /dev/null @@ -1,126 +0,0 @@ -name: build-and-release - -on: - workflow_dispatch - -jobs: - check_main: - runs-on: ubuntu-latest - steps: - - name: Release from main - if: github.ref == 'refs/heads/master' - run: echo '::notice ::Building and uploading release' - - name: No release - if: github.ref != 'refs/heads/master' - run: echo '::warning ::Release can only be run from the main branch!' && exit 1 - - linux: - runs-on: ubuntu-latest - strategy: - matrix: - target: [x86_64, aarch64, armv7] - needs: [check_main] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter - sccache: 'true' - manylinux: manylinux_2_28 - container: "ghcr.io/rust-cross/manylinux_2_28-cross:${{ matrix.target }}" - before-script-linux: | - sudo apt-get update - sudo apt-get install --yes --upgrade build-essential cmake protobuf-compiler libssl-dev glibc-source musl-tools - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheels-linux-${{ matrix.target }} - path: dist - - windows: - runs-on: windows-latest - strategy: - matrix: - target: [x64, x86] - needs: [check_main] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter - sccache: 'true' - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheels-windows-${{ matrix.target }} - path: dist - - macos: - runs-on: macos-latest - strategy: - matrix: - target: [x86_64, aarch64] - needs: [check_main] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter - sccache: 'true' - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheels-macos-${{ matrix.target }} - path: dist - - sdist: - runs-on: ubuntu-latest - needs: [check_main] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - name: Build sdist - uses: PyO3/maturin-action@v1 - with: - command: sdist - args: --out dist - - name: Upload sdist - uses: actions/upload-artifact@v4 - with: - name: wheels-sdist - path: dist - - publish_pypi: - runs-on: ubuntu-latest - needs: [linux, windows, macos, sdist] - environment: - name: pypi - url: https://pypi.org/p/simular-evm - permissions: - id-token: write - steps: - - name: Download dists - uses: actions/download-artifact@v4 - with: - pattern: wheels-* - merge-multiple: true - path: dist/ - - name: Publish distribution 📦 to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.14 From afe1b4673c38ff38d6e67d2362f76e4cbefe1810 Mon Sep 17 00:00:00 2001 From: Dave Bryson Date: Sat, 5 Apr 2025 11:34:46 -0500 Subject: [PATCH 17/17] fixed maturin metadata issues --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 634be8d..30c28e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["maturin>=1.1,<2.0"] +requires = ["maturin==1.7.4"] build-backend = "maturin" [tool.maturin] @@ -33,7 +33,7 @@ dependencies = ["eth-abi>=4.1.0", "eth-utils>=2.2.0"] dependencies = [ "pytest>=7.4.0", "black>=23.7.0", - "maturin>=1.6.0", + "maturin==1.7.4", "sphinx>=7.2.6", "sphinx_rtd_theme>=2.0.0", ]