diff --git a/.gitignore b/.gitignore index fa662180..547109bd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .idea .envrc +*.log # non git-controlled code and tools /tools/downloaded diff --git a/doc/website/sphinx-err-tm1s_x28.log b/doc/website/sphinx-err-tm1s_x28.log deleted file mode 100644 index 9bd28680..00000000 --- a/doc/website/sphinx-err-tm1s_x28.log +++ /dev/null @@ -1,34 +0,0 @@ -Versions -======== - -* Platform: linux; (Linux-6.12.76-x86_64-with-glibc2.40) -* Python version: 3.12.8 (CPython) -* Sphinx version: 8.2.3 -* Docutils version: 0.21.2 -* Jinja2 version: 3.1.6 -* Pygments version: 2.19.2 - -Last Messages -============= - -None. - -Loaded Extensions -================= - -None. - -Traceback -========= - - Traceback (most recent call last): - File "/home/kp2pml30/.cache/pypoetry/virtualenvs/genvm-docstypesgen-tCkolNvS-py3.12/lib/python3.12/site-packages/sphinx/cmd/build.py", line 414, in build_main - app = Sphinx( - ^^^^^^^ - File "/home/kp2pml30/.cache/pypoetry/virtualenvs/genvm-docstypesgen-tCkolNvS-py3.12/lib/python3.12/site-packages/sphinx/application.py", line 298, in __init__ - ensuredir(self.outdir) - File "/home/kp2pml30/.cache/pypoetry/virtualenvs/genvm-docstypesgen-tCkolNvS-py3.12/lib/python3.12/site-packages/sphinx/util/osutil.py", line 71, in ensuredir - os.makedirs(file, exist_ok=True) - File "", line 215, in makedirs - File "", line 225, in makedirs - OSError: [Errno 30] Read-only file system: '/tmp/claude' diff --git a/doc/website/src/conf.py b/doc/website/src/conf.py index a09ef864..3b1dd4e6 100644 --- a/doc/website/src/conf.py +++ b/doc/website/src/conf.py @@ -19,7 +19,12 @@ project = 'GenVM SDK' -copyright = f'{datetime.date.today().year}, GenLayer Labs' +copyright_year = str(datetime.date.today().year) +if 'COPYRIGHT_YEAR' in os.environ: + copyright_year = os.environ['COPYRIGHT_YEAR'] + + +copyright = f'{copyright_year}, GenLayer Labs' author = 'GenLayer Labs' release = os.environ.get('DOCS_VERSION', 'main') version = release diff --git a/executor/Cargo.lock b/executor/Cargo.lock index 07a00844..1d3de20e 100644 --- a/executor/Cargo.lock +++ b/executor/Cargo.lock @@ -948,7 +948,7 @@ dependencies = [ [[package]] name = "genlayer_calldata" -version = "0.0.1" +version = "0.0.2" dependencies = [ "arbitrary", "bytes", @@ -961,7 +961,7 @@ dependencies = [ [[package]] name = "genlayer_sdk" -version = "0.0.1" +version = "0.0.2" dependencies = [ "arbitrary", "bytes", @@ -976,7 +976,7 @@ dependencies = [ [[package]] name = "genvm" -version = "0.1.0" +version = "0.3.0" dependencies = [ "afl", "anyhow", diff --git a/executor/Cargo.toml b/executor/Cargo.toml index 96c84a49..e994d2dd 100644 --- a/executor/Cargo.toml +++ b/executor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "genvm" -version = "0.1.0" +version = "0.3.0" edition = "2021" [profile.dev.package.wasmtime] @@ -15,10 +15,6 @@ panic = "abort" debug = true panic = "abort" -[lints.rust] -# this is needed for wasmtime :( -static_mut_refs = "allow" - [[example]] name = "fuzz-genvm-storage" path = "fuzz/genvm-storage.rs" diff --git a/executor/crates/calldata/Cargo.lock b/executor/crates/calldata/Cargo.lock index 10cf52d9..87a7e5a6 100644 --- a/executor/crates/calldata/Cargo.lock +++ b/executor/crates/calldata/Cargo.lock @@ -162,7 +162,7 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "genlayer_calldata" -version = "0.0.1" +version = "0.0.2" dependencies = [ "arbitrary", "bytes", diff --git a/executor/crates/calldata/Cargo.toml b/executor/crates/calldata/Cargo.toml index 3147657a..48903cb4 100644 --- a/executor/crates/calldata/Cargo.toml +++ b/executor/crates/calldata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "genlayer_calldata" -version = "0.0.1" +version = "0.0.2" edition = "2024" repository = "https://github.com/genlayerlabs/genvm" description = "Crate for handling GenLayer calldata encoding and decoding" @@ -8,9 +8,9 @@ readme = "README.md" license-file = "LICENSE.txt" homepage = "https://genlayer.com" keywords = ["genlayer", "blockchain", "smart-contracts", "wasm", "wasi"] -categories = ["api-bindings", "wasm"] +categories = ["encoding", "parser-implementations"] exclude = [ - "/tests/**", + "/tests/**", "/fuzz/**" ] [features] diff --git a/executor/crates/calldata/LICENSE.txt b/executor/crates/calldata/LICENSE.txt new file mode 100644 index 00000000..cfc7ef11 --- /dev/null +++ b/executor/crates/calldata/LICENSE.txt @@ -0,0 +1,66 @@ +Business Source License 1.1 +License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. "Business Source License" is a trademark of MariaDB Corporation Ab. + + +Parameters +Licensor: GenLayer Labs Corp. +Licensed Work: GenLayer GenVM, Copyright © 2024 GenLayer Labs Corp. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software subject to the terms listed below. + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +Change Date: Four years from the date the Licensed Work is published +Change License: Apache License, Version 2.0 +Terms + +You are permitted to use, copy, and modify the Licensed Work, subject to +the following conditions: + +1. The Software may be used solely with the GenLayer ecosystem. +2. Any use of the Software to enable or interact with deployments on blockchains +not approved by GenLayer governance is strictly prohibited. + +3. The Software is provided “as is,” without warranty of any kind, express or +implied, including but not limited to the warranties of merchantability, +fitness for a particular purpose, and noninfringement. In no event shall +the authors or copyright holders be liable for any claim, damages, or other +liability, whether in an action of contract, tort, or otherwise, arising +from, out of, or in connection with the Software or the use or other +dealings in the Software. +The Business Source License (this document, or the “License”) is not an Open +Source license. However, the Licensed Work will eventually be made available +under an Open Source License, as stated in this License. + +Forks and Derivative Works: +You may create forks or derivative works of the Software, provided that such +works are used exclusively within the GenLayer ecosystem and do not conflict +with the conditions above. Any violation of these terms will result in the +immediate termination of this license. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work, and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +For additional terms, questions, or licensing requests, please contact +contact@genlayerlabs.com diff --git a/executor/crates/sdk-rs/.ya-test-config.json b/executor/crates/sdk-rs/.ya-test-config.json new file mode 100644 index 00000000..ecc4e418 --- /dev/null +++ b/executor/crates/sdk-rs/.ya-test-config.json @@ -0,0 +1,8 @@ +{ + "cargo_test_flags": [ + ], + "cargo_afl_build_flags": [ + "--features", "arbitrary,storage,dont_define_wasi_storage" + ], + "keep_env": [] +} diff --git a/executor/crates/sdk-rs/Cargo.lock b/executor/crates/sdk-rs/Cargo.lock index 2c966ed4..2ac0be22 100644 --- a/executor/crates/sdk-rs/Cargo.lock +++ b/executor/crates/sdk-rs/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "afl" +version = "0.15.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927cd71710d1a232519e2393470e8f74a178ae59367efe58fa122884bba35ca4" +dependencies = [ + "home", + "libc", + "rustc_version", + "xdg", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -16,6 +28,9 @@ name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "arrayvec" @@ -41,6 +56,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -124,12 +148,52 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crunchy" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -160,9 +224,19 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "genlayer_calldata" -version = "0.0.1" +version = "0.0.2" dependencies = [ "arbitrary", "bytes", @@ -175,8 +249,9 @@ dependencies = [ [[package]] name = "genlayer_sdk" -version = "0.0.1" +version = "0.0.2" dependencies = [ + "afl", "arbitrary", "bytes", "chrono", @@ -186,6 +261,7 @@ dependencies = [ "primitive-types", "serde", "serde_bytes", + "sha3", ] [[package]] @@ -211,6 +287,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -284,6 +369,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + [[package]] name = "libc" version = "0.2.180" @@ -455,12 +549,27 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -501,6 +610,16 @@ dependencies = [ "syn", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "shlex" version = "1.3.0" @@ -560,6 +679,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "uint" version = "0.10.0" @@ -584,6 +709,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -694,6 +825,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "winnow" version = "0.7.14" @@ -712,6 +852,12 @@ dependencies = [ "tap", ] +[[package]] +name = "xdg" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" + [[package]] name = "zerocopy" version = "0.8.34" diff --git a/executor/crates/sdk-rs/Cargo.toml b/executor/crates/sdk-rs/Cargo.toml index 9ebdf486..087aa4d5 100644 --- a/executor/crates/sdk-rs/Cargo.toml +++ b/executor/crates/sdk-rs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "genlayer_sdk" -version = "0.0.1" +version = "0.0.2" edition = "2024" repository = "https://github.com/genlayerlabs/genvm" description = "Genlayer SDK for building smart contracts in rust" @@ -10,7 +10,7 @@ homepage = "https://genlayer.com" keywords = ["genlayer", "blockchain", "smart-contracts", "wasm", "wasi"] categories = ["api-bindings", "wasm"] exclude = [ - "/tests/**", + "/tests/**", "/fuzz/**" ] # Example contract that fetches a webpage @@ -19,21 +19,41 @@ exclude = [ name = "fetch_webpage" path = "examples/fetch_webpage.rs" +[[example]] +name = "storage_dyn_array" +path = "examples/storage_dyn_array.rs" + +[[example]] +name = "storage_root" +path = "examples/storage_root.rs" + +[[example]] +name = "fuzz-gvm-tree-map" +path = "fuzz/gvm-tree-map.rs" +required-features = ["arbitrary", "storage", "dont_define_wasi_storage"] + [features] -default = ["wasi"] +default = ["wasi", "storage"] wasi = [] +dont_define_wasi_storage = [] +storage = ["dep:sha3", "wasi"] arbitrary = ["dep:arbitrary", "genlayer_calldata/arbitrary"] [dependencies] hex = "0.4.3" num-bigint = {version = "0.4.6", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } -arbitrary = { version = "1.0", optional = true } +arbitrary = { version = "1.0", optional = true, features = ["derive"] } primitive-types = { version = "0.13", features = ["impl-serde"] } serde_bytes = { version = "0.11.19"} bytes = {version = "1.11.0", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] } -genlayer_calldata = { path = "../calldata" } +sha3 = { version = "0.10", optional = true } + +[dependencies.genlayer_calldata] +path = "../calldata" +version = "0.0.2" [dev-dependencies] primitive-types = { version = "0.13", features = ["impl-serde"] } +afl = { version = "0.15.18", features = ["no_cfg_fuzzing"] } diff --git a/executor/crates/sdk-rs/examples/storage_dyn_array.rs b/executor/crates/sdk-rs/examples/storage_dyn_array.rs new file mode 100644 index 00000000..b792ff7d --- /dev/null +++ b/executor/crates/sdk-rs/examples/storage_dyn_array.rs @@ -0,0 +1,62 @@ +//! Example contract demonstrating DynArray storage. +//! +//! On deploy (is_init=true), populates a DynArray and prints its contents. + +use genlayer_sdk::abi::entry::MessageData; +use genlayer_sdk::abi::entry::contract_def::Contract; +use genlayer_sdk::calldata::Value; +use genlayer_sdk::storage::{DynArray, Slot, StorageType}; + +const ROOT_SLOT: Slot = Slot([0u8; 32]); + +#[derive(Default)] +pub struct StorageDynArrayExample; + +impl Contract for StorageDynArrayExample { + fn handle_main(&mut self, _message: MessageData, _data: bytes::Bytes) -> Result { + let arr = >::handle_at(ROOT_SLOT, 0); + + // populate + arr.append_slot().set(10); + arr.append_slot().set(20); + arr.append_slot().set(30); + + // iterate and print + let len = arr.len(); + println!("len={len}"); + for i in 0..len { + let val = arr.index(i).get(); + println!("[{i}]={val}"); + } + + // pop last, print again + arr.pop(); + let len = arr.len(); + println!("after pop len={len}"); + for i in 0..len { + let val = arr.index(i).get(); + println!("[{i}]={val}"); + } + + Ok(Value::Null) + } + + fn handle_sandbox( + &mut self, + _message: MessageData, + _data: bytes::Bytes, + ) -> Result, String> { + unimplemented!() + } + + fn handle_consensus_stage( + &mut self, + _message: MessageData, + _data: bytes::Bytes, + _stage_data: genlayer_sdk::abi::entry::contract_def::ConsensusStageData, + ) -> Result { + unimplemented!() + } +} + +genlayer_sdk::contract_main!(StorageDynArrayExample); diff --git a/executor/crates/sdk-rs/examples/storage_root.rs b/executor/crates/sdk-rs/examples/storage_root.rs new file mode 100644 index 00000000..cbe66ecb --- /dev/null +++ b/executor/crates/sdk-rs/examples/storage_root.rs @@ -0,0 +1,58 @@ +//! Example contract demonstrating Root access, code introspection, and contract data. + +use genlayer_sdk::abi::entry::MessageData; +use genlayer_sdk::abi::entry::contract_def::Contract; +use genlayer_sdk::calldata::Value; +use genlayer_sdk::storage::Root; + +const MAGIC: &str = "MAGIC_MARKER_42"; + +genlayer_sdk::record!(ContractData { counter: u32 }); + +#[derive(Default)] +pub struct StorageRootExample; + +impl Contract for StorageRootExample { + fn handle_main(&mut self, _message: MessageData, _data: bytes::Bytes) -> Result { + let root = Root::::get(); + + // search own code for the magic string + let code = root.code().get(); + let code_len = code.len(); + let mut code_bytes = vec![0u8; code_len as usize]; + if code_len > 0 { + code.slot.read(code.offset + 4, &mut code_bytes); + } + let found = code_bytes + .windows(MAGIC.len()) + .any(|w| w == MAGIC.as_bytes()); + println!("contains_magic={found}"); + + // write and read contract data through Root + let data = root.contract_instance().get(); + data.counter().set(42); + let val = data.counter().get(); + println!("counter={val}"); + + Ok(Value::Null) + } + + fn handle_sandbox( + &mut self, + _message: MessageData, + _data: bytes::Bytes, + ) -> Result, String> { + unimplemented!() + } + + fn handle_consensus_stage( + &mut self, + _message: MessageData, + _data: bytes::Bytes, + _stage_data: genlayer_sdk::abi::entry::contract_def::ConsensusStageData, + ) -> Result { + unimplemented!() + } +} + +genlayer_sdk::contract_main!(StorageRootExample); diff --git a/executor/crates/sdk-rs/fuzz/gvm-tree-map.rs b/executor/crates/sdk-rs/fuzz/gvm-tree-map.rs new file mode 100644 index 00000000..13a51bb9 --- /dev/null +++ b/executor/crates/sdk-rs/fuzz/gvm-tree-map.rs @@ -0,0 +1,107 @@ +use arbitrary::Arbitrary; +use genlayer_sdk::storage::{Slot, StorageType, TreeMap}; +use std::cell::RefCell; +use std::collections::BTreeMap; + +// Global paged storage backing the SDK's __genvm_storage_read / __genvm_storage_write. + +thread_local! { + static STORAGE: RefCell>> = RefCell::new(BTreeMap::new()); +} + +fn storage_clear() { + STORAGE.with(|s| s.borrow_mut().clear()); +} + +#[unsafe(no_mangle)] +pub fn __genvm_storage_read(slot: &[u8; 32], offset: u32, buf: &mut [u8]) -> u32 { + STORAGE.with(|s| { + let s = s.borrow(); + if let Some(data) = s.get(slot) { + let off = offset as usize; + for (i, b) in buf.iter_mut().enumerate() { + *b = data.get(off + i).copied().unwrap_or(0); + } + } else { + buf.fill(0); + } + }); + 0 +} + +#[unsafe(no_mangle)] +pub fn __genvm_storage_write(slot: &[u8; 32], offset: u32, buf: &[u8]) -> u32 { + STORAGE.with(|s| { + let mut s = s.borrow_mut(); + let data = s.entry(*slot).or_default(); + let end = offset as usize + buf.len(); + if data.len() < end { + data.resize(end, 0); + } + data[offset as usize..end].copy_from_slice(buf); + }); + 0 +} + +// Fuzz operations + +#[derive(Debug, Arbitrary)] +enum Op { + Insert { key: u32, value: u32 }, + Remove { key: u32 }, + Get { key: u32 }, +} + +fn check_order(tree: & as StorageType>::Handle, reference: &BTreeMap) { + let mut entries = Vec::new(); + tree.for_each(|k, v| entries.push((k, v))); + let ref_entries: Vec<(u32, u32)> = reference.iter().map(|(&k, &v)| (k, v)).collect(); + assert_eq!( + entries, ref_entries, + "order mismatch:\n tree: {entries:?}\n reference: {ref_entries:?}" + ); +} + +fn run_fuzz(ops: Vec) { + storage_clear(); + + let slot = Slot([0u8; 32]); + let tree = as StorageType>::handle_at(slot, 0); + let mut reference = BTreeMap::::new(); + + for op in ops { + match op { + Op::Insert { key, value } => { + eprintln!("insert({key}, {value})"); + tree.insert(&key, &value); + reference.insert(key, value); + check_order(&tree, &reference); + } + Op::Remove { key } => { + eprintln!("remove({key})"); + let existed = tree.remove(&key); + let ref_existed = reference.remove(&key).is_some(); + assert_eq!( + existed, ref_existed, + "remove({key}): tree={existed}, reference={ref_existed}" + ); + check_order(&tree, &reference); + } + Op::Get { key } => { + eprintln!("get({key})"); + let tree_val = tree.get(&key).map(|h| h.get()); + let ref_val = reference.get(&key).copied(); + assert_eq!( + tree_val, ref_val, + "get({key}): tree={tree_val:?}, reference={ref_val:?}" + ); + } + } + } +} + +fn main() { + afl::fuzz!(|data: Vec| { + run_fuzz(data); + }); +} diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/06bf706b95ef039be4fd5d27876a80d246ac0b53689f743c91c59b2e07dce47d b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/06bf706b95ef039be4fd5d27876a80d246ac0b53689f743c91c59b2e07dce47d new file mode 100644 index 00000000..46412a31 Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/06bf706b95ef039be4fd5d27876a80d246ac0b53689f743c91c59b2e07dce47d differ diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/13626dcd6e9c9b80652a6ec6ff3e09583825cb8bd10216edac54b1b34fe71f84 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/13626dcd6e9c9b80652a6ec6ff3e09583825cb8bd10216edac54b1b34fe71f84 new file mode 100644 index 00000000..b63921a8 Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/13626dcd6e9c9b80652a6ec6ff3e09583825cb8bd10216edac54b1b34fe71f84 differ diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/15e75ab26bce0bbcec48076e85e1df4d5a8fe145d53a8f2e15469ddcbb997bc1 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/15e75ab26bce0bbcec48076e85e1df4d5a8fe145d53a8f2e15469ddcbb997bc1 new file mode 100644 index 00000000..4419ff93 Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/15e75ab26bce0bbcec48076e85e1df4d5a8fe145d53a8f2e15469ddcbb997bc1 differ diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/16ff5f865c8931620cbbfb4be603037909806fcdf5a07cbe1f4254372a99d942 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/16ff5f865c8931620cbbfb4be603037909806fcdf5a07cbe1f4254372a99d942 new file mode 100644 index 00000000..cfc07282 Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/16ff5f865c8931620cbbfb4be603037909806fcdf5a07cbe1f4254372a99d942 differ diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/28e459619a2eed99102830de6905611118f9a93477bf5b4ffda6f7bc3834c156 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/28e459619a2eed99102830de6905611118f9a93477bf5b4ffda6f7bc3834c156 new file mode 100644 index 00000000..95e40dd6 Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/28e459619a2eed99102830de6905611118f9a93477bf5b4ffda6f7bc3834c156 differ diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/2f002715316a6584b162d7ef973363d231b76be0edca99799f2278e342c4fb0f b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/2f002715316a6584b162d7ef973363d231b76be0edca99799f2278e342c4fb0f new file mode 100644 index 00000000..9e9ab13b --- /dev/null +++ b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/2f002715316a6584b162d7ef973363d231b76be0edca99799f2278e342c4fb0f @@ -0,0 +1 @@ +aaQgTgaaQgTgXgggXgggQgTgaaQgTgXgXggg[ \ No newline at end of file diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/36d59cebc7be1b8fdb0be89b737293504055334c3344ad6acafe0e75353f3000 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/36d59cebc7be1b8fdb0be89b737293504055334c3344ad6acafe0e75353f3000 new file mode 100644 index 00000000..cdcb6143 --- /dev/null +++ b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/36d59cebc7be1b8fdb0be89b737293504055334c3344ad6acafe0e75353f3000 @@ -0,0 +1 @@ +aRQgTgaaQgTgXgggXggg[ \ No newline at end of file diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/3734dda7ac70a63554c58f2a6cddc2f1852cdcc838728c0c0144caabf6bf4afa b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/3734dda7ac70a63554c58f2a6cddc2f1852cdcc838728c0c0144caabf6bf4afa new file mode 100644 index 00000000..e2f226fe Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/3734dda7ac70a63554c58f2a6cddc2f1852cdcc838728c0c0144caabf6bf4afa differ diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/405de5958f5e1a62339cb512b67619dba3432fbd73d47349af30198f47723cf2 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/405de5958f5e1a62339cb512b67619dba3432fbd73d47349af30198f47723cf2 new file mode 100644 index 00000000..d29bc55a --- /dev/null +++ b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/405de5958f5e1a62339cb512b67619dba3432fbd73d47349af30198f47723cf2 @@ -0,0 +1 @@ +aaQgTgaaQgTgXgggaaqgTgXgggXggXggg[ \ No newline at end of file diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/40b64828f926a7a2a5550d0ced930df92944a8c47eac3d8f4686909ce3bf5fb6 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/40b64828f926a7a2a5550d0ced930df92944a8c47eac3d8f4686909ce3bf5fb6 new file mode 100644 index 00000000..b3bdb059 --- /dev/null +++ b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/40b64828f926a7a2a5550d0ced930df92944a8c47eac3d8f4686909ce3bf5fb6 @@ -0,0 +1 @@ +aaQgTgaa[[ \ No newline at end of file diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/4f52a1b5dad497fa91c08ba19d139f9af8fc934d3962f72bbb78dfdcdcb731d8 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/4f52a1b5dad497fa91c08ba19d139f9af8fc934d3962f72bbb78dfdcdcb731d8 new file mode 100644 index 00000000..5c7c00f8 --- /dev/null +++ b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/4f52a1b5dad497fa91c08ba19d139f9af8fc934d3962f72bbb78dfdcdcb731d8 @@ -0,0 +1 @@ +aaQgTgaaQgTgXgggQgTgaaQQgTgXgggXgggXgggA \ No newline at end of file diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/52cb07109bb8b897a0520be40dbdfa940b06c0c7b531e122e9b83b8ca00e2a1b b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/52cb07109bb8b897a0520be40dbdfa940b06c0c7b531e122e9b83b8ca00e2a1b new file mode 100644 index 00000000..3bf5c0d2 --- /dev/null +++ b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/52cb07109bb8b897a0520be40dbdfa940b06c0c7b531e122e9b83b8ca00e2a1b @@ -0,0 +1 @@ +aaaQgTgaaQwTgXgggQgTgaaQgTgXgggXggggggggggggggggggggggggggggggggu \ No newline at end of file diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/5b965dc27ab7d44e4ae08567af3bac838a9381a8add883b349a623cd176dc19e b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/5b965dc27ab7d44e4ae08567af3bac838a9381a8add883b349a623cd176dc19e new file mode 100644 index 00000000..6761566f --- /dev/null +++ b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/5b965dc27ab7d44e4ae08567af3bac838a9381a8add883b349a623cd176dc19e @@ -0,0 +1 @@ +aaQgTgaagTgXgggXggg[ \ No newline at end of file diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/692a7aef65a9540c332ccb0510816dd1974d1a70d61bb89461a495031609e0c6 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/692a7aef65a9540c332ccb0510816dd1974d1a70d61bb89461a495031609e0c6 new file mode 100644 index 00000000..433cb8b7 Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/692a7aef65a9540c332ccb0510816dd1974d1a70d61bb89461a495031609e0c6 differ diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/6ecec5abea7c0910aa70611de047b8c1b693333bdde613d8c6438f3611209f44 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/6ecec5abea7c0910aa70611de047b8c1b693333bdde613d8c6438f3611209f44 new file mode 100644 index 00000000..ce8b0186 Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/6ecec5abea7c0910aa70611de047b8c1b693333bdde613d8c6438f3611209f44 differ diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/729b0e7584815906122215442c1612166bd602f6cf60a69809b2ce1791535976 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/729b0e7584815906122215442c1612166bd602f6cf60a69809b2ce1791535976 new file mode 100644 index 00000000..bdd241b0 Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/729b0e7584815906122215442c1612166bd602f6cf60a69809b2ce1791535976 differ diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/8426f8ad50f9f478c6400c3f00ff5503ddbe242ae2002e8a54087741255dca8d b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/8426f8ad50f9f478c6400c3f00ff5503ddbe242ae2002e8a54087741255dca8d new file mode 100644 index 00000000..bdd11131 --- /dev/null +++ b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/8426f8ad50f9f478c6400c3f00ff5503ddbe242ae2002e8a54087741255dca8d @@ -0,0 +1 @@ +aaQgTgaaQgTgXgggXggg[ \ No newline at end of file diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/857da7e3f1e4c714a65b5fa191d6d626dc1362749c75a4cc64beda4a635ea8ab b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/857da7e3f1e4c714a65b5fa191d6d626dc1362749c75a4cc64beda4a635ea8ab new file mode 100644 index 00000000..f471f265 Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/857da7e3f1e4c714a65b5fa191d6d626dc1362749c75a4cc64beda4a635ea8ab differ diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/a63bac3bd3da3270f434b81e778b953809c4d9d0243661f4f36c9db817848c80 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/a63bac3bd3da3270f434b81e778b953809c4d9d0243661f4f36c9db817848c80 new file mode 100644 index 00000000..bc862ea7 Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/a63bac3bd3da3270f434b81e778b953809c4d9d0243661f4f36c9db817848c80 differ diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/a6b2a65760ebc6741c1dcbc53e416a261bf484cb8f38cf9affa114e7d7564236 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/a6b2a65760ebc6741c1dcbc53e416a261bf484cb8f38cf9affa114e7d7564236 new file mode 100644 index 00000000..98b247ea Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/a6b2a65760ebc6741c1dcbc53e416a261bf484cb8f38cf9affa114e7d7564236 differ diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/b2b81cb269091ae713698d9e6603e18c1bcb68aefc6896ae7ad1cfd51b238f37 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/b2b81cb269091ae713698d9e6603e18c1bcb68aefc6896ae7ad1cfd51b238f37 new file mode 100644 index 00000000..542cec81 Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/b2b81cb269091ae713698d9e6603e18c1bcb68aefc6896ae7ad1cfd51b238f37 differ diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/b684bac922ed878281605e6deb3fa777073cfa69f370d046fe9428d7d4928192 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/b684bac922ed878281605e6deb3fa777073cfa69f370d046fe9428d7d4928192 new file mode 100644 index 00000000..e737644f Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/b684bac922ed878281605e6deb3fa777073cfa69f370d046fe9428d7d4928192 differ diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/bcb43812d8a094ae43d4e05206fc0d62b10b8ccd1ccf09b2677513dcc3d0d2f3 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/bcb43812d8a094ae43d4e05206fc0d62b10b8ccd1ccf09b2677513dcc3d0d2f3 new file mode 100644 index 00000000..c63d4673 Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/bcb43812d8a094ae43d4e05206fc0d62b10b8ccd1ccf09b2677513dcc3d0d2f3 differ diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/bdb6ea571276879dbb982d9af337abd281a6d98fe2518bc2f1a94678a2016076 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/bdb6ea571276879dbb982d9af337abd281a6d98fe2518bc2f1a94678a2016076 new file mode 100644 index 00000000..88bbd1da --- /dev/null +++ b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/bdb6ea571276879dbb982d9af337abd281a6d98fe2518bc2f1a94678a2016076 @@ -0,0 +1 @@ +aaQgTgaaQgTgXgXgggaaQgTgaaQgTgX[ \ No newline at end of file diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/d59deb58e505f32c96bfdecc96c588452cd1f508890e8d78510b05e1fabf54a8 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/d59deb58e505f32c96bfdecc96c588452cd1f508890e8d78510b05e1fabf54a8 new file mode 100644 index 00000000..b12552fe Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/d59deb58e505f32c96bfdecc96c588452cd1f508890e8d78510b05e1fabf54a8 differ diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/e53faecf732b3682f891e75ce94848061194a6db05f22032cc4d03689785010d b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/e53faecf732b3682f891e75ce94848061194a6db05f22032cc4d03689785010d new file mode 100644 index 00000000..0eb3d3ed Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/e53faecf732b3682f891e75ce94848061194a6db05f22032cc4d03689785010d differ diff --git a/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/fbd0d4accb2d7afc56dcf694df661a9adc2bee8025f0a7dc1fdc8050e98e64e3 b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/fbd0d4accb2d7afc56dcf694df661a9adc2bee8025f0a7dc1fdc8050e98e64e3 new file mode 100644 index 00000000..f6d8039e Binary files /dev/null and b/executor/crates/sdk-rs/fuzz/inputs-gvm-tree-map/fbd0d4accb2d7afc56dcf694df661a9adc2bee8025f0a7dc1fdc8050e98e64e3 differ diff --git a/executor/crates/sdk-rs/src/abi/wasi.rs b/executor/crates/sdk-rs/src/abi/wasi.rs index 771dafd0..7a5dd075 100644 --- a/executor/crates/sdk-rs/src/abi/wasi.rs +++ b/executor/crates/sdk-rs/src/abi/wasi.rs @@ -16,7 +16,7 @@ pub mod raw { unsafe extern "C" { pub fn storage_read(slot: *const u8, index: u32, buf: *mut u8, buf_len: u32) -> u32; - pub fn storage_write(slot: *const u8, index: i32, buf: *const u8, buf_len: u32) -> u32; + pub fn storage_write(slot: *const u8, index: u32, buf: *const u8, buf_len: u32) -> u32; pub fn get_balance(address: *const u8, result: *mut u8) -> u32; @@ -96,6 +96,7 @@ impl std::error::Error for WasiError {} /// # Requirements /// * Sub-VM must have read storage permission /// * `index + buf.len()` must not overflow +#[inline(always)] pub fn storage_read(slot: &[u8; 32], index: u32, buf: &mut [u8]) -> Result<(), WasiError> { let ret = unsafe { raw::storage_read(slot.as_ptr(), index, buf.as_mut_ptr(), buf.len() as u32) }; @@ -114,7 +115,8 @@ pub fn storage_read(slot: &[u8; 32], index: u32, buf: &mut [u8]) -> Result<(), W /// * Sub-VM must have write storage permission /// * `index + buf.len()` must not overflow /// * Storage slot must not be locked (unless sender is in upgraders list) -pub fn storage_write(slot: &[u8; 32], index: i32, buf: &[u8]) -> Result<(), WasiError> { +#[inline(always)] +pub fn storage_write(slot: &[u8; 32], index: u32, buf: &[u8]) -> Result<(), WasiError> { let ret = unsafe { raw::storage_write(slot.as_ptr(), index, buf.as_ptr(), buf.len() as u32) }; WasiError::from_code(ret) diff --git a/executor/crates/sdk-rs/src/lib.rs b/executor/crates/sdk-rs/src/lib.rs index 9de0ba13..ac89b1cc 100644 --- a/executor/crates/sdk-rs/src/lib.rs +++ b/executor/crates/sdk-rs/src/lib.rs @@ -1,2 +1,5 @@ pub mod abi; pub use genlayer_calldata as calldata; + +#[cfg(feature = "storage")] +pub mod storage; diff --git a/executor/crates/sdk-rs/src/storage/array.rs b/executor/crates/sdk-rs/src/storage/array.rs new file mode 100644 index 00000000..19712ae7 --- /dev/null +++ b/executor/crates/sdk-rs/src/storage/array.rs @@ -0,0 +1,171 @@ +//! Fixed-size arrays, dynamically-sized arrays, and variable-length arrays. + +use super::core::{Slot, StorageType}; + +// ===== Fixed-size array ===== +// Layout: N elements sequentially at offset + i * element_size. + +pub struct StorageArray { + pub slot: Slot, + pub offset: u32, + _marker: core::marker::PhantomData, +} + +impl Clone for StorageArray { + fn clone(&self) -> Self { + *self + } +} +impl Copy for StorageArray {} + +impl StorageArray { + pub const fn len(&self) -> usize { + N + } + + pub fn index(&self, idx: usize) -> T::Handle { + assert!(idx < N, "index {idx} out of bounds for array of length {N}"); + T::handle_at(self.slot, self.offset + idx as u32 * T::SIZE) + } +} + +impl StorageType for [T; N] { + const SIZE: u32 = T::SIZE * N as u32; + type Handle = StorageArray; + fn handle_at(slot: Slot, offset: u32) -> Self::Handle { + StorageArray { + slot, + offset, + _marker: core::marker::PhantomData, + } + } +} + +// ===== Dynamically-sized array ===== +// Layout: [u32 length] at (slot, offset), elements at slot.indirect(offset). + +/// Marker type for a dynamically-sized storage array. +pub struct DynArray(core::marker::PhantomData); + +pub struct StorageDynArray { + pub slot: Slot, + pub offset: u32, + _marker: core::marker::PhantomData, +} + +impl Clone for StorageDynArray { + fn clone(&self) -> Self { + *self + } +} +impl Copy for StorageDynArray {} + +impl StorageDynArray { + pub fn len(&self) -> u32 { + u32::handle_at(self.slot, self.offset).get() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn set_len(&self, len: u32) { + u32::handle_at(self.slot, self.offset).set(len); + } + + pub fn index(&self, idx: u32) -> T::Handle { + assert!(idx < self.len(), "index out of bounds"); + T::handle_at(self.slot.indirect(self.offset), idx * T::SIZE) + } + + /// Grow by one and return a handle to the new (uninitialized) element. + pub fn append_slot(&self) -> T::Handle { + let len = self.len(); + self.set_len(len + 1); + T::handle_at(self.slot.indirect(self.offset), len * T::SIZE) + } + + pub fn pop(&self) { + let len = self.len(); + assert!(len > 0, "can't pop from empty array"); + self.set_len(len - 1); + } + + pub fn clear(&self) { + self.set_len(0); + } +} + +impl StorageType for DynArray { + const SIZE: u32 = 4; + type Handle = StorageDynArray; + fn handle_at(slot: Slot, offset: u32) -> Self::Handle { + StorageDynArray { + slot, + offset, + _marker: core::marker::PhantomData, + } + } +} + +// ===== VLA (Variable Length Array) ===== +// Layout: [u32 length] then elements inline in the same slot (no indirection). + +/// Marker type for a variable-length array with inline storage. +pub struct VLA(core::marker::PhantomData); + +pub struct StorageVLA { + pub slot: Slot, + pub offset: u32, + _marker: core::marker::PhantomData, +} + +impl Clone for StorageVLA { + fn clone(&self) -> Self { + *self + } +} +impl Copy for StorageVLA {} + +impl StorageVLA { + pub fn len(&self) -> u32 { + u32::handle_at(self.slot, self.offset).get() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn set_len(&self, len: u32) { + u32::handle_at(self.slot, self.offset).set(len); + } + + pub fn index(&self, idx: u32) -> T::Handle { + assert!(idx < self.len(), "index out of bounds"); + T::handle_at(self.slot, self.offset + 4 + idx * T::SIZE) + } + + /// Grow by one and return a handle to the new (uninitialized) element. + pub fn append_slot(&self) -> T::Handle { + let len = self.len(); + self.set_len(len + 1); + T::handle_at(self.slot, self.offset + 4 + len * T::SIZE) + } + + pub fn truncate(&self, to: u32) { + assert!(to <= self.len(), "truncate target exceeds current length"); + self.set_len(to); + } +} + +impl StorageType for VLA { + const SIZE: u32 = u32::MAX; + type Handle = StorageVLA; + fn handle_at(slot: Slot, offset: u32) -> Self::Handle { + StorageVLA { + slot, + offset, + _marker: core::marker::PhantomData, + } + } +} diff --git a/executor/crates/sdk-rs/src/storage/core.rs b/executor/crates/sdk-rs/src/storage/core.rs new file mode 100644 index 00000000..29cea1f2 --- /dev/null +++ b/executor/crates/sdk-rs/src/storage/core.rs @@ -0,0 +1,486 @@ +//! Core storage primitives: Slot, StorageType trait, and built-in scalar types. + +#[cfg(not(all(feature = "wasi", not(feature = "dont_define_wasi_storage"))))] +mod underlying_abi { + unsafe extern "Rust" { + pub fn __genvm_storage_read(slot: &[u8; 32], offset: u32, buf: &mut [u8]) -> u32; + pub fn __genvm_storage_write(slot: &[u8; 32], offset: u32, buf: &[u8]) -> u32; + } +} + +#[cfg(all(feature = "wasi", not(feature = "dont_define_wasi_storage")))] +mod underlying_abi { + use crate::abi::wasi; + + #[inline(always)] + pub unsafe fn __genvm_storage_read(slot: &[u8; 32], offset: u32, buf: &mut [u8]) -> u32 { + unsafe { + wasi::raw::storage_read(slot.as_ptr(), offset, buf.as_mut_ptr(), buf.len() as u32) + } + } + #[inline(always)] + pub unsafe fn __genvm_storage_write(slot: &[u8; 32], offset: u32, buf: &[u8]) -> u32 { + unsafe { wasi::raw::storage_write(slot.as_ptr(), offset, buf.as_ptr(), buf.len() as u32) } + } +} + +// ===== Slot ===== + +/// A 32-byte addressed region of contract storage. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Slot(pub [u8; 32]); + +impl Slot { + pub fn read(&self, offset: u32, buf: &mut [u8]) { + if unsafe { underlying_abi::__genvm_storage_read(&self.0, offset, buf) } != 0 { + panic!("storage read failed"); + } + } + + pub fn write(&self, offset: u32, buf: &[u8]) { + if unsafe { underlying_abi::__genvm_storage_write(&self.0, offset, buf) } != 0 { + panic!("storage write failed"); + } + } + + /// Derive a child slot via `sha3_256(slot_id || offset_le_bytes)`. + pub fn indirect(&self, offset: u32) -> Slot { + use sha3::{Digest, Sha3_256}; + let mut hasher = Sha3_256::new(); + hasher.update(self.0); + hasher.update(offset.to_le_bytes()); + Slot(hasher.finalize().into()) + } +} + +// ===== StorageType trait ===== + +/// Maps a logical value type to its storage handle. +/// +/// `SIZE` is the number of bytes occupied in the parent slot. +/// `Handle` is the pointer-like struct used to read/write the value. +pub trait StorageType { + const SIZE: u32; + type Handle; + fn handle_at(slot: Slot, offset: u32) -> Self::Handle; +} + +/// Trait for storage types with a simple get/set value pattern. +/// +/// Maps a `StorageType` to an owned Rust `Value` that can be read from +/// and written to the handle. +pub trait StorageValue: StorageType { + type Value; + fn storage_get(handle: &Self::Handle) -> Self::Value; + fn storage_set(handle: &Self::Handle, val: &Self::Value); +} + +// ===== Integer types ===== + +macro_rules! impl_int_storage { + ($($rust_ty:ty => $handle_name:ident),* $(,)?) => { $( + #[derive(Clone, Copy)] + pub struct $handle_name { + pub slot: Slot, + pub offset: u32, + } + + impl $handle_name { + pub fn get(&self) -> $rust_ty { + let mut buf = [0u8; core::mem::size_of::<$rust_ty>()]; + self.slot.read(self.offset, &mut buf); + <$rust_ty>::from_le_bytes(buf) + } + + pub fn set(&self, val: $rust_ty) { + self.slot.write(self.offset, &val.to_le_bytes()); + } + } + + impl StorageType for $rust_ty { + const SIZE: u32 = core::mem::size_of::<$rust_ty>() as u32; + type Handle = $handle_name; + fn handle_at(slot: Slot, offset: u32) -> Self::Handle { + $handle_name { slot, offset } + } + } + + impl StorageValue for $rust_ty { + type Value = $rust_ty; + fn storage_get(handle: &$handle_name) -> $rust_ty { handle.get() } + fn storage_set(handle: &$handle_name, val: &$rust_ty) { handle.set(*val) } + } + )* }; +} + +impl_int_storage! { + u8 => StorageU8, + u16 => StorageU16, + u32 => StorageU32, + u64 => StorageU64, + u128 => StorageU128, + i8 => StorageI8, + i16 => StorageI16, + i32 => StorageI32, + i64 => StorageI64, + i128 => StorageI128, +} + +// ===== Float types ===== + +macro_rules! impl_float_storage { + ($($rust_ty:ty => $handle_name:ident),* $(,)?) => { $( + #[derive(Clone, Copy)] + pub struct $handle_name { + pub slot: Slot, + pub offset: u32, + } + + impl $handle_name { + pub fn get(&self) -> $rust_ty { + let mut buf = [0u8; core::mem::size_of::<$rust_ty>()]; + self.slot.read(self.offset, &mut buf); + <$rust_ty>::from_le_bytes(buf) + } + + pub fn set(&self, val: $rust_ty) { + self.slot.write(self.offset, &val.to_le_bytes()); + } + } + + impl StorageType for $rust_ty { + const SIZE: u32 = core::mem::size_of::<$rust_ty>() as u32; + type Handle = $handle_name; + fn handle_at(slot: Slot, offset: u32) -> Self::Handle { + $handle_name { slot, offset } + } + } + + impl StorageValue for $rust_ty { + type Value = $rust_ty; + fn storage_get(handle: &$handle_name) -> $rust_ty { handle.get() } + fn storage_set(handle: &$handle_name, val: &$rust_ty) { handle.set(*val) } + } + )* }; +} + +impl_float_storage! { + f32 => StorageF32, + f64 => StorageF64, +} + +// ===== Bool ===== + +#[derive(Clone, Copy)] +pub struct StorageBool { + pub slot: Slot, + pub offset: u32, +} + +impl StorageBool { + pub fn get(&self) -> bool { + let mut buf = [0u8; 1]; + self.slot.read(self.offset, &mut buf); + buf[0] != 0 + } + + pub fn set(&self, val: bool) { + self.slot.write(self.offset, &[val as u8]); + } +} + +impl StorageType for bool { + const SIZE: u32 = 1; + type Handle = StorageBool; + fn handle_at(slot: Slot, offset: u32) -> Self::Handle { + StorageBool { slot, offset } + } +} + +impl StorageValue for bool { + type Value = bool; + fn storage_get(handle: &StorageBool) -> bool { + handle.get() + } + fn storage_set(handle: &StorageBool, val: &bool) { + handle.set(*val) + } +} + +// ===== String ===== +// Layout: [u32 length] at (slot, offset), UTF-8 data at slot.indirect(offset). + +#[derive(Clone, Copy)] +pub struct StorageStr { + pub slot: Slot, + pub offset: u32, +} + +impl StorageStr { + pub fn len(&self) -> u32 { + u32::handle_at(self.slot, self.offset).get() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn set_len(&self, len: u32) { + u32::handle_at(self.slot, self.offset).set(len); + } + + pub fn load(&self) -> String { + let len = self.len() as usize; + if len == 0 { + return String::new(); + } + let indirect = self.slot.indirect(self.offset); + let mut buf = vec![0u8; len]; + indirect.read(0, &mut buf); + String::from_utf8(buf).expect("invalid utf-8 in storage string") + } + + pub fn store(&self, val: &str) { + let bytes = val.as_bytes(); + self.set_len(bytes.len() as u32); + if !bytes.is_empty() { + self.slot.indirect(self.offset).write(0, bytes); + } + } + + pub fn slice(&self, start: u32, end: u32) -> String { + assert!(start <= end && end <= self.len()); + let len = (end - start) as usize; + if len == 0 { + return String::new(); + } + let indirect = self.slot.indirect(self.offset); + let mut buf = vec![0u8; len]; + indirect.read(start, &mut buf); + String::from_utf8(buf).expect("invalid utf-8 in storage string slice") + } + + pub fn index(&self, idx: u32) -> u8 { + assert!(idx < self.len(), "index out of bounds"); + let mut buf = [0u8; 1]; + self.slot.indirect(self.offset).read(idx, &mut buf); + buf[0] + } +} + +impl StorageType for String { + const SIZE: u32 = 4; + type Handle = StorageStr; + fn handle_at(slot: Slot, offset: u32) -> Self::Handle { + StorageStr { slot, offset } + } +} + +impl StorageValue for String { + type Value = String; + fn storage_get(handle: &StorageStr) -> String { + handle.load() + } + fn storage_set(handle: &StorageStr, val: &String) { + handle.store(val) + } +} + +// ===== Bytes ===== +// Layout: [u32 length] at (slot, offset), raw data at slot.indirect(offset). + +#[derive(Clone, Copy)] +pub struct StorageBytes { + pub slot: Slot, + pub offset: u32, +} + +impl StorageBytes { + pub fn len(&self) -> u32 { + u32::handle_at(self.slot, self.offset).get() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn set_len(&self, len: u32) { + u32::handle_at(self.slot, self.offset).set(len); + } + + pub fn load(&self) -> Vec { + let len = self.len() as usize; + if len == 0 { + return Vec::new(); + } + let indirect = self.slot.indirect(self.offset); + let mut buf = vec![0u8; len]; + indirect.read(0, &mut buf); + buf + } + + pub fn store(&self, val: &[u8]) { + self.set_len(val.len() as u32); + if !val.is_empty() { + self.slot.indirect(self.offset).write(0, val); + } + } + + pub fn slice(&self, start: u32, end: u32) -> Vec { + assert!(start <= end && end <= self.len()); + let len = (end - start) as usize; + if len == 0 { + return Vec::new(); + } + let indirect = self.slot.indirect(self.offset); + let mut buf = vec![0u8; len]; + indirect.read(start, &mut buf); + buf + } + + pub fn index(&self, idx: u32) -> u8 { + assert!(idx < self.len(), "index out of bounds"); + let mut buf = [0u8; 1]; + self.slot.indirect(self.offset).read(idx, &mut buf); + buf[0] + } +} + +impl StorageType for Vec { + const SIZE: u32 = 4; + type Handle = StorageBytes; + fn handle_at(slot: Slot, offset: u32) -> Self::Handle { + StorageBytes { slot, offset } + } +} + +impl StorageValue for Vec { + type Value = Vec; + fn storage_get(handle: &StorageBytes) -> Vec { + handle.load() + } + fn storage_set(handle: &StorageBytes, val: &Vec) { + handle.store(val) + } +} + +// ===== Unit (None) ===== + +impl StorageType for () { + const SIZE: u32 = 0; + type Handle = (); + fn handle_at(_slot: Slot, _offset: u32) -> Self::Handle {} +} + +// ===== Address ===== + +#[derive(Clone, Copy)] +pub struct StorageAddress { + pub slot: Slot, + pub offset: u32, +} + +impl StorageAddress { + pub fn get(&self) -> crate::calldata::Address { + let mut buf = [0u8; 20]; + self.slot.read(self.offset, &mut buf); + crate::calldata::Address::from(buf) + } + + pub fn set(&self, val: crate::calldata::Address) { + self.slot.write(self.offset, &val.raw()); + } +} + +impl StorageType for crate::calldata::Address { + const SIZE: u32 = 20; + type Handle = StorageAddress; + fn handle_at(slot: Slot, offset: u32) -> Self::Handle { + StorageAddress { slot, offset } + } +} + +impl StorageValue for crate::calldata::Address { + type Value = crate::calldata::Address; + fn storage_get(handle: &StorageAddress) -> crate::calldata::Address { + handle.get() + } + fn storage_set(handle: &StorageAddress, val: &crate::calldata::Address) { + handle.set(*val) + } +} + +// ===== U256 ===== + +#[derive(Clone, Copy)] +pub struct StorageU256 { + pub slot: Slot, + pub offset: u32, +} + +impl StorageU256 { + pub fn get(&self) -> primitive_types::U256 { + let mut buf = [0u8; 32]; + self.slot.read(self.offset, &mut buf); + primitive_types::U256::from_little_endian(&buf) + } + + pub fn set(&self, val: primitive_types::U256) { + self.slot.write(self.offset, &val.to_little_endian()); + } +} + +impl StorageType for primitive_types::U256 { + const SIZE: u32 = 32; + type Handle = StorageU256; + fn handle_at(slot: Slot, offset: u32) -> Self::Handle { + StorageU256 { slot, offset } + } +} + +impl StorageValue for primitive_types::U256 { + type Value = primitive_types::U256; + fn storage_get(handle: &StorageU256) -> primitive_types::U256 { + handle.get() + } + fn storage_set(handle: &StorageU256, val: &primitive_types::U256) { + handle.set(*val) + } +} + +// ===== Indirection ===== +// Data lives at a derived slot. Occupies 1 byte in the parent to prevent collisions. + +/// Marker type for indirected storage. +pub struct Indirection(core::marker::PhantomData); + +pub struct StorageIndirection { + pub slot: Slot, + pub offset: u32, + _marker: core::marker::PhantomData, +} + +impl Clone for StorageIndirection { + fn clone(&self) -> Self { + *self + } +} +impl Copy for StorageIndirection {} + +impl StorageIndirection { + pub fn get(&self) -> T::Handle { + T::handle_at(self.slot.indirect(self.offset), 0) + } +} + +impl StorageType for Indirection { + const SIZE: u32 = 1; + type Handle = StorageIndirection; + fn handle_at(slot: Slot, offset: u32) -> Self::Handle { + StorageIndirection { + slot, + offset, + _marker: core::marker::PhantomData, + } + } +} diff --git a/executor/crates/sdk-rs/src/storage/mod.rs b/executor/crates/sdk-rs/src/storage/mod.rs new file mode 100644 index 00000000..33ebfaba --- /dev/null +++ b/executor/crates/sdk-rs/src/storage/mod.rs @@ -0,0 +1,30 @@ +//! Storage types for GenLayer smart contracts. + +pub mod array; +pub mod core; +pub mod record; +pub mod tree_map; + +pub use self::array::*; +pub use self::core::*; +pub use self::tree_map::*; + +use crate::calldata::Address; + +crate::record!(Root[T] { + contract_instance: Indirection, + code: Indirection>, + locked_slots: Indirection>, + upgraders: Indirection>, + major: u8, +}); + +impl Root { + pub const SLOT: Slot = Slot([0u8; 32]); +} + +impl Root { + pub fn get() -> Self { + ::handle_at(Self::SLOT, 0) + } +} diff --git a/executor/crates/sdk-rs/src/storage/record.rs b/executor/crates/sdk-rs/src/storage/record.rs new file mode 100644 index 00000000..0e5567b7 --- /dev/null +++ b/executor/crates/sdk-rs/src/storage/record.rs @@ -0,0 +1,69 @@ +//! The `record!` macro for defining storage record types. + +/// Define a storage record type. +/// +/// # Examples +/// +/// Without generics: +/// ```ignore +/// record!(Root { +/// code: Indirection>, +/// major: u8, +/// }); +/// ``` +/// +/// With generics: +/// ```ignore +/// record!(Pair[T, V] { +/// first: T, +/// second: V, +/// }); +/// ``` +#[macro_export] +macro_rules! record { + // Name with generics: record!(Foo[T, V] { ... }) + ($name:ident [$($gen:ident),* $(,)?] { $($field:ident : $ty:ty),* $(,)? }) => { + $crate::record!(@impl [$($gen),*]($name) { $($field: $ty,)* }); + }; + + // Name without generics: record!(Foo { ... }) + ($name:ident { $($field:ident : $ty:ty),* $(,)? }) => { + $crate::record!(@impl []($name) { $($field: $ty,)* }); + }; + + (@impl [$($gen:ident),*]($name:ident) { $($field:ident : $ty:ty,)* }) => { + pub struct $name<$($gen,)*> { + pub slot: $crate::storage::Slot, + pub offset: u32, + _marker: ::core::marker::PhantomData<($($gen,)*)>, + } + + impl<$($gen,)*> ::core::clone::Clone for $name<$($gen,)*> { + fn clone(&self) -> Self { *self } + } + impl<$($gen,)*> ::core::marker::Copy for $name<$($gen,)*> {} + + impl<$($gen: $crate::storage::StorageType,)*> $name<$($gen,)*> { + $crate::record!(@methods []; $($field: $ty,)*); + } + + impl<$($gen: $crate::storage::StorageType,)*> $crate::storage::StorageType for $name<$($gen,)*> { + const SIZE: u32 = 0 $(+ <$ty as $crate::storage::StorageType>::SIZE)*; + type Handle = Self; + fn handle_at(slot: $crate::storage::Slot, offset: u32) -> Self::Handle { + Self { slot, offset, _marker: ::core::marker::PhantomData } + } + } + }; + + (@methods [$($prev_ty:ty),*]; ) => {}; + (@methods [$($prev_ty:ty),*]; $field:ident: $ty:ty, $($rest:tt)*) => { + pub fn $field(&self) -> <$ty as $crate::storage::StorageType>::Handle { + <$ty as $crate::storage::StorageType>::handle_at( + self.slot, + self.offset $(+ <$prev_ty as $crate::storage::StorageType>::SIZE)* + ) + } + $crate::record!(@methods [$($prev_ty,)* $ty]; $($rest)*); + }; +} diff --git a/executor/crates/sdk-rs/src/storage/tree_map.rs b/executor/crates/sdk-rs/src/storage/tree_map.rs new file mode 100644 index 00000000..6fb64e38 --- /dev/null +++ b/executor/crates/sdk-rs/src/storage/tree_map.rs @@ -0,0 +1,500 @@ +//! AVL tree map backed by contract storage. +//! +//! Port of `genlayer.storage.tree_map.TreeMap`. + +use super::core::{StorageType, StorageValue}; + +// Node: key, value, left child index (1-based), right child index (1-based), AVL balance factor +crate::record!(TreeMapNode[K, V] { + key: K, + value: V, + left: u32, + right: u32, + balance: i8, +}); + +// TreeMap layout: root index (1-based, 0=empty), node pool, free-slot stack +crate::record!(TreeMap[K, V] { + root_idx: u32, + slots: super::array::DynArray>, + free_slots: super::array::DynArray, +}); + +impl TreeMap +where + K: StorageValue, + K::Value: Ord, + V: StorageType, +{ + // ---- helpers ---- + + /// Get node handle by 1-based index. + fn node(&self, idx: u32) -> TreeMapNode { + self.slots().index(idx - 1) + } + + /// Walk from root to `k`, returning the path (1-based indices, last is 0 if + /// not found) and whether the last comparison was "go left". + fn find_path(&self, k: &K::Value) -> (Vec, bool) { + let mut path = Vec::new(); + let mut cur = self.root_idx().get(); + let mut is_left = true; + loop { + path.push(cur); + if cur == 0 { + break; + } + let node = self.node(cur); + let node_key = K::storage_get(&node.key()); + if *k < node_key { + cur = node.left().get(); + is_left = true; + } else if node_key < *k { + cur = node.right().get(); + is_left = false; + } else { + break; // found + } + } + (path, is_left) + } + + /// Allocate a slot (recycle or append). Returns (0-based index, node handle). + fn alloc_slot(&self) -> (u32, TreeMapNode) { + let free = self.free_slots(); + if free.len() > 0 { + let idx = free.index(free.len() - 1).get(); + free.pop(); + (idx, self.slots().index(idx)) + } else { + let idx = self.slots().len(); + let node = self.slots().append_slot(); + (idx, node) + } + } + + /// Return a slot to the free list (or shrink pool). + fn free_slot(&self, slot_idx: u32) { + if slot_idx + 1 == self.slots().len() { + self.slots().pop(); + } else { + self.free_slots().append_slot().set(slot_idx); + } + } + + fn init_node(&self, node: TreeMapNode, k: &K::Value) + where + K: StorageValue, + { + K::storage_set(&node.key(), k); + node.left().set(0); + node.right().set(0); + node.balance().set(0); + } + + // ---- rotations ---- + + fn rot_left(&self, par: u32, cur: u32) { + let par_n = self.node(par); + let cur_n = self.node(cur); + let cur_l = cur_n.left().get(); + cur_n.left().set(par); + par_n.right().set(cur_l); + if cur_n.balance().get() == 0 { + par_n.balance().set(1); + cur_n.balance().set(-1); + } else { + par_n.balance().set(0); + cur_n.balance().set(0); + } + } + + fn rot_right(&self, par: u32, cur: u32) { + let par_n = self.node(par); + let cur_n = self.node(cur); + let cur_r = cur_n.right().get(); + cur_n.right().set(par); + par_n.left().set(cur_r); + if cur_n.balance().get() == 0 { + par_n.balance().set(-1); + cur_n.balance().set(1); + } else { + par_n.balance().set(0); + cur_n.balance().set(0); + } + } + + fn rot_right_left(&self, gpar: u32, par: u32, cur: u32) { + let gpar_n = self.node(gpar); + let par_n = self.node(par); + let cur_n = self.node(cur); + let cur_l = cur_n.left().get(); + let cur_r = cur_n.right().get(); + + gpar_n.right().set(cur_l); + par_n.left().set(cur_r); + cur_n.left().set(gpar); + cur_n.right().set(par); + + let bal = cur_n.balance().get(); + if bal == 0 { + par_n.balance().set(0); + gpar_n.balance().set(0); + } else if bal > 0 { + gpar_n.balance().set(-1); + par_n.balance().set(0); + } else { + gpar_n.balance().set(0); + par_n.balance().set(1); + } + cur_n.balance().set(0); + } + + fn rot_left_right(&self, gpar: u32, par: u32, cur: u32) { + let gpar_n = self.node(gpar); + let par_n = self.node(par); + let cur_n = self.node(cur); + let cur_l = cur_n.left().get(); + let cur_r = cur_n.right().get(); + + gpar_n.left().set(cur_r); + par_n.right().set(cur_l); + cur_n.left().set(par); + cur_n.right().set(gpar); + + let bal = cur_n.balance().get(); + if bal == 0 { + par_n.balance().set(0); + gpar_n.balance().set(0); + } else if bal > 0 { + par_n.balance().set(-1); + gpar_n.balance().set(0); + } else { + par_n.balance().set(0); + gpar_n.balance().set(1); + } + cur_n.balance().set(0); + } + + /// Update parent link or root after a rotation replaces `old_child` with `new_child`. + fn replace_child(&self, seq: &[u32], depth: usize, old_child: u32, new_child: u32) { + if depth == 0 { + self.root_idx().set(new_child); + } else { + let gp = self.node(seq[depth - 1]); + if gp.left().get() == old_child { + gp.left().set(new_child); + } else { + gp.right().set(new_child); + } + } + } + + // ---- public API ---- + + pub fn len(&self) -> u32 { + self.slots().len() - self.free_slots().len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn clear(&self) { + self.root_idx().set(0); + self.slots().clear(); + self.free_slots().clear(); + } + + pub fn contains_key(&self, k: &K::Value) -> bool { + let (path, _) = self.find_path(k); + *path.last().unwrap() != 0 + } + + /// Look up a key and return a handle to its value, or `None`. + pub fn get(&self, k: &K::Value) -> Option<::Handle> { + let (path, _) = self.find_path(k); + let last = *path.last().unwrap(); + if last == 0 { + None + } else { + Some(self.node(last).value()) + } + } + + /// Insert or update. Returns a handle to the value slot. + pub fn insert(&self, k: &K::Value, v: &::Value) -> ::Handle + where + V: StorageValue, + { + let (mut seq, is_left) = self.find_path(k); + + // key exists → update value + if *seq.last().unwrap() != 0 { + let node = self.node(*seq.last().unwrap()); + V::storage_set(&node.value(), v); + return node.value(); + } + + // empty tree + if seq.len() == 1 { + let (idx, node) = self.alloc_slot(); + self.root_idx().set(idx + 1); + self.init_node(node, k); + V::storage_set(&node.value(), v); + return node.value(); + } + + // allocate new node and link to parent + let (new_idx, new_node) = self.alloc_slot(); + let parent = self.node(seq[seq.len() - 2]); + if is_left { + parent.left().set(new_idx + 1); + } else { + parent.right().set(new_idx + 1); + } + let last = seq.len() - 1; + seq[last] = new_idx + 1; + self.init_node(new_node, k); + V::storage_set(&new_node.value(), v); + let ret = new_node.value(); + + // rebalance after insert + while seq.len() >= 2 { + let cur = *seq.last().unwrap(); + let par = seq[seq.len() - 2]; + let par_n = self.node(par); + let is_left = cur == par_n.left().get(); + let delta: i8 = if is_left { -1 } else { 1 }; + let new_b = par_n.balance().get() + delta; + + if new_b == -2 { + let gp_depth = seq.len() - 2; + let cur_n = self.node(cur); + if cur_n.balance().get() > 0 { + let right_child = cur_n.right().get(); + self.rot_left_right(par, cur, right_child); + seq.pop(); // cur + seq.pop(); // par + seq.push(right_child); + } else { + self.rot_right(par, cur); + let slen = seq.len(); + seq.remove(slen - 2); // par + } + self.replace_child(&seq, gp_depth, par, *seq.last().unwrap()); + break; + } else if new_b == 2 { + let gp_depth = seq.len() - 2; + let cur_n = self.node(cur); + if cur_n.balance().get() < 0 { + let left_child = cur_n.left().get(); + self.rot_right_left(par, cur, left_child); + seq.pop(); // cur + seq.pop(); // par + seq.push(left_child); + } else { + self.rot_left(par, cur); + let slen = seq.len(); + seq.remove(slen - 2); // par + } + self.replace_child(&seq, gp_depth, par, *seq.last().unwrap()); + break; + } else { + par_n.balance().set(new_b); + if new_b == 0 { + break; + } + seq.pop(); + } + } + if self.root_idx().get() != seq[0] { + self.root_idx().set(seq[0]); + } + ret + } + + /// Remove the entry with the given key. Returns `true` if it was present. + pub fn remove(&self, k: &K::Value) -> bool { + let (mut seq, is_left) = self.find_path(k); + + // not found + if *seq.last().unwrap() == 0 { + return false; + } + + let del_idx = *seq.last().unwrap(); + let del_node = self.node(del_idx); + let del_left = del_node.left().get(); + let del_right = del_node.right().get(); + let del_balance = del_node.balance().get(); + self.free_slot(del_idx - 1); + + let mut special_null = false; + let seq_move_to = seq.len() - 1; + + if del_left == 0 || del_right == 0 { + // <= 1 child + seq[seq_move_to] = if del_left == 0 { del_right } else { del_left }; + special_null = true; + } else { + // two children: find in-order successor (go right, then left*) + seq.push(del_right); + loop { + let cur_n = self.node(*seq.last().unwrap()); + let lft = cur_n.left().get(); + if lft != 0 { + seq.push(lft); + } else { + break; + } + } + let replacement = *seq.last().unwrap(); + seq[seq_move_to] = replacement; + let rep_n = self.node(replacement); + rep_n.left().set(del_left); + if seq_move_to + 2 != seq.len() { + // moved left: detach replacement from its parent + let parent_of_rep = self.node(seq[seq.len() - 2]); + parent_of_rep.left().set(rep_n.right().get()); + rep_n.right().set(del_right); + let slen = seq.len(); + seq[slen - 1] = self.node(seq[seq.len() - 2]).left().get(); + } else { + // moved right once + let slen = seq.len(); + seq[slen - 1] = rep_n.right().get(); + } + } + + // update parent link + if seq_move_to > 0 { + let par_n = self.node(seq[seq_move_to - 1]); + if is_left { + par_n.left().set(seq[seq_move_to]); + } else { + par_n.right().set(seq[seq_move_to]); + } + } else { + self.root_idx().set(seq[seq_move_to]); + } + + // patch balance of replacement + if seq[seq_move_to] != 0 { + let rep_n = self.node(seq[seq_move_to]); + if special_null { + rep_n.balance().set(0); + } else { + rep_n.balance().set(del_balance); + } + } + + // rebalance after delete + while seq.len() >= 2 { + let cur = *seq.last().unwrap(); + let par = seq[seq.len() - 2]; + let par_n = self.node(par); + let is_left_child = if special_null { + is_left + } else { + cur == par_n.left().get() + }; + special_null = false; + // deletion decreased depth on the is_left_child side + let delta: i8 = if is_left_child { 1 } else { -1 }; + let new_b = par_n.balance().get() + delta; + + if new_b == -2 { + let gp_depth = seq.len() - 2; + let sib = par_n.left().get(); + let sib_n = self.node(sib); + let sib_bal = sib_n.balance().get(); + if sib_bal > 0 { + let right_child = sib_n.right().get(); + self.rot_left_right(par, sib, right_child); + seq.pop(); + seq.pop(); + seq.push(right_child); + } else { + self.rot_right(par, sib); + let slen = seq.len(); + seq.remove(slen - 2); + let slen = seq.len(); + seq[slen - 1] = sib; + } + self.replace_child(&seq, gp_depth, par, *seq.last().unwrap()); + if sib_bal == 0 { + break; + } + } else if new_b == 2 { + let gp_depth = seq.len() - 2; + let sib = par_n.right().get(); + let sib_n = self.node(sib); + let sib_bal = sib_n.balance().get(); + if sib_bal < 0 { + let left_child = sib_n.left().get(); + self.rot_right_left(par, sib, left_child); + seq.pop(); + seq.pop(); + seq.push(left_child); + } else { + self.rot_left(par, sib); + let slen = seq.len(); + seq.remove(slen - 2); + let slen = seq.len(); + seq[slen - 1] = sib; + } + self.replace_child(&seq, gp_depth, par, *seq.last().unwrap()); + if sib_bal == 0 { + break; + } + } else { + par_n.balance().set(new_b); + if new_b != 0 { + break; + } + seq.pop(); + } + } + if self.root_idx().get() != seq[0] { + self.root_idx().set(seq[0]); + } + true + } + + // ---- iteration ---- + + fn visit_impl(&self, idx: u32, f: &mut F) + where + F: FnMut(TreeMapNode), + { + if idx == 0 { + return; + } + let node = self.node(idx); + self.visit_impl(node.left().get(), f); + f(node); + self.visit_impl(node.right().get(), f); + } + + /// Iterate entries in key order, calling `f` with each node handle. + pub fn for_each_node(&self, mut f: F) + where + F: FnMut(TreeMapNode), + { + self.visit_impl(self.root_idx().get(), &mut f); + } + + /// Iterate entries in key order, calling `f(key, value)`. + pub fn for_each(&self, mut f: F) + where + V: StorageValue, + F: FnMut(K::Value, ::Value), + { + self.for_each_node(|node| { + let k = K::storage_get(&node.key()); + let v = V::storage_get(&node.value()); + f(k, v); + }); + } +} diff --git a/modules/implementation/Cargo.lock b/modules/implementation/Cargo.lock index 0d1e037f..a00a783d 100644 --- a/modules/implementation/Cargo.lock +++ b/modules/implementation/Cargo.lock @@ -126,6 +126,9 @@ name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "arg_enum_proc_macro" @@ -633,6 +636,17 @@ dependencies = [ "serde", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -895,7 +909,7 @@ dependencies = [ [[package]] name = "genlayer_calldata" -version = "0.0.1" +version = "0.0.2" dependencies = [ "arbitrary", "bytes", @@ -908,7 +922,7 @@ dependencies = [ [[package]] name = "genlayer_sdk" -version = "0.0.1" +version = "0.0.2" dependencies = [ "arbitrary", "bytes", diff --git a/modules/implementation/fuzz/features.txt b/modules/implementation/fuzz/features.txt deleted file mode 100644 index c11a931b..00000000 --- a/modules/implementation/fuzz/features.txt +++ /dev/null @@ -1 +0,0 @@ -vendored-lua diff --git a/modules/implementation/src/llm/ctx.rs b/modules/implementation/src/llm/ctx.rs index b0fcc7a7..ccb5430b 100644 --- a/modules/implementation/src/llm/ctx.rs +++ b/modules/implementation/src/llm/ctx.rs @@ -50,9 +50,9 @@ impl CtxPart { .await .map(|resp| resp.map(llm_iface::PromptAnswerData::Text)), prompt::ExtendedOutputFormat::JSON => provider - .exec_prompt_json(dflt, prompt, model) + .exec_prompt_json_as_text(dflt, prompt, model) .await - .map(|resp| resp.map(llm_iface::PromptAnswerData::Object)), + .map(|resp| resp.map(llm_iface::PromptAnswerData::Text)), prompt::ExtendedOutputFormat::Bool => provider .exec_prompt_bool_reason(dflt, prompt, model) .await diff --git a/modules/implementation/src/llm/providers.rs b/modules/implementation/src/llm/providers.rs index e5e72522..5af22dd0 100644 --- a/modules/implementation/src/llm/providers.rs +++ b/modules/implementation/src/llm/providers.rs @@ -116,7 +116,9 @@ pub trait Provider { prompt: &prompt::Internal, model: &str, ) -> ModuleResult> { - self.exec_prompt_text(ctx, prompt, model).await + let res = self.exec_prompt_json(ctx, prompt, model).await?; + let serialized = serde_json::to_string(&res.result)?; + Ok(ProviderResponse::new(serialized, res.tokens)) } async fn exec_prompt_json( @@ -328,12 +330,12 @@ impl Provider for OpenAICompatible { Ok(ProviderResponse::new(response.to_owned(), tokens)) } - async fn exec_prompt_json( + async fn exec_prompt_json_as_text( &self, ctx: &scripting::CtxPart, prompt: &prompt::Internal, model: &str, - ) -> ModuleResult>> { + ) -> ModuleResult> { let mut request = serde_json::json!({ "model": model, "messages": prompt.to_openai_messages()?, @@ -394,11 +396,7 @@ impl Provider for OpenAICompatible { .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("can't get response field {}", &res.body))?; - let response = sanitize_json_str(response); - let parsed = - serde_json::from_str(&response).with_context(|| format!("parsing {response:?}"))?; - - Ok(ProviderResponse::new(parsed, tokens)) + Ok(ProviderResponse::new(response.to_owned(), tokens)) } } diff --git a/modules/install/data/manifest.yaml b/modules/install/data/manifest.yaml index 4c6d9b88..a84fd274 100644 --- a/modules/install/data/manifest.yaml +++ b/modules/install/data/manifest.yaml @@ -2,7 +2,7 @@ executor_versions: # new major.minor.* versions will override older ones v0.3.0: available_after: "2026-04-21T00:00:00Z" - v0.2.0: + v0.2.16: available_after: "2024-09-01T00:00:00Z" runners_download_urls: diff --git a/modules/interfaces/src/lib.rs b/modules/interfaces/src/lib.rs index a6643729..aefd278c 100644 --- a/modules/interfaces/src/lib.rs +++ b/modules/interfaces/src/lib.rs @@ -133,7 +133,6 @@ pub mod llm { pub enum PromptAnswerData { Text(String), Bool(bool), - Object(serde_json::Map), } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] diff --git a/runners/genlayer-py-std/fuzz/resave.py b/runners/genlayer-py-std/fuzz/resave.py index 0c5cb605..2fd31c8b 100755 --- a/runners/genlayer-py-std/fuzz/resave.py +++ b/runners/genlayer-py-std/fuzz/resave.py @@ -10,6 +10,8 @@ in_dir = Path(inp) for path in in_dir.iterdir(): + if path.is_dir(): + continue data = path.read_bytes() name = hashlib.sha3_256(data).digest().hex() Path(out).joinpath(name).write_bytes(data) diff --git a/runners/genlayer-py-std/src/genlayer/nondet/__init__.py b/runners/genlayer-py-std/src/genlayer/nondet/__init__.py index d3773078..873de4d2 100644 --- a/runners/genlayer-py-std/src/genlayer/nondet/__init__.py +++ b/runners/genlayer-py-std/src/genlayer/nondet/__init__.py @@ -20,7 +20,7 @@ from genlayer._internal import _lazy_api from genlayer.types import * import _genlayer_wasi as wasi -import io +import json import dataclasses import genlayer._internal.on_chain.gl_call as gl_call @@ -40,6 +40,10 @@ def _decode_nondet(buf): return ret['ok'] +def _decode_nondet_json(buf): + return json.loads(_decode_nondet(buf)) + + if typing.TYPE_CHECKING: import PIL.Image @@ -105,15 +109,17 @@ def exec_prompt( else: images.append(im) + format = config.get('response_format', 'text') + return gl_call.gl_call_generic( { 'ExecPrompt': { 'prompt': prompt, - 'response_format': config.get('response_format', 'text'), + 'response_format': format, 'images': images, } }, - _decode_nondet, + _decode_nondet_json if format == 'json' else _decode_nondet, ) diff --git a/runners/support/current/hashes.nix b/runners/support/current/hashes.nix index 6ebc06bf..87955ffb 100644 --- a/runners/support/current/hashes.nix +++ b/runners/support/current/hashes.nix @@ -27,7 +27,7 @@ let }; genlayer-std = { - hash = "sha256-9V4wfSdihldRIRVvZ4rYKwoR53DZbr6w8VO2CFiYJLk="; + hash = "sha256-CY4VlwG2fWB32+ueqQ/8vxLhFza2R1Vq/Tc6JYcWr4A="; }; genlayer-embeddings = { @@ -55,7 +55,7 @@ let wrappers = { __prefix = ""; py-genlayer = { - hash = "sha256-/6xtA3mlPQNhvJzqzoX9dm+FwN8bwAM6gmX1Tapr/Iw="; + hash = "sha256-mr5Jhww3vcEmJLd7EfSEJ9aIjTY+Ozr4l2x+5ShwK68="; depends = [ cpython pyLibs.cloudpickle @@ -63,7 +63,7 @@ let ]; }; py-genlayer-multi = { - hash = "sha256-z9kgsnJnaATfLhUJi8UtTxyhnkAZ6I1Wo2mkAkGcow8="; + hash = "sha256-UM5G4c0SR3Afw9FMKR7gmivEutsaRsQUlRA2r7ATr0s="; depends = [ cpython pyLibs.cloudpickle diff --git a/support/ci/pipelines/docs.sh b/support/ci/pipelines/docs.sh index f0bbcb2e..ad0ef5df 100755 --- a/support/ci/pipelines/docs.sh +++ b/support/ci/pipelines/docs.sh @@ -3,6 +3,9 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) source "$SCRIPT_DIR/_common.sh" +COPYRIGHT_YEAR="$(git log -1 --date=format:%Y --format=%ad)" +export COPYRIGHT_YEAR + python3 ./doc/website/generate.py doc/website/src/impl-spec/appendix/runners-versions.json nix develop -i .#gen-docs --command bash ./support/ci/pipelines/src/docs.sh diff --git a/tests/cases/.gitignore b/tests/cases/.gitignore index 365bc3e1..19b8300e 100644 --- a/tests/cases/.gitignore +++ b/tests/cases/.gitignore @@ -1,3 +1,4 @@ /temp /stable/claude/temp /stable/claude/agent +*.wasm diff --git a/tests/cases/semi-stable/llm/issue_288.0.stdout b/tests/cases/semi-stable/llm/issue_288.0.stdout new file mode 100644 index 00000000..771a57d9 --- /dev/null +++ b/tests/cases/semi-stable/llm/issue_288.0.stdout @@ -0,0 +1,4 @@ +dict +float +True +executed with `Return(null)` diff --git a/tests/cases/semi-stable/llm/issue_288.jsonnet b/tests/cases/semi-stable/llm/issue_288.jsonnet new file mode 100644 index 00000000..41bee2ae --- /dev/null +++ b/tests/cases/semi-stable/llm/issue_288.jsonnet @@ -0,0 +1,6 @@ +local simple = import 'templates/simple_deploy.jsonnet'; +local util = import 'templates/util.jsonnet'; +{entry: util.addPaths([simple.run('${jsonnetDir}/${fileBaseName}.py') { + stable_hash: false, + modes: 'l', +}])} diff --git a/tests/cases/semi-stable/llm/issue_288.py b/tests/cases/semi-stable/llm/issue_288.py new file mode 100644 index 00000000..08638e5b --- /dev/null +++ b/tests/cases/semi-stable/llm/issue_288.py @@ -0,0 +1,21 @@ +# { "Depends": "py-genlayer:test" } +import sys + +import genlayer as gl +from genlayer.types import * + + +class Contract(gl.contract.Contract): + def __init__(self): + def run(): + v = gl.nondet.exec_prompt( + 'Respond with a json object with a single key "random" and value between 0 and 1, like 0.5', + response_format='json', + ) + print(v, file=sys.stderr) + print(type(v).__name__) + r = v['random'] + print(type(r).__name__) + print(r >= 0 and r <= 1) + + gl.eq_principle.strict_eq(run) diff --git a/tests/cases/stable/nondet/metod_det_get_webpage.0_0.hash b/tests/cases/stable/nondet/metod_det_get_webpage.0_0.hash index 0b92afb3..b33e2bd7 100644 --- a/tests/cases/stable/nondet/metod_det_get_webpage.0_0.hash +++ b/tests/cases/stable/nondet/metod_det_get_webpage.0_0.hash @@ -1 +1 @@ -LRFcZXhpdF9jb2RlIDEWBmZyYW1lcxUWBGZ1bmPJuwQLbW9kdWxlX25hbWU8Y3B5dGhvbhYEZnVuY6EKC21vZHVsZV9uYW1lPGNweXRob24QbW9kdWxlX2luc3RhbmNlcxYHY3B5dGhvbg4IbWVtb3JpZXMNgwJH9q4vF30wFiubouizuQqXQBwLWvVDu8rmgH7ZDkWf+Qlzb2Z0ZmxvYXQOCG1lbW9yaWVzDYMC7QkgrBZoP7TlRNWs5afHGk9HJDE4z47T7v82kFb6vS4FBQ== +LRFcZXhpdF9jb2RlIDEWBmZyYW1lcxUWBGZ1bmPJuwQLbW9kdWxlX25hbWU8Y3B5dGhvbhYEZnVuY6EKC21vZHVsZV9uYW1lPGNweXRob24QbW9kdWxlX2luc3RhbmNlcxYHY3B5dGhvbg4IbWVtb3JpZXMNgwL3uL5P9J1zjsgn/32zG2Buo1Ab0rUUbxe41eulsYAX6wlzb2Z0ZmxvYXQOCG1lbW9yaWVzDYMC7QkgrBZoP7TlRNWs5afHGk9HJDE4z47T7v82kFb6vS4FBQ== diff --git a/tests/cases/stable/storage/storage_dyn_array_rust/prepare.py b/tests/cases/stable/storage/storage_dyn_array_rust/prepare.py new file mode 100644 index 00000000..cc74fc52 --- /dev/null +++ b/tests/cases/stable/storage/storage_dyn_array_rust/prepare.py @@ -0,0 +1,34 @@ +import subprocess +import json +from pathlib import Path + +root = Path(__file__).parent +repo_root = root.parents[4] + +build_info = json.loads(repo_root.joinpath('build', 'info.json').read_text()) +target_dir = Path(build_info['rust_target_dir']) + +subprocess.run( + [ + 'cargo', + 'build', + '--example', + 'storage_dyn_array', + '--target', + 'wasm32-wasip1', + '--release', + '--target-dir', + str(target_dir), + ], + cwd=repo_root / 'executor' / 'crates' / 'sdk-rs', + check=True, +) + +src = target_dir / 'wasm32-wasip1' / 'release' / 'examples' / 'storage_dyn_array.wasm' +dst = root / 'storage_dyn_array.wasm' +wat = root / 'storage_dyn_array.wat' + +# Round-trip through wabt to normalize to MVP format +subprocess.run(['wasm2wat', str(src), '-o', str(wat)], check=True) +subprocess.run(['wat2wasm', str(wat), '-o', str(dst)], check=True) +wat.unlink() diff --git a/tests/cases/stable/storage/storage_dyn_array_rust/storage_dyn_array_rust.0.stdout b/tests/cases/stable/storage/storage_dyn_array_rust/storage_dyn_array_rust.0.stdout new file mode 100644 index 00000000..23c42306 --- /dev/null +++ b/tests/cases/stable/storage/storage_dyn_array_rust/storage_dyn_array_rust.0.stdout @@ -0,0 +1,8 @@ +len=3 +[0]=10 +[1]=20 +[2]=30 +after pop len=2 +[0]=10 +[1]=20 +executed with `Return(null)` diff --git a/tests/cases/stable/storage/storage_dyn_array_rust/storage_dyn_array_rust.jsonnet b/tests/cases/stable/storage/storage_dyn_array_rust/storage_dyn_array_rust.jsonnet new file mode 100644 index 00000000..b2255241 --- /dev/null +++ b/tests/cases/stable/storage/storage_dyn_array_rust/storage_dyn_array_rust.jsonnet @@ -0,0 +1,12 @@ +local simple = import 'templates/simple_deploy.jsonnet'; +local util = import 'templates/util.jsonnet'; +{ + prepare: '${jsonnetDir}/prepare.py', + entry: util.addPaths([ + simple.run('${jsonnetDir}/storage_dyn_array.wasm') { + "calldata": ||| + {} + |||, + stable_hash: false, + }]) +} diff --git a/tests/cases/stable/storage/storage_root_rust/prepare.py b/tests/cases/stable/storage/storage_root_rust/prepare.py new file mode 100644 index 00000000..e67b99d1 --- /dev/null +++ b/tests/cases/stable/storage/storage_root_rust/prepare.py @@ -0,0 +1,34 @@ +import subprocess +import json +from pathlib import Path + +root = Path(__file__).parent +repo_root = root.parents[4] + +build_info = json.loads(repo_root.joinpath('build', 'info.json').read_text()) +target_dir = Path(build_info['rust_target_dir']) + +subprocess.run( + [ + 'cargo', + 'build', + '--example', + 'storage_root', + '--target', + 'wasm32-wasip1', + '--release', + '--target-dir', + str(target_dir), + ], + cwd=repo_root / 'executor' / 'crates' / 'sdk-rs', + check=True, +) + +src = target_dir / 'wasm32-wasip1' / 'release' / 'examples' / 'storage_root.wasm' +dst = root / 'storage_root.wasm' +wat = root / 'storage_root.wat' + +# Round-trip through wabt to normalize to MVP format +subprocess.run(['wasm2wat', str(src), '-o', str(wat)], check=True) +subprocess.run(['wat2wasm', str(wat), '-o', str(dst)], check=True) +wat.unlink() diff --git a/tests/cases/stable/storage/storage_root_rust/storage_root_rust.0.stdout b/tests/cases/stable/storage/storage_root_rust/storage_root_rust.0.stdout new file mode 100644 index 00000000..8b069b26 --- /dev/null +++ b/tests/cases/stable/storage/storage_root_rust/storage_root_rust.0.stdout @@ -0,0 +1,3 @@ +contains_magic=true +counter=42 +executed with `Return(null)` diff --git a/tests/cases/stable/storage/storage_root_rust/storage_root_rust.jsonnet b/tests/cases/stable/storage/storage_root_rust/storage_root_rust.jsonnet new file mode 100644 index 00000000..66dee6e2 --- /dev/null +++ b/tests/cases/stable/storage/storage_root_rust/storage_root_rust.jsonnet @@ -0,0 +1,12 @@ +local simple = import 'templates/simple_deploy.jsonnet'; +local util = import 'templates/util.jsonnet'; +{ + prepare: '${jsonnetDir}/prepare.py', + entry: util.addPaths([ + simple.run('${jsonnetDir}/storage_root.wasm') { + "calldata": ||| + {} + |||, + stable_hash: false, + }]) +} diff --git a/tests/runner/ya_test_runner_plugins/cargo.py b/tests/runner/ya_test_runner_plugins/cargo.py index 335ce265..5944f596 100644 --- a/tests/runner/ya_test_runner_plugins/cargo.py +++ b/tests/runner/ya_test_runner_plugins/cargo.py @@ -254,7 +254,7 @@ def cargo_fuzz( extra_config = rust_root_dir.joinpath('.ya-test-config.json') if extra_config.exists(): extra_conf = json.loads(extra_config.read_text()) - extra_flags.extend(extra_conf.get('cargo_test_flags', [])) + extra_flags.extend(extra_conf.get('cargo_afl_build_flags', [])) # Track fuzz binary for coverage if _is_coverage_enabled(): @@ -268,10 +268,11 @@ def cargo_fuzz( ya_test_runner.exec.step.SetCwd(path=rust_root_dir), ] + [ya_test_runner.exec.step.SetEnv(key=k, value=v) for k, v in test_env.items()] + + [ya_test_runner.exec.step.SetEnv(key='CARGO', value='cargo')] + [ ya_test_runner.exec.step.Run( args=[ - 'cargo', + 'cargo-afl', 'afl', 'build', '--target-dir', @@ -290,7 +291,7 @@ def cargo_fuzz( ), ya_test_runner.exec.step.Run( args=[ - 'cargo', + 'cargo-afl', 'afl', 'fuzz', '-c', @@ -333,7 +334,7 @@ async def remove_inputs_dir(_): steps.append( ya_test_runner.exec.step.Run( args=[ - 'cargo', + 'cargo-afl', 'afl', 'cmin', '-T',