From 407250c42e7879bb86ed9a0965e81211cb2cf9eb Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com> Date: Mon, 9 Jun 2025 11:58:57 -0300 Subject: [PATCH 1/3] add toy_vm with sort lookup argument example --- Cargo.lock | 113 +++++-- Cargo.toml | 2 + Makefile | 1 + example_toy_vm/example/Cargo.toml | 29 ++ example_toy_vm/example/src/lib.rs | 24 ++ example_toy_vm/example/src/main.rs | 109 +++++++ example_toy_vm/toy_vm/Cargo.toml | 17 + example_toy_vm/toy_vm/src/lib.rs | 436 ++++++++++++++++++++++++++ example_toy_vm/toy_vm/src/value.rs | 46 +++ starstream_sys/src/lib.rs | 3 + starstream_vm/src/lib.rs | 29 ++ starstream_vm/src/nebula.rs | 9 + starstream_vm/tests/toy_vm_example.rs | 18 ++ 13 files changed, 814 insertions(+), 22 deletions(-) create mode 100644 example_toy_vm/example/Cargo.toml create mode 100644 example_toy_vm/example/src/lib.rs create mode 100644 example_toy_vm/example/src/main.rs create mode 100644 example_toy_vm/toy_vm/Cargo.toml create mode 100644 example_toy_vm/toy_vm/src/lib.rs create mode 100644 example_toy_vm/toy_vm/src/value.rs create mode 100644 starstream_vm/tests/toy_vm_example.rs diff --git a/Cargo.lock b/Cargo.lock index 43e94aa4..84ce8f5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,9 +113,9 @@ dependencies = [ "pairing", "pasta_curves", "proptest", - "rand", - "rand_chacha", - "rand_core", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", "rayon", "rayon-scan", "ref-cast", @@ -306,7 +306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d14b9606aa9550d34651bc481443203bc014237bdb992d201d2afa62d2ec6dea" dependencies = [ "ambient-authority", - "rand", + "rand 0.8.5", ] [[package]] @@ -602,6 +602,21 @@ dependencies = [ "starstream_sys", ] +[[package]] +name = "example_toy_vm" +version = "0.0.0" +dependencies = [ + "rand 0.9.1", + "rand_chacha 0.9.0", + "spin 0.10.0", + "stack_dst", + "starstream_sys", + "starstream_vm", + "talc", + "toy_vm", + "wasmi", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -617,7 +632,7 @@ dependencies = [ "bitvec", "byteorder", "ff_derive", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -722,7 +737,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -736,8 +751,8 @@ dependencies = [ "getrandom 0.2.16", "halo2curves", "pasta_curves", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "rayon", "semolina", "sppark", @@ -760,8 +775,8 @@ dependencies = [ "pairing", "pasta_curves", "paste", - "rand", - "rand_core", + "rand 0.8.5", + "rand_core 0.6.4", "rayon", "serde", "serde_arrays", @@ -969,7 +984,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin", + "spin 0.9.8", ] [[package]] @@ -1096,7 +1111,7 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", - "rand", + "rand 0.8.5", "serde", ] @@ -1183,7 +1198,7 @@ dependencies = [ "group", "hex", "lazy_static", - "rand", + "rand 0.8.5", "serde", "static_assertions", "subtle", @@ -1245,8 +1260,8 @@ dependencies = [ "bitflags 2.9.0", "lazy_static", "num-traits", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "rand_xorshift", "regex-syntax 0.8.5", "rusty-fork", @@ -1297,8 +1312,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_core 0.9.3", ] [[package]] @@ -1308,7 +1332,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -1320,13 +1354,19 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" + [[package]] name = "rand_xorshift" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1648,6 +1688,15 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "lock_api", +] + [[package]] name = "sppark" version = "0.1.6" @@ -1722,7 +1771,7 @@ version = "0.0.0" dependencies = [ "byteorder", "log", - "rand", + "rand 0.8.5", "sha2", "tiny-keccak", "wasmi", @@ -1785,6 +1834,15 @@ dependencies = [ "winx", ] +[[package]] +name = "talc" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fcad3be1cfe36eb7d716a04791eba36a197da9d9b6ea1e28e64ac569da3701d" +dependencies = [ + "lock_api", +] + [[package]] name = "tap" version = "1.0.1" @@ -1871,6 +1929,17 @@ dependencies = [ "crunchy", ] +[[package]] +name = "toy_vm" +version = "0.0.0" +dependencies = [ + "rand 0.9.1", + "rand_chacha 0.9.0", + "spin 0.10.0", + "starstream_sys", + "talc", +] + [[package]] name = "tracing" version = "0.1.41" @@ -2145,7 +2214,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "spin", + "spin 0.9.8", "wasmi_arena", "wasmi_core", "wasmparser-nostd", @@ -2599,7 +2668,7 @@ dependencies = [ "ff", "getrandom 0.2.16", "itertools 0.12.1", - "rand", + "rand 0.8.5", "serde", "serde_json", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index aa2e0456..e3f75cdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,8 @@ members = [ "example_contract", "example_contract_permissioned", + "example_toy_vm/example", + "example_toy_vm/toy_vm", "starstream_cli", "starstream_compiler", "starstream_sandbox", diff --git a/Makefile b/Makefile index 23f0651e..c608dc66 100644 --- a/Makefile +++ b/Makefile @@ -9,3 +9,4 @@ cargo: all: vsc vsc: @cd starstream_vscode && npm i && npx vsce package + diff --git a/example_toy_vm/example/Cargo.toml b/example_toy_vm/example/Cargo.toml new file mode 100644 index 00000000..8e7fa7f8 --- /dev/null +++ b/example_toy_vm/example/Cargo.toml @@ -0,0 +1,29 @@ +cargo-features = ["per-package-target"] + +[package] +name = "example_toy_vm" +version = "0.0.0" +edition = "2024" +license = "MIT" + +forced-target = "wasm32-unknown-unknown" + +[lib] +test = false + +[[bin]] +name = "example_toy_vm" +test = false + +[dependencies] +stack_dst = { version = "0.8.1", default-features = false, features = ["const_generics"] } +starstream_sys = { path = "../../starstream_sys" } +toy_vm = { path = "../toy_vm", features = ["starstream"] } +talc = "4.4.2" +spin = "0.10.0" +rand = { version = "0.9.1", default-features = false } +rand_chacha = { version = "0.9.0", default-features = false } + +[dev-dependencies] +starstream_vm = { path = "../../starstream_vm" } +wasmi = { git = "https://github.com/ICME-Lab/zkEngine_dev" } diff --git a/example_toy_vm/example/src/lib.rs b/example_toy_vm/example/src/lib.rs new file mode 100644 index 00000000..bc0f6170 --- /dev/null +++ b/example_toy_vm/example/src/lib.rs @@ -0,0 +1,24 @@ +#![no_std] + +use starstream::utxo_import; + +#[link(wasm_import_module = "starstream_utxo:example_toy_vm")] +unsafe extern "C" { + safe fn starstream_new_VmUtxo_new() -> VmUtxo; +} + +utxo_import! { + "starstream_utxo:example_toy_vm"; + VmUtxo; + starstream_status_VmUtxo; + starstream_resume_VmUtxo; + (); +} + +impl VmUtxo { + #[allow(clippy::new_without_default)] + #[inline] + pub fn new() -> Self { + starstream_new_VmUtxo_new() + } +} diff --git a/example_toy_vm/example/src/main.rs b/example_toy_vm/example/src/main.rs new file mode 100644 index 00000000..7eaeff49 --- /dev/null +++ b/example_toy_vm/example/src/main.rs @@ -0,0 +1,109 @@ +#![no_std] +#![no_main] +#![allow(dead_code)] +#![feature(allocator_api)] + +use core::alloc::{GlobalAlloc, Layout}; +use rand::SeedableRng as _; +use starstream::Utxo; +use talc::{ClaimOnOom, Span, Talc, Talck}; +use toy_vm::Op; + +extern crate alloc; + +starstream::panic_handler!(); + +// don't set a global allocator +// this is needed because we are using the alloc crate, but we can use the +// allocator api instead. +struct DummyAllocator; + +unsafe impl GlobalAlloc for DummyAllocator { + unsafe fn alloc(&self, _layout: Layout) -> *mut u8 { + core::ptr::null_mut() + } + + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {} +} + +#[global_allocator] +static DUMMY_ALLOCATOR: DummyAllocator = DummyAllocator; + +// half a page of wasm memory for the vm +const MEMORY_SIZE: usize = 65536 / 2; + +// fixed size arena for allocator, if the program requires more memory than this +// then the program will panic +static mut ARENA: [u8; MEMORY_SIZE] = [0; MEMORY_SIZE]; + +static ALLOCATOR: Talck, ClaimOnOom> = + Talc::new(unsafe { ClaimOnOom::new(Span::from_array(core::ptr::addr_of!(ARENA).cast_mut())) }) + .lock(); + +// Vm utxo +pub struct VmUtxo {} + +impl VmUtxo { + #[allow(clippy::new_ret_no_self)] + pub fn new(sleep: fn(&VmUtxo)) { + let this = VmUtxo {}; + + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(42); + + // we set the stack initially to have a 0, since we need a value first + // to do the addition. + let ops = [Op::Push(toy_vm::Value::from_slice(&[0], &ALLOCATOR))]; + + let stack = toy_vm::Stack::new(&ALLOCATOR); + + let mut stack = toy_vm::run(&ops, &ALLOCATOR, stack, &mut rng).unwrap(); + + // each time the utxo is resumed, it generates 500 random values, adds + // that to the current state, and then sorts that array + // + // this is not supposed to be useful, it's just an example of how the VM + // can be used in a stateful way. + let ops = [Op::Rand { len: 500 }, Op::Add, Op::Sort]; + + loop { + // because we are keeping the stack in memory, we are also keeping + // the state for the next call. + stack = toy_vm::run(&ops, &ALLOCATOR, stack, &mut rng).unwrap(); + + sleep(&this); + } + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn starstream_new_VmUtxo_new() { + VmUtxo::new(starstream::sleep::<(), VmUtxo>) +} + +// ---------------------------------------------------------------------------- +// Coordination script + +#[unsafe(no_mangle)] +pub extern "C" fn new_utxo() -> example_toy_vm::VmUtxo { + example_toy_vm::VmUtxo::new() +} + +#[unsafe(no_mangle)] +pub extern "C" fn state_transition(utxo: example_toy_vm::VmUtxo) { + utxo.resume(()); +} + +// #[unsafe(no_mangle)] +// pub extern "C" fn test() { +// let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(42); + +// let ops = [Op::Push(toy_vm::Value::from_slice(&[0], &ALLOCATOR))]; + +// let stack = toy_vm::Stack::new(&ALLOCATOR); + +// let mut stack = toy_vm::run(&ops, &ALLOCATOR, stack, &mut rng).unwrap(); + +// let ops = [Op::Rand { len: 500 }, Op::Add, Op::Sort]; + +// stack = toy_vm::run(&ops, &ALLOCATOR, stack, &mut rng).unwrap(); +// } diff --git a/example_toy_vm/toy_vm/Cargo.toml b/example_toy_vm/toy_vm/Cargo.toml new file mode 100644 index 00000000..ebd32d0d --- /dev/null +++ b/example_toy_vm/toy_vm/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "toy_vm" +version = "0.0.0" +edition = "2024" + +[features] +default = [] +starstream = ["dep:starstream_sys"] + +[dependencies] +starstream_sys = { path = "../../starstream_sys", optional = true } +rand = { version = "0.9.1", default-features = false } + +[dev-dependencies] +talc = "4.4.2" +spin = "0.10.0" +rand_chacha = { version = "0.9.0", default-features = false } diff --git a/example_toy_vm/toy_vm/src/lib.rs b/example_toy_vm/toy_vm/src/lib.rs new file mode 100644 index 00000000..d9d3e179 --- /dev/null +++ b/example_toy_vm/toy_vm/src/lib.rs @@ -0,0 +1,436 @@ +#![no_std] +#![feature(allocator_api)] + +use alloc::{alloc::Allocator, rc::Rc, vec}; +use core::{cmp::Ordering, fmt}; +use rand::RngCore; +pub use value::Value; +use vec::Vec; +pub mod value; + +extern crate alloc; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum TestResult { + L = 0, + E = 1, + G = 2, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Op { + Push(Value), + Pop, + Add, + Sub, + Mul, + Div, + Dup { i: u32 }, + Test, + CondJump { i: u32, on: TestResult }, + Jump { i: u32 }, + Swap { i: u32 }, + Rand { len: u32 }, + Sort, +} + +pub struct Stack { + inner: Vec, A>, +} + +impl core::fmt::Debug for Stack +where + Vec, A>: core::fmt::Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Stack").field("inner", &self.inner).finish() + } +} +impl Eq for Stack {} + +impl PartialEq for Stack { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + +impl Stack { + pub fn new(allocator: A) -> Self { + let stack: Vec, A> = Vec::new_in(allocator); + + Stack { inner: stack } + } + pub fn push(&mut self, value: Value) { + self.inner.push(value); + } + + pub fn pop(&mut self) -> Result, Error> { + self.inner.pop().ok_or(Error::EmptyStackError) + } + + pub fn swap(&mut self, a: usize, b: usize) { + self.inner.swap(a, b); + } + + pub fn len(&self) -> usize { + self.inner.len() + } + + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + pub fn get(&self, pos: usize) -> Result<&Value, Error> { + self.inner.get(pos).ok_or(Error::EmptyStackError) + } +} + +pub enum Error { + EmptyStackError, + TypeError, +} + +// toy vm implementing a basic stack machine +// +// arithmetic operations work on vectors +// +// the allocator is parametric, so the strategy can be controlled by the caller +// NOTE: the allocator must be the same across calls if the resulting stack is re-used. +pub fn run( + ops: &[Op], + allocator: A, + mut stack: Stack, + mut rng: R, +) -> Result, Error> { + let mut pc = 0u32; + + fn combine_arrays( + a: &[u32], + b: &[u32], + f: impl Fn(u32, u32) -> u32, + allocator: A, + ) -> Value { + let result_len = core::cmp::max(a.len(), b.len()); + + let mut result = Vec::new_in(allocator); + + for e in a + .iter() + .cycle() + .zip(b.iter().cycle()) + .take(result_len) + .map(|(a, b)| f(*a, *b)) + { + result.push(e); + } + + Value::Array(Rc::new_in(result, allocator)) + } + + while let Some(op) = ops.get(pc as usize) { + match op { + Op::Push(imm) => { + stack.push(imm.clone()); + } + Op::Pop => { + stack.pop()?; + } + op @ (Op::Add | Op::Sub | Op::Mul | Op::Div) => { + let a = stack.pop()?; + let b = stack.pop()?; + + match (a, b) { + (Value::Array(a), Value::Array(b)) => { + stack.push(combine_arrays( + &a, + &b, + match op { + Op::Add => |a: u32, b: u32| a.wrapping_add(b), + Op::Sub => |a: u32, b: u32| a.wrapping_sub(b), + Op::Mul => |a: u32, b: u32| a.wrapping_mul(b), + Op::Div => |a: u32, b: u32| a.wrapping_div(b), + _ => unreachable!(), + }, + allocator, + )); + } + _ => return Err(Error::TypeError), + } + } + Op::Dup { i } => { + let x = stack.get(stack.len() - 1 - *i as usize)?; + + stack.push(x.clone()); + } + Op::Test => { + let a = stack.pop()?; + let b = stack.pop()?; + + match (a, b) { + (Value::Array(a), Value::Array(b)) => { + let res = match a.cmp(&b) { + Ordering::Less => TestResult::L, + Ordering::Equal => TestResult::E, + Ordering::Greater => TestResult::G, + }; + + stack.push(Value::TestResult(res)); + } + _ => return Err(Error::TypeError), + } + } + Op::CondJump { i, on } => { + let test_result = stack.pop()?; + + let Value::TestResult(test_result) = test_result else { + return Err(Error::TypeError); + }; + + if on == &test_result { + pc = *i; + continue; + } + } + Op::Jump { i } => { + pc = *i; + continue; + } + Op::Swap { i } => { + let len = stack.len(); + + if len < 2 { + return Err(Error::EmptyStackError); + } + + stack.swap(len - 1, len - 1 - *i as usize); + } + Op::Sort => { + let arr = stack.pop()?; + + let Value::Array(mut arr) = arr else { + return Err(Error::TypeError); + }; + + #[cfg(feature = "starstream")] + { + let sorted = starstream_lookup_arguments::array_sort(&arr, allocator); + + let arr_borrow = Rc::make_mut(&mut arr); + + *arr_borrow = sorted; + } + + // outside of starstream we just run sort directly. + // this is needed for the unit tests to work in particular + #[cfg(not(feature = "starstream"))] + { + Rc::make_mut(&mut arr).sort(); + } + + stack.push(Value::Array(arr)); + } + Op::Rand { len } => { + let mut result = Vec::new_in(allocator); + + result.reserve_exact(*len as usize); + + for _ in 0..*len { + result.push(rng.next_u32()) + } + + stack.push(Value::Array(Rc::new_in(result, allocator))); + } + } + + pc += 1; + } + + Ok(stack) +} + +#[cfg(feature = "starstream")] +mod starstream_lookup_arguments { + use alloc::{alloc::Allocator, vec::Vec}; + + #[cfg(feature = "starstream")] + pub(crate) struct SortLookupArguments { + pub(crate) input_ptr: *const u8, + pub(crate) output_ptr: *mut u8, + pub(crate) len: usize, + } + + pub(crate) fn array_sort(arr: &[u32], allocator: A) -> Vec { + // validation part: + // we want to check that: + // 1. The result is sorted. + // 2. The result is made of elements of the original array. + // 3. No repetitions were introduced. + // + // we sort the indices instead of the actual array, since that way it's + // easy to check 2 an 3. + let mut argsort = Vec::new_in(allocator); + argsort.extend(0u32..arr.len() as u32); + + let mut seen_before = Vec::new_in(allocator); + seen_before.extend(core::iter::repeat_n(false, arr.len())); + + let args = SortLookupArguments { + input_ptr: (&arr[0]) as *const u32 as *const u8, + output_ptr: (&mut argsort[0]) as *mut u32 as *mut u8, + len: core::mem::size_of_val(arr), + }; + + unsafe { + starstream::starstream_run_unconstrained( + sort_lookup_argument_external as *const (), + &args as *const _ as *const (), + ) + }; + + for i in 0..argsort.len() { + // 3. No repetitions were introduced. + assert!(!seen_before[i]); + seen_before[i] = true; + // 2. The result is made of elements of the original array (otherwise + // the indexes will be out of bounds). + argsort[i] = arr[argsort[i] as usize]; + if i > 0 { + // 1. The result is actually a sort. + assert!(argsort[i] >= argsort[i - 1]); + } + } + + argsort + } + + #[cfg(feature = "starstream")] + pub(crate) unsafe extern "C" fn sort_lookup_argument_external(args_erased: *const ()) { + let args = unsafe { args_erased.cast::().as_ref().unwrap() }; + + let value_array_in_bytes = unsafe { core::slice::from_raw_parts(args.input_ptr, args.len) }; + + let (_, s, _) = unsafe { value_array_in_bytes.align_to::() }; + + let index_array_in_bytes = + unsafe { core::slice::from_raw_parts_mut(args.output_ptr, args.len) }; + + let (_, si, _) = unsafe { index_array_in_bytes.align_to_mut::() }; + + si.sort_by_key(|i| s[*i as usize]); + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::EmptyStackError => write!(f, "attempted to pop from an empty stack"), + Error::TypeError => write!(f, "Type error"), + } + } +} + +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl core::error::Error for Error {} + +#[cfg(test)] +mod tests { + use super::*; + + use rand::SeedableRng as _; + use talc::*; + + const MEMORY_SIZE: usize = 10 * 1024; + + // max size of the stack + static mut ARENA: [u8; MEMORY_SIZE] = [0; MEMORY_SIZE]; + + static ALLOCATOR: Talck, ClaimOnOom> = Talc::new(unsafe { + ClaimOnOom::new(Span::from_array(core::ptr::addr_of!(ARENA).cast_mut())) + }) + .lock(); + + #[test] + fn add_x_y() { + let stack = Stack::new(&ALLOCATOR); + + let rng = rand_chacha::ChaCha12Rng::seed_from_u64(42); + + run( + &[ + Op::Push(Value::from_slice(&[3u32, 5], &ALLOCATOR)), + Op::Push(Value::from_slice(&[1, 2], &ALLOCATOR)), + Op::Add, + ], + &ALLOCATOR, + stack, + rng, + ) + .unwrap(); + } + + #[test] + fn test_dup() { + let stack = Stack::new(&ALLOCATOR); + + let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(42); + + let stack1 = run( + &[ + Op::Push(Value::from_slice(&[3, 1], &ALLOCATOR)), + Op::Push(Value::from_slice(&[5, 5], &ALLOCATOR)), + Op::Dup { i: 1 }, + ], + &ALLOCATOR, + stack, + &mut rng, + ) + .unwrap(); + + let stack = Stack::new(&ALLOCATOR); + + let stack2 = run( + &[ + Op::Push(Value::from_slice(&[3, 1], &ALLOCATOR)), + Op::Push(Value::from_slice(&[5, 5], &ALLOCATOR)), + Op::Push(Value::from_slice(&[3, 1], &ALLOCATOR)), + ], + &ALLOCATOR, + stack, + &mut rng, + ) + .unwrap(); + + assert_eq!(stack1, stack2); + } + + #[test] + fn test_sort() { + let stack = Stack::new(&ALLOCATOR); + + let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(42); + + let mut stack = run( + &[ + Op::Push(Value::from_slice(&[3, 1, 2], &ALLOCATOR)), + Op::Sort, + ], + &ALLOCATOR, + stack, + &mut rng, + ) + .unwrap(); + + let res = stack.pop().unwrap(); + + match res { + Value::Array(items) => assert!(items.is_sorted()), + Value::TestResult(_) => todo!(), + } + } +} diff --git a/example_toy_vm/toy_vm/src/value.rs b/example_toy_vm/toy_vm/src/value.rs new file mode 100644 index 00000000..df6c48e0 --- /dev/null +++ b/example_toy_vm/toy_vm/src/value.rs @@ -0,0 +1,46 @@ +use alloc::{alloc::Allocator, rc::Rc, vec::Vec}; + +use crate::TestResult; + +#[derive(Clone)] +pub enum Value { + Array(Rc, A>), + TestResult(TestResult), +} + +impl Eq for Value {} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Array(l0), Self::Array(r0)) => l0 == r0, + (Self::TestResult(l0), Self::TestResult(r0)) => l0 == r0, + _ => false, + } + } +} + +impl Value { + pub fn from_slice(slice: &[u32], allocator: A) -> Self { + let mut a = Vec::new_in(allocator); + a.extend(slice); + + Self::Array(Rc::new_in(a, allocator)) + } + + pub fn from_iter(iter: impl Iterator, allocator: A) -> Self { + let mut a = Vec::new_in(allocator); + a.extend(iter); + + Self::Array(Rc::new_in(a, allocator)) + } +} + +impl core::fmt::Debug for Value { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Value::Array(array) => f.debug_tuple("Array").field(&array.as_slice()).finish(), + Value::TestResult(result) => f.debug_tuple("TestResult").field(result).finish(), + } + } +} diff --git a/starstream_sys/src/lib.rs b/starstream_sys/src/lib.rs index 035779ca..ddada46c 100644 --- a/starstream_sys/src/lib.rs +++ b/starstream_sys/src/lib.rs @@ -171,6 +171,9 @@ unsafe extern "C" { #[link_name = "starstream_keccak256"] unsafe fn precompile_keccak256(buf: *const u8, len: usize, result: *mut u8); + + #[link_name = "starstream_run_unconstrained"] + pub unsafe fn starstream_run_unconstrained(f_ptr: *const (), args: *const ()); } #[inline] diff --git a/starstream_vm/src/lib.rs b/starstream_vm/src/lib.rs index 2bf6194b..2101b8c1 100644 --- a/starstream_vm/src/lib.rs +++ b/starstream_vm/src/lib.rs @@ -304,6 +304,35 @@ fn starstream_env(linker: &mut Linker, module: &str, this_code: &ContractC }, ) .unwrap(); + + linker + .func_wrap( + "env", + "starstream_run_unconstrained", + |caller: Caller, fptr: u32, args: u32| { + run_unconstrained(caller, fptr, args); + }, + ) + .unwrap(); +} + +pub(crate) fn run_unconstrained(mut caller: Caller<'_, T>, fptr: u32, args: u32) { + let table = caller + .get_export("__indirect_function_table") + .unwrap() + .into_table() + .unwrap(); + + let indirect_table = table.get(&caller, fptr).unwrap(); + let f = indirect_table.funcref().unwrap().func().unwrap(); + + // TODO: we need some way to guarantee that the function only uses the + // memory it needs. + // + // otherwise it's not really possible for the caller to verify the result + // properly. + f.call(&mut caller, &[Value::I32(args as i32)], &mut []) + .unwrap(); } /// Fulfiller of imports from `starstream_utxo_env`. diff --git a/starstream_vm/src/nebula.rs b/starstream_vm/src/nebula.rs index d5ad5b16..7a3190d9 100644 --- a/starstream_vm/src/nebula.rs +++ b/starstream_vm/src/nebula.rs @@ -50,6 +50,15 @@ fn starstream_env_zk(linker: &mut Linker, module: &str, this_code: CodeHas }, ) .unwrap(); + linker + .func_wrap( + "env", + "starstream_run_unconstrained", + |caller: Caller, fptr: u32, args: u32| { + crate::run_unconstrained(caller, fptr, args); + }, + ) + .unwrap(); // TODO: effect handler stuff } diff --git a/starstream_vm/tests/toy_vm_example.rs b/starstream_vm/tests/toy_vm_example.rs new file mode 100644 index 00000000..5600898f --- /dev/null +++ b/starstream_vm/tests/toy_vm_example.rs @@ -0,0 +1,18 @@ +use starstream_vm::*; + +pub fn main() { + env_logger::Builder::new().init(); + + let mut tx = Transaction::new(); + dbg!(&tx); + + let example_contract = tx.code_cache().load_debug("example_toy_vm"); + + let utxo = tx.run_coordination_script(&example_contract, "new_utxo", vec![]); + // dbg!(&tx); + + tx.run_coordination_script(&example_contract, "state_transition", vec![utxo]); + dbg!(&tx); + + // tx.do_nebula_stuff(); +} From 6853bf4dd1cf095f03c32295010b1f658b8b892b Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com> Date: Mon, 9 Jun 2025 12:57:38 -0300 Subject: [PATCH 2/3] fix the test missing #[test] --- starstream_vm/tests/toy_vm_example.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/starstream_vm/tests/toy_vm_example.rs b/starstream_vm/tests/toy_vm_example.rs index 5600898f..4b35f8c4 100644 --- a/starstream_vm/tests/toy_vm_example.rs +++ b/starstream_vm/tests/toy_vm_example.rs @@ -1,7 +1,13 @@ use starstream_vm::*; +#[test] pub fn main() { - env_logger::Builder::new().init(); + std::process::Command::new("cargo") + .arg("build") + .arg("-p") + .arg("example_toy_vm") + .status() + .unwrap(); let mut tx = Transaction::new(); dbg!(&tx); From 6eae3c1e3b4638567c88f79138ccaaa427519ae1 Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com> Date: Mon, 9 Jun 2025 13:04:15 -0300 Subject: [PATCH 3/3] fix cargo test --workspace not compiling --- example_toy_vm/example/Cargo.toml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/example_toy_vm/example/Cargo.toml b/example_toy_vm/example/Cargo.toml index 8e7fa7f8..fe232c03 100644 --- a/example_toy_vm/example/Cargo.toml +++ b/example_toy_vm/example/Cargo.toml @@ -15,10 +15,18 @@ test = false name = "example_toy_vm" test = false +# do this only so that `cargo test --workspace` doesn't fail by trying to +# link with starstream functions, since outside of wasm those are not imported +# dynamically. +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +toy_vm = { path = "../toy_vm", features = [] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +toy_vm = { path = "../toy_vm", features = ["starstream"] } + [dependencies] stack_dst = { version = "0.8.1", default-features = false, features = ["const_generics"] } starstream_sys = { path = "../../starstream_sys" } -toy_vm = { path = "../toy_vm", features = ["starstream"] } talc = "4.4.2" spin = "0.10.0" rand = { version = "0.9.1", default-features = false }