diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 13e8ee6d..5a3ef561 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,10 +16,11 @@ jobs: # Build crates and whatever else. - run: make # Test all crates, except those that force-target WASM. - - run: cargo test --workspace --exclude example_contract --exclude example_contract_permissioned --exclude starstream_sys --exclude starstream_sandbox + - run: cargo test --workspace --exclude example_contract --exclude example_contract_permissioned --exclude impact_vm_example --exclude starstream_sys --exclude starstream_sandbox # Run split test scripts. - run: ./test - run: ./test_permissioned + - run: ./test_impact # Cosmetic checks. - run: cargo clippy - run: cargo fmt --check diff --git a/Cargo.lock b/Cargo.lock index a8c1f930..fa885c18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -811,6 +811,14 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "impact_vm_example" +version = "0.0.0" +dependencies = [ + "stack_dst", + "starstream_sys", +] + [[package]] name = "indexmap" version = "2.9.0" diff --git a/Cargo.toml b/Cargo.toml index aa2e0456..b34572a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "example_contract", "example_contract_permissioned", + "impact_vm_example", "starstream_cli", "starstream_compiler", "starstream_sandbox", diff --git a/Makefile b/Makefile index 6f64c425..33ff469e 100644 --- a/Makefile +++ b/Makefile @@ -13,3 +13,5 @@ vsc: target/debug/examples/self_test: cargo target/debug/examples/self_test_permissioned: cargo + +target/debug/examples/impact_vm: cargo diff --git a/impact_vm_example/Cargo.toml b/impact_vm_example/Cargo.toml new file mode 100644 index 00000000..9fd6990c --- /dev/null +++ b/impact_vm_example/Cargo.toml @@ -0,0 +1,12 @@ +cargo-features = ["per-package-target"] + +[package] +name = "impact_vm_example" +version = "0.0.0" +edition = "2024" + +forced-target = "wasm32-unknown-unknown" + +[dependencies] +stack_dst = { version = "0.8.1", default-features = false, features = ["const_generics"] } +starstream_sys = { path = "../starstream_sys" } diff --git a/impact_vm_example/build.rs b/impact_vm_example/build.rs new file mode 100644 index 00000000..1dc381b8 --- /dev/null +++ b/impact_vm_example/build.rs @@ -0,0 +1,20 @@ +use std::env; +use std::path::Path; + +fn main() { + let lib_name = "impact_vm"; + + let dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + + let lib_dir = Path::new(&dir).join("libs"); + + println!("cargo:rustc-link-search=native={}", lib_dir.display()); + + println!("cargo:rustc-link-lib=static={}", lib_name); + + println!( + "cargo:rerun-if-changed={}/{}.a", + lib_dir.display(), + lib_name + ); +} diff --git a/impact_vm_example/libs/libimpact_vm.a b/impact_vm_example/libs/libimpact_vm.a new file mode 100644 index 00000000..36eea318 Binary files /dev/null and b/impact_vm_example/libs/libimpact_vm.a differ diff --git a/impact_vm_example/src/defs.rs b/impact_vm_example/src/defs.rs new file mode 100644 index 00000000..4a3f465f --- /dev/null +++ b/impact_vm_example/src/defs.rs @@ -0,0 +1,56 @@ +use starstream::utxo_import; + +#[link(name = "impact_vm", kind = "static")] +unsafe extern "C" { + pub fn run_program( + program: *const u8, + size: usize, + state_in: *const (), + out_ptr: *mut *const (), + ) -> u32; + pub fn deserialize_state( + buffer: *const u8, + buffer_len: usize, + state_out_ptr: *mut *const (), + ) -> u32; + pub fn serialize_state( + state_in_ptr: *const (), + buffer: *mut *const u8, + buffer_len: *mut usize, + ) -> u32; + pub fn free_state(state_ptr: *const ()); + pub fn free_buffer(buffer: *const u8, len: usize); +} + +#[link(wasm_import_module = "starstream_utxo:impact_vm_example")] +unsafe extern "C" { + safe fn starstream_new_ImpactVM_new() -> ImpactVM; + safe fn starstream_mutate_ImpactVM_increment(utxo: ImpactVM); + safe fn starstream_query_ImpactVM_get_counter(utxo: ImpactVM) -> u32; +} + +utxo_import! { + "starstream_utxo:impact_vm_example"; + ImpactVM; + starstream_status_ImpactVM; + starstream_resume_ImpactVM; + (); +} + +impl ImpactVM { + #[inline] + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + starstream_new_ImpactVM_new() + } + + #[inline] + pub fn increment(self) { + starstream_mutate_ImpactVM_increment(self); + } + + #[inline] + pub fn get_counter(self) -> u32 { + starstream_query_ImpactVM_get_counter(self) + } +} diff --git a/impact_vm_example/src/main.rs b/impact_vm_example/src/main.rs new file mode 100644 index 00000000..61916d0d --- /dev/null +++ b/impact_vm_example/src/main.rs @@ -0,0 +1,156 @@ +#![no_main] + +mod defs; + +use defs::{deserialize_state, free_buffer, free_state, run_program, serialize_state}; +use starstream::eprintln; + +// TODO: don't really know how to setup the panic handler without no_std +pub fn hook(info: &std::panic::PanicHookInfo) { + #[link(wasm_import_module = "env")] + unsafe extern "C" { + unsafe fn abort(); + } + + unsafe { + eprintln!("{info}"); + + abort(); + #[allow(clippy::empty_loop)] + loop {} + } +} + +#[inline] +pub fn set_once() { + use std::sync::Once; + static SET_HOOK: Once = Once::new(); + SET_HOOK.call_once(|| { + std::panic::set_hook(Box::new(hook)); + }); +} + +pub struct ImpactVM { + // this is a pointer to the heap allocated state. + // + // since the library is statically linked it shares memory with the utxo. + // otherwise this could hold a serialized versioned of the current state. + current_state: *const (), +} + +impl ImpactVM { + #[allow(clippy::new_ret_no_self)] + pub fn new(sleep: fn(&mut Self), current_state: *const ()) { + let mut this = ImpactVM { current_state }; + sleep(&mut this); + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn starstream_new_ImpactVM_new() { + // an array with a cell with a number set to 1. + // this in most cases be an input to the coordination script. + let initial_state = [ + 0, 2, 2, 225, 1, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 1, 1, 65, 1, 0, 0, 0, + 32, 0, 0, 0, 201, 170, 41, 9, 144, 82, 87, 49, 160, 49, 144, 30, 239, 178, 231, 84, 119, + 202, 205, 66, 153, 73, 239, 83, 224, 62, 167, 161, 12, 99, 232, 187, 1, 0, 0, 0, 1, 1, 0, + 0, 0, 32, 0, 0, 0, 235, 20, 33, 218, 247, 211, 225, 169, 176, 105, 251, 68, 191, 56, 15, + 145, 40, 40, 175, 74, 10, 210, 163, 206, 65, 152, 68, 89, 161, 106, 141, 101, 7, 0, 0, 0, + 3, 1, 1, 0, 0, 0, 0, + ]; + + let mut current_state = std::ptr::null::<()>(); + + unsafe { + assert_eq!( + deserialize_state( + initial_state.as_ptr(), + initial_state.len(), + &mut current_state as *mut _, + ), + 0 + ) + }; + + ImpactVM::new(starstream::sleep_mut::<(), ImpactVM>, current_state) +} + +#[unsafe(no_mangle)] +pub extern "C" fn starstream_mutate_ImpactVM_increment(this: &mut ImpactVM) { + // serialized version of: + // + // dup 0 + // idx [<[-]: f>] + // addi 1 + // pushs <[-]: f> + // swap 0 + // ins 1 + let program = [ + 0u8, 6, 0, 0, 0, 2, 2, 48, 2, 2, 80, 64, 65, 2, 2, 14, 1, 2, 2, 17, 64, 65, 2, 2, 64, 2, 2, + 145, + ]; + + unsafe { + let mut out = std::ptr::null::<()>(); + assert!( + run_program( + program.as_ptr(), + program.len(), + this.current_state, + &mut out as *mut _ + ) == 0 + ); + + assert!(!out.is_null()); + + free_state(this.current_state); + + this.current_state = out; + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn starstream_query_ImpactVM_get_counter(this: &mut ImpactVM) -> u32 { + unsafe { + let mut serialized_state_ptr = std::ptr::null::(); + let mut serialized_state_len = 0usize; + + assert_eq!( + serialize_state( + this.current_state, + &mut serialized_state_ptr as *mut _, + &mut serialized_state_len as *mut _, + ), + 0 + ); + + let buffer = std::slice::from_raw_parts(serialized_state_ptr, serialized_state_len); + + // TODO: this only works as long as the value fits in a byte. but this + // is mostly here for debugging purposes, since it's probably not worth + // writing bindings for deserialization as that would be done in the + // frontend. + let counter = buffer[23] as u32; + + free_buffer(serialized_state_ptr, serialized_state_len); + + counter + } +} + +// ---------------------------------------------------------------------------- +// Coordination script +#[unsafe(no_mangle)] +pub extern "C" fn new_counter() -> defs::ImpactVM { + defs::ImpactVM::new() +} + +#[unsafe(no_mangle)] +pub extern "C" fn increase_counter(utxo: defs::ImpactVM) { + utxo.increment(); +} + +#[unsafe(no_mangle)] +pub extern "C" fn get_counter(utxo: defs::ImpactVM) -> u32 { + utxo.get_counter() +} diff --git a/starstream_vm/examples/impact_vm.rs b/starstream_vm/examples/impact_vm.rs new file mode 100644 index 00000000..cb89ae07 --- /dev/null +++ b/starstream_vm/examples/impact_vm.rs @@ -0,0 +1,21 @@ +use starstream_vm::*; + +pub fn main() { + let mut tx = Transaction::new(); + + let example_contract = tx.code_cache().load_debug("impact_vm_example"); + + let utxo = tx.run_coordination_script(&example_contract, "new_counter", vec![]); + + let counter = tx.run_coordination_script(&example_contract, "get_counter", vec![utxo.clone()]); + assert_eq!(counter.i32().expect("invalid counter type") as u32, 1); + + tx.run_coordination_script(&example_contract, "increase_counter", vec![utxo.clone()]); + + let counter = tx.run_coordination_script(&example_contract, "get_counter", vec![utxo.clone()]); + assert_eq!(counter.i32().expect("invalid counter type") as u32, 2); + + tx.run_coordination_script(&example_contract, "increase_counter", vec![utxo.clone()]); + let counter = tx.run_coordination_script(&example_contract, "get_counter", vec![utxo.clone()]); + assert_eq!(counter.i32().expect("invalid counter type") as u32, 3); +} diff --git a/test_impact b/test_impact new file mode 100755 index 00000000..144ed842 --- /dev/null +++ b/test_impact @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail +make target/debug/examples/impact_vm +exec target/debug/examples/impact_vm "$@"