diff --git a/Cargo.lock b/Cargo.lock index 0c67f6f..0cfe4fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,54 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloy-json-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fe46acf61ad5bd7a5c21839bb2526358382fb8a6526f7ba393bdf593e2ae43f" +dependencies = [ + "alloy-primitives", + "alloy-sol-type-parser", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-primitives" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e416903084d3392ebd32d94735c395d6709415b76c7728e594d3f996f2b03e65" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "hex-literal", + "itoa", + "proptest", + "ruint", + "serde", + "tiny-keccak", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f938f00332d63a5b0ac687bd6f46d03884638948921d9f8b50c59563d421ae25" +dependencies = [ + "arrayvec", + "bytes", + "smol_str", +] + +[[package]] +name = "alloy-sol-type-parser" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "627a32998aee7a7eedd351e9b6d4cacef9426935219a3a61befa332db1be5ca3" + [[package]] name = "anstream" version = "0.5.0" @@ -391,6 +439,7 @@ dependencies = [ name = "cargo-stylus" version = "0.1.5" dependencies = [ + "alloy-json-abi", "brotli2", "bytes", "bytesize", @@ -402,6 +451,7 @@ dependencies = [ "serde", "serde_json", "thiserror", + "tiny-keccak", "tokio", "wasmer", ] @@ -578,6 +628,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "corosensei" version = "0.1.4" @@ -858,8 +914,10 @@ version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ + "convert_case", "proc-macro2", "quote", + "rustc_version", "syn 1.0.109", ] @@ -1727,6 +1785,15 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" [[package]] name = "hmac" @@ -2018,7 +2085,7 @@ dependencies = [ "lalrpop-util", "petgraph", "regex", - "regex-syntax", + "regex-syntax 0.7.5", "string_cache", "term", "tiny-keccak", @@ -2049,6 +2116,12 @@ version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + [[package]] name = "linux-raw-sys" version = "0.4.5" @@ -2200,6 +2273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2580,6 +2654,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e35c06b98bf36aba164cc17cb25f7e232f5c4aeea73baa14b8a9f0d92dbfa65" +dependencies = [ + "bit-set", + "bitflags 1.3.2", + "byteorder", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax 0.6.29", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "ptr_meta" version = "0.1.4" @@ -2600,6 +2694,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.33" @@ -2645,6 +2745,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" version = "1.7.0" @@ -2717,7 +2826,7 @@ dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax", + "regex-syntax 0.7.5", ] [[package]] @@ -2728,9 +2837,15 @@ checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.7.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.7.5" @@ -2882,6 +2997,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ruint" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95294d6e3a6192f3aabf91c38f56505a625aa495533442744185a36d75a790c4" +dependencies = [ + "proptest", + "rand", + "ruint-macro", + "serde", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e666a5496a0b2186dbcd0ff6106e29e093c15591bde62c20d3842007c6978a09" + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2972,6 +3107,18 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.15" @@ -3259,6 +3406,15 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +[[package]] +name = "smol_str" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74212e6bbe9a4352329b2f68ba3130c15a3f26fe88ff22dbdc6cdd58fa85e99c" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.4.9" @@ -3711,6 +3867,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -3789,12 +3951,27 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.3.3" diff --git a/Cargo.toml b/Cargo.toml index a5d3695..5dcbe7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,11 @@ clap = { version = "4.3.17", features = [ "derive" ] } eyre = "0.6.8" hex = "0.4.3" serde = { version = "1.0.174", features = ["derive"] } +alloy-json-abi = "0.3.2" bytesize = "1.2.0" ethers = "2.0.8" serde_json = "1.0.103" tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread" ] } wasmer = "3.1.0" thiserror = "1.0.47" +tiny-keccak = { version = "2.0.2", features = ["keccak"] } diff --git a/src/c/gen.rs b/src/c/gen.rs new file mode 100644 index 0000000..dc98f4e --- /dev/null +++ b/src/c/gen.rs @@ -0,0 +1,274 @@ +// Copyright 2023, Offchain Labs, Inc. +// For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/main/licenses/COPYRIGHT.md + +use alloy_json_abi::{Function, JsonAbi, StateMutability}; +use eyre::bail; +use serde_json::Value; +use std::fs; +use std::io::BufReader; +use std::{collections::HashMap, fmt::Write}; +use tiny_keccak::{Hasher, Keccak}; + +fn c_bytearray_initializer(val: &[u8]) -> String { + let inner: Vec<_> = val.iter().map(|byte| format!("0x{byte:02x}")).collect(); + format!("{{{}}}", inner.join(", ")) +} + +pub fn c_gen(in_path: String, out_path: String) -> eyre::Result<()> { + let f = fs::File::open(&in_path)?; + + let input: Value = serde_json::from_reader(BufReader::new(f))?; + + let Some(input_contracts) = input["contracts"].as_object() else { + bail!("did not find top-level contracts object in {}", in_path) + }; + + let mut pathbuf = std::path::PathBuf::new(); + pathbuf.push(out_path); + + for (solidity_file_name, solidity_file_out) in input_contracts { + let debug_path = vec![solidity_file_name.as_str()]; + let Some(contracts) = solidity_file_out.as_object() else { + println!("skipping output for {:?} not an object..", &debug_path); + continue; + }; + pathbuf.push(solidity_file_name); + fs::create_dir_all(&pathbuf)?; + + for (contract_name, contract_val) in contracts { + let mut debug_path = debug_path.clone(); + debug_path.push(contract_name); + + let Some(properties) = contract_val.as_object() else { + println!("skipping output for {:?} not an object..", &debug_path); + continue; + }; + + let mut methods: HashMap> = HashMap::default(); + + if let Some(raw) = properties.get("abi") { + // Sadly, JsonAbi = serde_json::from_value is not supported. + // Tonight, we hack! + let abi_json = serde_json::to_string(raw)?; + let abi: JsonAbi = serde_json::from_str(&abi_json)?; + for function in abi.functions() { + let name = function.name.clone(); + methods.entry(name).or_default().push(function.clone()); + } + } else { + println!("skipping abi for {:?}: not found", &debug_path); + } + + let mut header = String::default(); + let mut router = String::default(); + + for (simple_name, mut overloads) in methods { + overloads.sort_by_key(|a| a.signature()); + + for (index, overload) in overloads.iter().enumerate() { + let c_name = match index { + 0 => simple_name.clone(), + x => format!("{simple_name}_{x}"), + }; + let selector = u32::from_be_bytes(overload.selector()); + + let (hdr_params, call_params, payable) = match overload.state_mutability { + StateMutability::Pure => { + ("(uint8_t *input, size_t len)", "(input, len)", false) + } + StateMutability::View => ( + "(const void *storage, uint8_t *input, size_t len)", + "(NULL, input, len)", + false, + ), + StateMutability::NonPayable => ( + "(void *storage, uint8_t *input, size_t len)", + "(NULL, input, len)", + false, + ), + StateMutability::Payable => ( + "(void *storage, uint8_t *input, size_t len, bebi32 value)", + "(NULL, input, len, value)", + true, + ), + }; + + let sig = &overload.signature(); + writeln!( + header, + "#define SELECTOR_{c_name} 0x{selector:08x} // {sig}" + )?; + writeln!(header, "ArbResult {c_name}{hdr_params}; // {sig}")?; + + writeln!(router, " if (selector==SELECTOR_{c_name}) {{")?; + if !payable { + writeln!(router, " if (!bebi32_is_zero(value)) revert();")?; + } + writeln!(router, " return {c_name}{call_params};\n }}")?; + } + } + + if !header.is_empty() { + header.push('\n'); + } + debug_path.push("storageLayout"); + + if let Some(Value::Object(layout_vals)) = properties.get("storageLayout") { + debug_path.push("storage"); + + if let Some(Value::Array(storage_arr)) = layout_vals.get("storage") { + for storage_val in storage_arr { + let Some(storage_obj) = storage_val.as_object() else { + println!("skipping output inside {debug_path:?}: not an object.."); + continue; + }; + let Some(Value::String(label)) = storage_obj.get("label") else { + println!("skipping output inside {debug_path:?}: no label.."); + continue; + }; + let Some(Value::String(slot)) = storage_obj.get("slot") else { + println!("skipping output inside {debug_path:?}: no slot.."); + continue; + }; + let Ok(slot) = slot.parse::() else { + println!("skipping output inside {debug_path:?}: slot not u64.."); + continue; + }; + let Some(Value::String(val_type)) = storage_obj.get("type") else { + println!("skipping output inside {debug_path:?}: no type.."); + continue; + }; + let Some(Value::Number(read_offset)) = storage_obj.get("offset") else { + println!("skipping output inside {debug_path:?}: no offset.."); + continue; + }; + let offset = match read_offset.as_i64() { + None => { + println!( + "skipping output inside {debug_path:?}: unexpected offset..", + ); + continue; + } + Some(num) => { + if !(0..=32).contains(&num) { + println!( + "skipping output inside {debug_path:?}: unexpected offset..", + ); + continue; + }; + 32 - num + } + }; + let mut slot_buf = vec![0u8; 32 - 8]; + slot_buf.extend(slot.to_be_bytes()); + + writeln!( + header, + "#define STORAGE_SLOT_{label} {} // {val_type}", + c_bytearray_initializer(&slot_buf), + )?; + if val_type.starts_with("t_array(") { + if val_type.ends_with(")dyn_storage") { + let mut keccak = Keccak::v256(); + keccak.update(&slot_buf); + keccak.finalize(&mut slot_buf); + writeln!( + header, + "#define STORAGE_BASE_{label} {} // {val_type}", + c_bytearray_initializer(&slot_buf), + )?; + } + } else if !val_type.starts_with("t_mapping") { + writeln!( + header, + "#define STORAGE_END_OFFSET_{label} {offset} // {val_type}", + )?; + } + } + } else { + println!("skipping output for {debug_path:?}: not an array.."); + } + debug_path.pop(); + } else { + println!("skipping output for {:?}: not an object..", &debug_path); + } + debug_path.pop(); + if !header.is_empty() { + let mut unique_identifier = String::from("__"); + unique_identifier += &solidity_file_name.to_uppercase(); + unique_identifier += "_"; + unique_identifier += &contract_name.to_uppercase(); + unique_identifier += "_"; + + let contents = format!( + r#" // autogenerated by cargo-stylus +#ifndef {uniq} +#define {uniq} + +#include +#include + +#ifdef __cplusplus +extern "C" {{ +#endif + +ArbResult default_func(void *storage, uint8_t *input, size_t len, bebi32 value); + +{body} + +#ifdef __cplusplus +}} +#endif + +#endif // {uniq} +"#, + uniq = unique_identifier, + body = header + ); + + let filename: String = contract_name.into(); + pathbuf.push(filename + ".h"); + fs::write(&pathbuf, &contents)?; + pathbuf.pop(); + } + if !router.is_empty() { + let contents = format!( + r#" // autogenerated by cargo-stylus + +#include "{contract}.h" +#include +#include +#include + + +ArbResult {contract}_entry(uint8_t *input, size_t len) {{ + bebi32 value; + msg_value(value); + if (len < 4) {{ + return default_func(NULL, input, len, value); + }} + uint32_t selector = bebi_get_u32(input, 0); + input +=4; + len -=4; +{body} + input -=4; + len +=4; + return default_func(NULL, input, len, value); +}} + +ENTRYPOINT({contract}_entry) +"#, + contract = contract_name, + body = router + ); + + let filename: String = contract_name.into(); + pathbuf.push(filename + "_main.c"); + fs::write(&pathbuf, &contents)?; + pathbuf.pop(); + } + } + pathbuf.pop(); + } + Ok(()) +} diff --git a/src/c/mod.rs b/src/c/mod.rs new file mode 100644 index 0000000..dd261c3 --- /dev/null +++ b/src/c/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2023, Offchain Labs, Inc. +// For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/main/licenses/COPYRIGHT.md + +pub mod gen; diff --git a/src/main.rs b/src/main.rs index e6f777f..7f83e13 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use clap::{Args, Parser, ValueEnum}; use color::Color; use ethers::types::H160; +mod c; mod check; mod color; mod constants; @@ -21,6 +22,15 @@ mod wallet; #[command(bin_name = "cargo")] enum CargoCli { Stylus(StylusArgs), + CGen(CGenArgs), // not behind the stylus command, to hide it from rust-developers. +} + +#[derive(Parser, Debug)] +#[command(name = "c_generate")] +struct CGenArgs { + #[arg(required = true)] + input: String, + out_dir: String, } #[derive(Parser, Debug)] @@ -158,7 +168,12 @@ pub struct TxSendingOpts { #[tokio::main] async fn main() -> eyre::Result<()> { - let CargoCli::Stylus(args) = CargoCli::parse(); + let args = match CargoCli::parse() { + CargoCli::Stylus(args) => args, + CargoCli::CGen(args) => { + return c::gen::c_gen(args.input, args.out_dir); + } + }; match args.command { StylusSubcommands::New { name, minimal } => {