diff --git a/Cargo.lock b/Cargo.lock index 9e02b82..e90d17e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.6.20" @@ -384,8 +393,11 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" name = "jade" version = "0.0.13-pre.1" dependencies = [ + "anyhow", "jade-derive", + "jade-testing", "polkavm-derive", + "serde", "serde-jam", "spacejam-service", ] @@ -400,6 +412,22 @@ dependencies = [ "syn", ] +[[package]] +name = "jade-testing" +version = "0.0.13-pre.1" +dependencies = [ + "anyhow", + "cjam", + "etc", + "hex", + "serde", + "serde-jam", + "spacejam-service", + "spacevm-sys", + "tracing", + "tracing-subscriber", +] + [[package]] name = "jam-codec" version = "0.1.1" @@ -448,6 +476,12 @@ dependencies = [ "libc", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.176" @@ -470,6 +504,15 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.7.5" @@ -477,12 +520,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] -name = "null-authorizer" +name = "nauth" version = "0.0.13-pre.1" dependencies = [ + "cjam", "jade", - "polkavm-derive", - "spacejam-service", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", ] [[package]] @@ -512,6 +563,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "polkavm" version = "0.24.0" @@ -666,6 +723,23 @@ dependencies = [ "smallvec", ] +[[package]] +name = "regex-automata" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -752,6 +826,15 @@ dependencies = [ "serde", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "slice-group-by" version = "0.3.1" @@ -823,6 +906,8 @@ version = "0.0.13-pre.1" dependencies = [ "cjam", "jade", + "nauth", + "serde", "serde-jam", ] @@ -875,6 +960,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "toml" version = "0.8.23" @@ -946,6 +1040,67 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + [[package]] name = "typenum" version = "1.18.0" @@ -970,6 +1125,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" @@ -1015,6 +1176,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -1039,6 +1209,22 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.4" @@ -1049,7 +1235,7 @@ dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.53.0", "windows_i686_msvc 0.53.0", "windows_x86_64_gnu 0.53.0", "windows_x86_64_gnullvm 0.53.0", @@ -1062,6 +1248,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.0" @@ -1074,6 +1266,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.0" @@ -1086,12 +1284,24 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.0" @@ -1104,6 +1314,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.0" @@ -1116,6 +1332,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.0" @@ -1128,6 +1350,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.0" @@ -1140,6 +1368,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.0" diff --git a/Cargo.toml b/Cargo.toml index b3e82da..ea3f7e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,12 @@ repository = "https://github.com/spacejamapp/jade" cjam = { path = "crates/cli" } jade = { path = "crates/jade", default-features = false } jade-derive = { path = "crates/jade/derive" } -sys = { path = "crates/sys" } +spacevm = { path = "crates/sys", package = "spacevm-sys" } +testing = { path = "crates/testing", package = "jade-testing" } + +# services +nauth = { path = "services/nauth" } +stoken = { path = "services/stoken" } # spacejam dependencies codec = { package = "serde-jam", version = "0.0.13-pre.1", default-features = false } diff --git a/crates/cli/src/builder.rs b/crates/cli/src/builder.rs index f9c4d04..5a19159 100644 --- a/crates/cli/src/builder.rs +++ b/crates/cli/src/builder.rs @@ -161,7 +161,7 @@ pub fn build_pvm_blob( } child - .args(["build", "-Z", "build-std=core,alloc"]) + .args(["rustc", "-Z", "build-std=core,alloc"]) .arg(profile.to_arg()) .arg("--target") .arg(target_json_path) @@ -170,7 +170,9 @@ pub fn build_pvm_blob( "tiny" } else { "" - }) // Disable stripping for LLVM 19 compatibility + }) + .arg("--lib") + .arg("--crate-type=cdylib") .env( "RUSTFLAGS", format!( @@ -223,12 +225,13 @@ pub fn build_pvm_blob( .expect("Failed to link pvm program:"); // Write out a full `.pvm` blob for debugging/inspection. - fs::create_dir_all(out_dir).expect("Failed to create jam directory"); - let output_path_pvm = out_dir.join(format!("{}.pvm", &info.name)); + let jam_out = out_dir.join("jam"); + fs::create_dir_all(&jam_out).expect("Failed to create jam directory"); + let output_path_pvm = jam_out.join(format!("{}.pvm", &info.name)); fs::write(output_path_pvm, &linked).expect("Error writing resulting binary"); let name = info.name.clone(); let metadata = ConventionalMetadata::Info(info).encode().into(); - let output_file = blob_type.output_file(out_dir, &name); + let output_file = blob_type.output_file(&jam_out, &name); if !matches!(blob_type, BlobType::CoreVmGuest) { let parts = polkavm_linker::ProgramParts::from_bytes(linked.into()) .expect("failed to deserialize linked PolkaVM program"); diff --git a/crates/cli/src/cmd/build.rs b/crates/cli/src/cmd/build.rs index 802ac5d..b7d708d 100644 --- a/crates/cli/src/cmd/build.rs +++ b/crates/cli/src/cmd/build.rs @@ -1,12 +1,11 @@ //! `jam build` command -use std::path::PathBuf; - use crate::{ builder, manifest::{ModuleType, Profile}, }; use clap::Parser; +use std::path::PathBuf; /// CLI utility for building PVM code blobs, particularly services and authorizers. #[derive(Parser, Debug, Default)] diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index 9251a30..dd5e2a6 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -19,11 +19,10 @@ pub fn build(package: &str, module: Option, path: Option) -> // Build the service let target = etc::find_up("target")?; - let jam = target.join("jam"); let current = path .map(PathBuf::from) .unwrap_or_else(|| env::current_dir().expect("Unable to get current directory")); - let binary = jam.join(package).join(format!("{package}.jam")); + let binary = target.join("jam").join(format!("{package}.jam")); let rebuild = if !binary.exists() { true } else { @@ -36,7 +35,7 @@ pub fn build(package: &str, module: Option, path: Option) -> if let Some(module) = module { build.module = module; } - build.target = Some(jam.join(package)); + build.target = Some(target); build.run()?; } diff --git a/crates/jade/Cargo.toml b/crates/jade/Cargo.toml index 8bd6ed0..00929eb 100644 --- a/crates/jade/Cargo.toml +++ b/crates/jade/Cargo.toml @@ -8,12 +8,17 @@ homepage.workspace = true repository.workspace = true [dependencies] +anyhow.workspace = true codec.workspace = true jade-derive.workspace = true +serde.workspace = true service.workspace = true polkavm-derive.workspace = true +testing = { workspace = true, optional = true } [features] default = [] -std = ["codec/std", "service/std"] +logging = [] +testing = ["dep:testing"] +std = ["anyhow/std", "codec/std", "serde/std", "service/std"] tiny = [] diff --git a/crates/jade/derive/src/accumulate.rs b/crates/jade/derive/src/accumulate.rs index f09baad..49b5ebb 100644 --- a/crates/jade/derive/src/accumulate.rs +++ b/crates/jade/derive/src/accumulate.rs @@ -22,7 +22,8 @@ pub fn accumulate(_args: TokenStream, input: TokenStream) -> TokenStream { let buf = unsafe { core::slice::from_raw_parts(ptr as *const u8, size as usize) }; let jade::service::vm::AccumulateParams {slot, id, results} = jade::codec::decode(buf).expect("failed to decode accumulate parameters"); - if let Some(result) = #funame(slot, id, results) { + let operands = jade::host::fetch::operands().expect("failed to fetch operands"); + if let Some(result) = #funame(slot, id, operands) { ((&result).as_ptr() as u64, result.len() as u64) } else { (0, 0) diff --git a/crates/jade/derive/src/authorize.rs b/crates/jade/derive/src/authorize.rs index b845de4..dd66d93 100644 --- a/crates/jade/derive/src/authorize.rs +++ b/crates/jade/derive/src/authorize.rs @@ -20,9 +20,10 @@ pub fn is_authorized(_args: TokenStream, input: TokenStream) -> TokenStream { #fun let buf = unsafe { core::slice::from_raw_parts(ptr as *const u8, size as usize) }; - let (param, package, core_index): (AuthConfig, WorkPackage, CoreIndex) = - jade::codec::decode(buf).expect("failed to decode is_authorized parameters"); - let result = #funame(param, package, core_index); + let core_index: CoreIndex = + jade::codec::decode(buf).inspect_err(|e| jade::error!("decoded is_authorized parameters: {:?}", e)) + .expect("failed to decode is_authorized parameters"); + let result = #funame(core_index); ((&result).as_ptr() as u64, result.len() as u64) } } diff --git a/crates/jade/src/host/general.rs b/crates/jade/src/host/general.rs new file mode 100644 index 0000000..1d4bd75 --- /dev/null +++ b/crates/jade/src/host/general.rs @@ -0,0 +1,80 @@ +//! General host calls + +use crate::host::import; +use core::ptr; + +/// Get the gas used +pub fn gas() -> u64 { + unsafe { import::gas() } +} + +/// Fetch operations +pub mod fetch { + use super::*; + use crate::prelude::{Vec, vec}; + use anyhow::Result; + use service::vm::Operand; + + /// Fetch a value from the storage + pub fn operands() -> Result> { + let len = unsafe { import::fetch(core::ptr::null_mut(), 0, 0, 14, 0, 0) }; + let mut target = vec![0; len as usize]; + let _ = unsafe { import::fetch(target.as_mut_ptr(), 0, len as u64, 14, 0, 0) }; + codec::decode(target.as_slice()).map_err(Into::into) + } +} + +/// Storage operations +pub mod storage { + use super::*; + use anyhow::Result; + + /// Read a value from the storage + pub fn read(key: impl AsRef<[u8]>) -> Option { + let len = unsafe { + import::read( + u64::MAX as _, + key.as_ref().as_ptr(), + key.as_ref().len() as u64, + ptr::null_mut(), + 0, + 0, + ) + }; + + if len == u64::MAX { + return None; + } else if len == 0 { + return None; + } + + let ptr = unsafe { + import::read( + u64::MAX as _, + key.as_ref().as_ptr(), + key.as_ref().len() as u64, + ptr::null_mut(), + 0, + len, + ) + }; + + let value = unsafe { core::slice::from_raw_parts(ptr as _, len as usize) }; + codec::decode(value).ok() + } + + /// Write a value to the storage + pub fn write(key: impl AsRef<[u8]>, value: &W) -> Result<()> { + let value = codec::encode(value)?; + unsafe { + import::write( + key.as_ref().as_ptr(), + key.as_ref().len() as u64, + value.as_ptr(), + value.len() as u64, + ); + }; + + Ok(()) + } +} diff --git a/crates/jade/src/host/import.rs b/crates/jade/src/host/import.rs new file mode 100644 index 0000000..d1b3624 --- /dev/null +++ b/crates/jade/src/host/import.rs @@ -0,0 +1,41 @@ +//! Imports of host calls + +#[polkavm_derive::polkavm_import] +extern "C" { + // NOTE: This is NOT part of the GP. + #[polkavm_import(index = 100)] + pub fn log( + level: u64, + target_ptr: *const u8, + target_len: u64, + text_ptr: *const u8, + text_len: u64, + ); + + /// Get the gas used + #[polkavm_import(index = 0)] + pub fn gas() -> u64; + + /// Fetch a value from the storage + #[polkavm_import(index = 1)] + pub fn fetch(buffer: *mut u8, offset: u64, buffer_len: u64, kind: u64, a: u64, b: u64) -> u64; + + /// Read a value from the storage + #[polkavm_import(index = 3)] + pub fn read( + service: u64, + key_ptr: *const u8, + key_len: u64, + out: *mut u8, + offset: u64, + out_len: u64, + ) -> u64; + + /// Write a value to the storage + #[polkavm_import(index = 4)] + pub fn write(key_ptr: *const u8, key_len: u64, value: *const u8, value_len: u64) -> u64; + + /// Get the info of the service + #[polkavm_import(index = 5)] + pub fn info(service: u64, service_info_ptr: *mut u8) -> u64; +} diff --git a/crates/jade/src/host/mod.rs b/crates/jade/src/host/mod.rs new file mode 100644 index 0000000..5858b5e --- /dev/null +++ b/crates/jade/src/host/mod.rs @@ -0,0 +1,6 @@ +//! Host calls + +pub use general::*; + +mod general; +pub(crate) mod import; diff --git a/crates/jade/src/lib.rs b/crates/jade/src/lib.rs index e038808..abd3abb 100644 --- a/crates/jade/src/lib.rs +++ b/crates/jade/src/lib.rs @@ -6,8 +6,13 @@ extern crate alloc; pub use {codec, jade_derive::*, polkavm_derive, service}; +pub mod host; +pub mod logging; pub mod prelude; +#[cfg(feature = "testing")] +pub use testing; + #[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] #[global_allocator] static ALLOCATOR: polkavm_derive::LeakingAllocator = polkavm_derive::LeakingAllocator; diff --git a/crates/jade/src/logging.rs b/crates/jade/src/logging.rs new file mode 100644 index 0000000..0eb1fea --- /dev/null +++ b/crates/jade/src/logging.rs @@ -0,0 +1,126 @@ +//! This module is extracted from `jam-pvm-common` + +#[cfg(any(feature = "logging", doc))] +pub mod stuff { + + /// Log a message with the `error` level. Regular formatting may be used. + #[macro_export] + macro_rules! error { + (target=$target:expr,$($arg:tt)*) => { + $crate::logging::stuff::log_target(0, $target, &$crate::prelude::format!($($arg)*)); + }; + ($($arg:tt)*) => { + $crate::logging::stuff::log(0, &$crate::prelude::format!($($arg)*)); + }; + } + + /// Log a message with the `warn` level. Regular formatting may be used. + #[macro_export] + macro_rules! warn { + (target=$target:expr,$($arg:tt)*) => { + $crate::logging::stuff::log_target(1, $target, &$crate::prelude::format!($($arg)*)); + }; + ($($arg:tt)*) => { + $crate::logging::stuff::log(1, &$crate::prelude::format!($($arg)*)); + }; + } + + /// Log a message with the `info` level. Regular formatting may be used. + #[macro_export] + macro_rules! info { + (target=$target:expr,$($arg:tt)*) => { + $crate::logging::stuff::log_target(2, $target, &$crate::prelude::format!($($arg)*)); + }; + ($($arg:tt)*) => { + $crate::logging::stuff::log(2, &$crate::prelude::format!($($arg)*)); + }; + } + + /// Log a message with the `debug` level. Regular formatting may be used. + #[macro_export] + macro_rules! debug { + (target=$target:expr,$($arg:tt)*) => { + $crate::logging::stuff::log_target(3, $target, &$crate::prelude::format!($($arg)*)); + }; + ($($arg:tt)*) => { + $crate::logging::stuff::log(3, &$crate::prelude::format!($($arg)*)); + }; + } + + /// Log a message with the `trace` level. Regular formatting may be used. + #[macro_export] + macro_rules! trace { + (target=$target:expr,$($arg:tt)*) => { + $crate::logging::stuff::log_target(4, $target, &$crate::prelude::format!($($arg)*)); + }; + ($($arg:tt)*) => { + $crate::logging::stuff::log(4, &$crate::prelude::format!($($arg)*)); + }; + } + + // CAUTION: Not public API. DO NOT USE. + pub fn log_target(level: u64, target: &str, msg: &str) { + let t = target.as_bytes(); + let m = msg.as_bytes(); + unsafe { + crate::host::import::log( + level, + t.as_ptr(), + t.len() as u64, + m.as_ptr(), + m.len() as u64, + ) + } + } + + // CAUTION: Not public API. DO NOT USE. + pub fn log(level: u64, msg: &str) { + let m = msg.as_bytes(); + unsafe { + crate::host::import::log(level, core::ptr::null(), 0u64, m.as_ptr(), m.len() as u64) + } + } +} + +#[cfg(not(any(feature = "logging", doc)))] +pub mod stuff { + /// Log a message with the `error` level. Regular formatting may be used.mod stuff { + #[macro_export] + macro_rules! error { + ($($arg:tt)*) => { + { let _ = ($( $arg, )*); } + }; + } + + /// Log a message with the `warn` level. Regular formatting may be used. + #[macro_export] + macro_rules! warn { + ($($arg:tt)*) => { + { let _ = ($( $arg, )*); } + }; + } + + /// Log a message with the `info` level. Regular formatting may be used. + #[macro_export] + macro_rules! info { + ($($arg:tt)*) => { + { let _ = ($( $arg, )*); } + }; + } + + /// Log a message with the `debug` level. Regular formatting may be used. + #[macro_export] + macro_rules! debug { + ($($arg:tt)*) => { + { let _ = ($( $arg, )*); } + }; + } + + /// Log a message with the `trace` level. Regular formatting may be used. + #[macro_export] + macro_rules! trace { + ($($arg:tt)*) => { + { let _ = ($( $arg, )*); } + }; + } +} diff --git a/crates/jade/src/prelude.rs b/crates/jade/src/prelude.rs index ee76447..894177d 100644 --- a/crates/jade/src/prelude.rs +++ b/crates/jade/src/prelude.rs @@ -4,13 +4,13 @@ pub use codec; pub use service::{OpaqueHash, service::WorkPackage}; #[cfg(feature = "std")] -pub use std::{string::String, vec, vec::Vec}; +pub use std::{collections::BTreeMap, format, string::String, vec, vec::Vec}; #[cfg(not(feature = "std"))] -pub use alloc::{string::String, vec, vec::Vec}; +pub use alloc::{collections::BTreeMap, format, string::String, vec, vec::Vec}; /// Type to represent the index of a compute core. -pub type CoreIndex = u32; +pub type CoreIndex = u16; /// Type to represent the authorizer configuration. pub type AuthConfig = Vec; diff --git a/crates/sys/Cargo.toml b/crates/sys/Cargo.toml index faada42..cf8e551 100644 --- a/crates/sys/Cargo.toml +++ b/crates/sys/Cargo.toml @@ -19,4 +19,5 @@ service.workspace = true [features] default = [] tiny = [] +interp = [] std = ["anyhow/std", "codec/std", "service/std"] diff --git a/crates/sys/src/lib.rs b/crates/sys/src/lib.rs index 9b14cac..fd7d7a2 100644 --- a/crates/sys/src/lib.rs +++ b/crates/sys/src/lib.rs @@ -1,6 +1,7 @@ //! SpaceVM system interface use crate::abi::Buffer; +pub use abi::init_logger; use anyhow::Result; use service::{ api::{AccumulateArgs, Accumulated, AuthorizeArgs, RefineArgs}, @@ -15,8 +16,8 @@ pub fn authorize(args: AuthorizeArgs) -> Result { len: encoded.len(), }; - let output = unsafe { abi::authorize(input) }; - codec::decode(output.as_slice()).map_err(Into::into) + let output: Buffer = unsafe { abi::authorize(input) }; + codec::decode(&output.to_vec()).map_err(Into::into) } /// Run the refine invocation @@ -28,7 +29,7 @@ pub fn refine(args: RefineArgs) -> Result { }; let output = unsafe { abi::refine(input) }; - codec::decode(output.as_slice()).map_err(Into::into) + codec::decode(&output.to_vec()).map_err(Into::into) } /// Run the accumulate invocation @@ -40,12 +41,20 @@ pub fn accumulate(args: AccumulateArgs) -> Result { }; let output = unsafe { abi::accumulate(input) }; - codec::decode(output.as_slice()).map_err(Into::into) + codec::decode(&output.to_vec()).map_err(Into::into) } mod abi { + #[cfg(feature = "interp")] + pub use { + interp_accumulate as accumulate, interp_authorize as authorize, interp_refine as refine, + }; + + #[cfg(not(feature = "interp"))] + pub use {comp_accumulate as accumulate, comp_authorize as authorize, comp_refine as refine}; + #[repr(C)] - #[derive(Copy, Clone)] + #[derive(Copy, Clone, Debug)] pub struct Buffer { pub ptr: *const u8, pub len: usize, @@ -53,19 +62,42 @@ mod abi { impl Buffer { /// Get the buffer as a byte slice - pub fn as_slice(&self) -> &[u8] { - unsafe { std::slice::from_raw_parts(self.ptr, self.len) } + pub fn to_vec(&self) -> Vec { + let result = unsafe { core::slice::from_raw_parts(self.ptr, self.len).to_vec() }; + unsafe { + let layout = std::alloc::Layout::from_size_align(self.len, 1).unwrap(); + std::alloc::dealloc(self.ptr as *mut _, layout); + } + result } } unsafe extern "C" { - /// Run accumulate invocation - pub fn accumulate(args: Buffer) -> Buffer; + /// Initialize the logger + pub fn init_logger(ansi: bool, timer: bool); + + /// Run the authorize invocation + #[cfg(not(feature = "interp"))] + pub fn comp_authorize(args: Buffer) -> Buffer; /// Run the refine invocation - pub fn refine(args: Buffer) -> Buffer; + #[cfg(not(feature = "interp"))] + pub fn comp_refine(args: Buffer) -> Buffer; + + /// Run the accumulate invocation + #[cfg(not(feature = "interp"))] + pub fn comp_accumulate(args: Buffer) -> Buffer; /// Run the is_authorized invocation - pub fn authorize(args: Buffer) -> Buffer; + #[cfg(feature = "interp")] + pub fn interp_authorize(args: Buffer) -> Buffer; + + /// Run the refine invocation + #[cfg(feature = "interp")] + pub fn interp_refine(args: Buffer) -> Buffer; + + /// Run accumulate invocation + #[cfg(feature = "interp")] + pub fn interp_accumulate(args: Buffer) -> Buffer; } } diff --git a/crates/testing/Cargo.toml b/crates/testing/Cargo.toml new file mode 100644 index 0000000..c5a8bb2 --- /dev/null +++ b/crates/testing/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "jade-testing" +version.workspace = true +edition.workspace = true + +[dependencies] +anyhow.workspace = true +codec.workspace = true +cjam.workspace = true +etc.workspace = true +hex = { workspace = true, features = ["std"] } +spacevm = { workspace = true, features = ["interp"] } +serde.workspace = true +service = { workspace = true, features = ["blake2"] } +tracing.workspace = true +tracing-subscriber.workspace = true diff --git a/crates/testing/README.md b/crates/testing/README.md new file mode 100644 index 0000000..e69de29 diff --git a/crates/testing/src/account.rs b/crates/testing/src/account.rs new file mode 100644 index 0000000..09afb4b --- /dev/null +++ b/crates/testing/src/account.rs @@ -0,0 +1,63 @@ +//! Service account builder + +use crate::{Jam, key}; +use service::{OpaqueHash, ServiceId, service::ServiceAccount}; + +impl Jam { + /// Add a service account + pub fn add_account(&mut self, service: ServiceId, mut account: ServiceAccount) { + account.index = service; + self.chain.accounts.insert(service, account); + } + + /// Add a service account + pub fn add_service(&mut self, service: ServiceId, code: Vec) { + let hash = self.add_preimage(service, code); + self.set_code(service, hash); + self.mint(service, 1_000_000_000); + } + + /// Add a preimage to the service account + pub fn add_preimage(&mut self, service: ServiceId, preimage: Vec) -> OpaqueHash { + let account = self.chain.accounts.entry(service).or_default(); + let hash = service::blake2b(preimage.as_slice()); + let len = preimage.len() as u32; + account.index = service; + account.preimage.insert(hash, preimage); + account + .lookup + .insert((hash, len), vec![self.chain.best.slot]); + hash + } + + /// Get a storage of an account + pub fn get_storage( + &self, + service: ServiceId, + key: &[u8], + ) -> Option { + let account = self.chain.accounts.get(&service)?; + let vkey = key.to_vec(); + let key = key::storage(service, &codec::encode(&vkey).ok()?); + let encoded = account.storage.get(key.as_ref())?; + codec::decode(&mut &encoded[..]).ok() + } + + /// Set the code of the service account + pub fn set_code(&mut self, service: ServiceId, code: OpaqueHash) { + let account = self.chain.accounts.entry(service).or_default(); + account.info.code = code; + } + + /// Mint balance to a service account + pub fn mint(&mut self, service: ServiceId, amount: u64) { + let account = self.chain.accounts.entry(service).or_default(); + account.info.balance += amount; + } + + /// Add a service account + pub fn with_account(mut self, service: ServiceId, account: ServiceAccount) -> Self { + self.chain.accounts.insert(service, account); + self + } +} diff --git a/crates/testing/src/auth.rs b/crates/testing/src/auth.rs new file mode 100644 index 0000000..d0261d5 --- /dev/null +++ b/crates/testing/src/auth.rs @@ -0,0 +1,81 @@ +//! Authorization related stuffs + +use crate::Jam; +use service::{OpaqueHash, ServiceId, service::ServiceAccount}; + +/// Authorization related stuffs +#[derive(Default)] +pub struct Auth { + /// The authorization token + pub token: Vec, + + /// The authorization host + pub host: ServiceId, + + /// The auth code hash + pub code_hash: OpaqueHash, + + /// The authorizer config + pub config: Vec, +} + +impl Auth { + /// Set the authorization token + pub fn with_token(mut self, token: Vec) -> Self { + self.token = token; + self + } + + /// Set the authorizer + pub fn with_authorizer(mut self, service: ServiceId, code: OpaqueHash) -> Self { + self.host = service; + self.code_hash = code; + self + } + + /// Set the authorizer config + pub fn with_config(mut self, config: Vec) -> Self { + self.config = config; + self + } +} + +impl Jam { + /// Set the authorization + pub fn with_auth(mut self, service: ServiceId, code: Vec) -> Self { + let mut auth = ServiceAccount::default(); + auth.info.balance = 1000; + auth.info.creation = self.chain.best.slot; + + // register the service account + self.add_account(service, auth); + let hash = self.add_preimage(service, code); + + // set the code hash + if let Some(account) = self.chain.accounts.get_mut(&service) { + account.info.code = hash; + } + + self.auth.code_hash = hash; + self.auth.host = service; + self + } + + /// Set the authorization token + pub fn with_auth_token(mut self, token: Vec) -> Self { + self.auth.token = token; + self + } + + /// Set the authorizer config + pub fn with_auth_config(mut self, config: Vec) -> Self { + self.auth.config = config; + self + } + + /// Set the authorization + pub fn with_authorizer(mut self, auth: Auth) -> Self { + self.auth = auth; + self + } +} diff --git a/crates/testing/src/builder.rs b/crates/testing/src/builder.rs new file mode 100644 index 0000000..0c764a8 --- /dev/null +++ b/crates/testing/src/builder.rs @@ -0,0 +1,54 @@ +//! Work package builder implementation + +use crate::Jam; +use anyhow::Result; +use service::{ + ServiceId, + service::{WorkItem, WorkPackage}, +}; + +impl Jam { + /// Add a work item + /// + /// TODO: validate the work item + pub fn add_item(&mut self, item: WorkItem) { + self.items.push(item); + } + + /// Build a work package + pub fn build(&mut self) -> Result { + let package = WorkPackage { + authorization: self.auth.token.clone(), + auth_code_host: self.auth.host, + auth_code_hash: self.auth.code_hash, + config: self.auth.config.clone(), + context: self.chain.refine_context(), + items: self.items.drain(..).collect(), + }; + + Ok(package) + } + + /// pack a work item + pub fn pack(&mut self, service: ServiceId, payload: Vec) -> Result<()> { + let item = WorkItem { + service, + code_hash: self.chain.service(service)?, + payload, + refine_gas_limit: 1_000_000, + accumulate_gas_limit: 1_000_000, + import_segments: Default::default(), + extrinsic: Default::default(), + export_count: Default::default(), + }; + + self.add_item(item); + Ok(()) + } + + /// Send a work package + pub fn send(&mut self, service: ServiceId, payload: Vec) -> Result { + self.pack(service, payload)?; + self.build() + } +} diff --git a/crates/testing/src/chain.rs b/crates/testing/src/chain.rs new file mode 100644 index 0000000..b3a2300 --- /dev/null +++ b/crates/testing/src/chain.rs @@ -0,0 +1,58 @@ +//! Chain environment + +use anyhow::{Result, anyhow}; +use service::{ + EntropyBuffer, OpaqueHash, ServiceId, + service::{RefineContext, ServiceAccount}, +}; +use std::collections::BTreeMap; + +/// Head of a block +#[derive(Clone, Default)] +pub struct Head { + /// Hash of the block + pub hash: OpaqueHash, + + /// Slot of the block + pub slot: u32, +} + +/// Chain environment +#[derive(Clone, Default)] +pub struct Chain { + /// Best block + pub best: Head, + + /// Entropy buffer + pub entropy: EntropyBuffer, + + /// Finalized block + pub finalized: Head, + + /// Service accounts + pub accounts: BTreeMap, +} + +impl Chain { + /// Find a service code + pub fn service(&self, service: ServiceId) -> Result { + self.accounts + .get(&service) + .map(|account| account.info.code) + .ok_or_else(|| anyhow!("Service not found")) + } + + /// Get the refine context + /// + /// TODO: support prerequisites + pub fn refine_context(&self) -> RefineContext { + RefineContext { + anchor: self.best.hash, + state_root: Default::default(), + beefy_root: Default::default(), + lookup_anchor: self.finalized.hash, + lookup_anchor_slot: self.finalized.slot, + prerequisites: Default::default(), + } + } +} diff --git a/crates/testing/src/exec.rs b/crates/testing/src/exec.rs new file mode 100644 index 0000000..87d7bbc --- /dev/null +++ b/crates/testing/src/exec.rs @@ -0,0 +1,195 @@ +//! Execution API of JAM VM + +use crate::{Jam, key}; +use anyhow::Result; +use service::{ + ServiceId, + api::{ + AccumulateArgs, AccumulateState, Accumulated, AuthorizeArgs, Reason, RefineArgs, + ValidatorData, + }, + service::{ + Privileges, RefineLoad, ServiceAccount, WorkExecResult, WorkPackage, WorkResult, + result::Executed, + }, + vm::Operand, +}; +use std::collections::BTreeMap; + +/// The result of an execution +#[derive(Debug, Default)] +pub struct ExecutionInfo { + /// The refine gas used + pub refine_gas: u64, + + /// The accumulate gas used + pub accumulate_gas: u64, + + /// The account changes + pub accounts: BTreeMap, +} + +impl ExecutionInfo { + /// Create a new execution info + pub fn new(acc: Vec) -> Self { + let mut info = Self::default(); + for acc in acc.iter() { + info.accumulate_gas += acc.gas; + + // FIXME: need to merge account data + info.accounts = acc.context.accounts.clone(); + } + + info + } + + /// Get a storage of an account + pub fn get_storage( + &self, + service: ServiceId, + key: &[u8], + ) -> Option { + let account = self.accounts.get(&service)?; + let vkey = key.to_vec(); + let key = key::storage(service, &vkey); + let encoded = account.storage.get(key.as_ref())?; + codec::decode(&mut &encoded[..]).ok() + } +} + +impl Jam { + /// Execute a work item directly + /// + /// TODO: introduce better execution result + pub fn execute(&mut self, service: ServiceId, payload: Vec) -> Result { + let package = self.send(service, payload)?; + let result = self.refine(&package)?; + Ok(ExecutionInfo::new(self.accumulate(result)?)) + } + + /// Authorize the work package + #[tracing::instrument(name = "authorize", skip_all)] + pub fn authorize(&mut self, work: &WorkPackage, core_idx: u16) -> Result { + tracing::debug!( + "service={}, code=0x{}", + work.auth_code_host, + hex::encode(work.auth_code_hash) + ); + + spacevm::authorize(AuthorizeArgs { + package: work.clone(), + core_idx: core_idx, + accounts: self.chain.accounts.clone(), + timeslot: self.chain.best.slot, + }) + } + + /// Refine the work package + /// + /// NOTE: run refine for all work items + #[tracing::instrument(name = "refine", skip_all)] + pub fn refine(&mut self, work: &WorkPackage) -> Result> { + tracing::debug!("package: items={}", work.items.len()); + if work.items.is_empty() { + anyhow::bail!("no work items"); + } + + let mut result = Vec::new(); + for (index, item) in work.items.iter().enumerate() { + let refined = spacevm::refine(RefineArgs { + accounts: self.chain.accounts.clone(), + core: 0, + index: 0, + package: work.clone(), + export_offset: 0, + timeslot: self.chain.best.slot, + auth_output: Default::default(), + all_imports: Default::default(), + })?; + + if !matches!(refined.executed.exec, WorkExecResult::Ok(_)) { + return Err(anyhow::anyhow!( + "work item {index} refine failed: {:?}", + refined.executed.exec + )); + } + + result.push(WorkResult { + service_id: item.service, + code_hash: item.code_hash, + payload_hash: Default::default(), + accumulate_gas: Default::default(), + result: refined.executed.exec, + refine_load: RefineLoad { + gas_used: refined.executed.gas, + imports: Default::default(), + extrinsic_count: Default::default(), + extrinsic_size: Default::default(), + exports: Default::default(), + }, + }); + } + + Ok(result) + } + + /// Accumulate the work package + /// + /// 1. convert work package to work report + /// 2. run accumulate for all work items + /// 3. return the accumulated result + #[tracing::instrument(name = "accumulate", skip_all)] + pub fn accumulate(&mut self, results: Vec) -> Result> { + tracing::debug!("work: items={}", results.len()); + if results.is_empty() { + anyhow::bail!("no results"); + } + + let accounts = self.chain.accounts.clone(); + let mut state = AccumulateState { + accounts, + validators: [ValidatorData { + bandersnatch: Default::default(), + ed25519: Default::default(), + bls: [0; 144], + metadata: [0; 128], + }; 6], + authorization: Default::default(), + privileges: Privileges::default(), + entropy: Default::default(), + }; + + let service = results.first().expect("checked").service_id; + let operands = { + let mut operands = vec![]; + for work in results.iter() { + operands.push(Operand { + package: Default::default(), + exports_root: Default::default(), + authorizer_hash: Default::default(), + auth_output: Default::default(), + payload: work.payload_hash, + gas: work.accumulate_gas, + data: work.result.clone(), + }); + } + operands + }; + + // run accumulation + let accumulated = spacevm::accumulate(AccumulateArgs { + context: state, + timeslot: self.chain.best.slot, + service, + gas: 1_000_000_000, + operands, + })?; + + if !matches!(accumulated.reason, Reason::Halt) { + anyhow::bail!("accumulate failed: {:?}", accumulated.reason); + } + state = accumulated.context.clone(); + self.chain.accounts = state.accounts; + Ok(vec![accumulated]) + } +} diff --git a/crates/testing/src/extrinsic.rs b/crates/testing/src/extrinsic.rs new file mode 100644 index 0000000..5ebfc50 --- /dev/null +++ b/crates/testing/src/extrinsic.rs @@ -0,0 +1,15 @@ +//! extrinsic context + +use service::OpaqueHash; + +/// Extrinsic context +pub struct Extrinsic { + /// The extrinsic + pub extrinsic: Vec, + + /// The extrinsic hash + pub hash: OpaqueHash, + + /// The extrinsic length + pub len: u32, +} diff --git a/crates/testing/src/key.rs b/crates/testing/src/key.rs new file mode 100644 index 0000000..48ed05c --- /dev/null +++ b/crates/testing/src/key.rs @@ -0,0 +1,37 @@ +//! Storage keys + +use service::ServiceId; + +const STORAGE_PREFIX: [u8; 4] = [255, 255, 255, 255]; + +/// Compute the storage key +pub fn storage(service: ServiceId, key: &[u8]) -> [u8; 31] { + let mut hashed = STORAGE_PREFIX.to_vec(); + hashed.extend_from_slice(key); + let hash = service::blake2b(hashed.as_slice()); + + let mut key = [0u8; 31]; + let mut hashp = [0; 4]; + hashp.copy_from_slice(&hash[..4]); + key[..8].copy_from_slice(&prefix(service, &hashp)); + key[8..].copy_from_slice(&hash[4..27]); + key +} + +/// Generate a prefix for a storage +/// +/// service: [0, 2, 4, 6] +/// prefix: [1, 3, 5, 7] +pub fn prefix(service: u32, prefix: &[u8; 4]) -> [u8; 8] { + let mut key = [0; 8]; + service + .to_le_bytes() + .iter() + .zip(prefix.iter()) + .enumerate() + .for_each(|(i, (a, b))| { + key[i * 2] = *a; + key[(i + 1) * 2 - 1] = *b; + }); + key +} diff --git a/crates/testing/src/lib.rs b/crates/testing/src/lib.rs new file mode 100644 index 0000000..b91bf08 --- /dev/null +++ b/crates/testing/src/lib.rs @@ -0,0 +1,30 @@ +//! Testing library for the PVM + +pub use service::service::ServiceAccount as Account; +use service::service::WorkItem; +pub use {auth::Auth, chain::Chain, extrinsic::Extrinsic}; + +mod account; +mod auth; +mod builder; +mod chain; +mod exec; +mod extrinsic; +pub mod key; +pub mod util; + +/// JAM environment +#[derive(Default)] +pub struct Jam { + /// Chain environment + chain: Chain, + + /// authorization token + auth: Auth, + + /// work items + items: Vec, + + /// extrinsics + _extrinsic: Vec, +} diff --git a/crates/testing/src/util.rs b/crates/testing/src/util.rs new file mode 100644 index 0000000..04ce0c0 --- /dev/null +++ b/crates/testing/src/util.rs @@ -0,0 +1,49 @@ +//! Prelude for the PVM testing library + +use anyhow::{Context, Result}; +use cjam::ModuleType; +use tracing_subscriber::EnvFilter; + +/// Initialize the logger +pub fn init_logger() { + unsafe { + spacevm::init_logger(true, true); + } + + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); +} + +/// Load the service +pub fn load_service(package: &str) -> Result> { + let target = etc::find_up("target") + .expect("Failed to find target directory") + .join("jam") + .join(format!("{package}.jam")); + + std::fs::read(&target).context(format!("Failed to read {}", target.display())) +} + +/// Build the service +pub fn build_service(package: &str, module: Option, path: Option) { + cjam::util::build(package, module, path).expect("Failed to build service"); +} + +/// Load the current service +#[macro_export] +macro_rules! service { + () => {{ + $crate::util::init_logger(); + match $crate::util::load_service(env!("CARGO_PKG_NAME")) { + Ok(blob) => blob, + Err(e) => { + $crate::util::build_service( + env!("CARGO_PKG_NAME"), + Some(env!("CARGO_MANIFEST_DIR").to_string()), + ); + $crate::util::load_service(env!("CARGO_PKG_NAME")).expect("Failed to load service") + } + } + }}; +} diff --git a/services/nauth/Cargo.toml b/services/nauth/Cargo.toml index ba6f104..25b58c2 100644 --- a/services/nauth/Cargo.toml +++ b/services/nauth/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "null-authorizer" +name = "nauth" version.workspace = true edition.workspace = true authors.workspace = true @@ -8,10 +8,14 @@ license.workspace = true publish = true description = "A JAM authorizer which always authorizes" -[lib] -crate-type = ["cdylib"] +[features] +tiny = [] [dependencies] -jade.workspace = true -service.workspace = true -polkavm-derive.workspace = true +jade = { workspace = true, features = ["logging"] } + +[dev-dependencies] +jade = { workspace = true, features = ["testing"] } + +[build-dependencies] +cjam.workspace = true diff --git a/services/nauth/build.rs b/services/nauth/build.rs new file mode 100644 index 0000000..2cb3181 --- /dev/null +++ b/services/nauth/build.rs @@ -0,0 +1,10 @@ +//! Build the service + +fn main() { + cjam::util::build( + env!("CARGO_PKG_NAME"), + Some(cjam::ModuleType::Authorizer), + None, + ) + .expect("Failed to build service"); +} diff --git a/services/nauth/src/lib.rs b/services/nauth/src/lib.rs index ecba00f..4ed6736 100644 --- a/services/nauth/src/lib.rs +++ b/services/nauth/src/lib.rs @@ -1,8 +1,12 @@ #![cfg_attr(any(target_arch = "riscv32", target_arch = "riscv64"), no_std)] -use jade::prelude::{AuthConfig, AuthTrace, CoreIndex, WorkPackage}; +use jade::prelude::{AuthTrace, CoreIndex}; #[jade::is_authorized] -fn is_authorized(_param: AuthConfig, _package: WorkPackage, _core_index: CoreIndex) -> AuthTrace { +fn is_authorized(_core_index: CoreIndex) -> AuthTrace { Default::default() } + +/// The service blob for the null authorizer +#[cfg(not(any(target_arch = "riscv32", target_arch = "riscv64")))] +pub const SERVICE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/service.jam")); diff --git a/services/nauth/tests/main.rs b/services/nauth/tests/main.rs new file mode 100644 index 0000000..0a84763 --- /dev/null +++ b/services/nauth/tests/main.rs @@ -0,0 +1,19 @@ +//! Basic VM tests + +use jade::testing::{self, Jam}; +use nauth::SERVICE; + +const AUTHORIZER: u32 = 500; + +#[test] +fn test_nauth() { + testing::util::init_logger(); + + let mut jam = Jam::default().with_auth(AUTHORIZER, SERVICE.to_vec()); + let package = jam + .send(AUTHORIZER, vec![]) + .expect("failed to send work item"); + + let result = jam.authorize(&package, 0).expect("failed to authorize"); + assert!(result.is_ok(), "{:?}", result); +} diff --git a/services/stoken/Cargo.toml b/services/stoken/Cargo.toml index 83fcc71..229d933 100644 --- a/services/stoken/Cargo.toml +++ b/services/stoken/Cargo.toml @@ -7,9 +7,6 @@ license.workspace = true homepage.workspace = true repository.workspace = true -[lib] -crate-type = ["cdylib"] - [features] default = [] tiny = [] @@ -17,8 +14,12 @@ std = ["codec/std"] [dependencies] codec.workspace = true -# serde = { workspace = true, features = ["alloc"] } -jade.workspace = true +serde = { workspace = true, features = ["alloc"] } +jade = { workspace = true, features = ["logging"] } + +[dev-dependencies] +nauth.workspace = true +jade = { workspace = true, features = ["testing"] } [build-dependencies] cjam.workspace = true diff --git a/services/stoken/build.rs b/services/stoken/build.rs index 70490df..f3c07a2 100644 --- a/services/stoken/build.rs +++ b/services/stoken/build.rs @@ -1,10 +1,10 @@ //! Build the service fn main() { - cjam::util::build( + let _ = cjam::util::build( env!("CARGO_PKG_NAME"), Some(cjam::ModuleType::Service), None, ) - .expect("Failed to build service"); + .ok(); } diff --git a/services/stoken/src/lib.rs b/services/stoken/src/lib.rs index 06738d1..d7b31ea 100644 --- a/services/stoken/src/lib.rs +++ b/services/stoken/src/lib.rs @@ -1,15 +1,11 @@ #![cfg_attr(any(target_arch = "riscv32", target_arch = "riscv64"), no_std)] -use jade::prelude::{OpaqueHash, Vec}; +pub use {instruction::Instruction, storage::Holders}; -// pub mod instruction; +pub mod instruction; +mod service; +mod storage; -#[jade::accumulate] -fn accumulate(_slot: u32, _id: u32, _results: u32) -> Option { - unimplemented!() -} - -#[jade::refine] -fn refine(_core: u16, _index: u16, _id: u32, _payload: Vec, _package: OpaqueHash) -> Vec { - unimplemented!() -} +/// The service blob for the simple token service +#[cfg(not(any(target_arch = "riscv32", target_arch = "riscv64")))] +pub const SERVICE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/service.jam")); diff --git a/services/stoken/src/service.rs b/services/stoken/src/service.rs new file mode 100644 index 0000000..d1753f6 --- /dev/null +++ b/services/stoken/src/service.rs @@ -0,0 +1,65 @@ +//! Simple Token Service + +use crate::{Holders, Instruction}; +use jade::{ + error, info, + prelude::Vec, + service::{OpaqueHash, service::WorkExecResult, vm::Operand}, +}; + +#[jade::refine] +fn refine( + _core: u16, + _index: u16, + _id: u32, + payload: Vec, + _package_hash: OpaqueHash, +) -> Vec { + info!("entering refine logic ..."); + let Ok(instructions) = codec::decode::>(&mut payload.as_slice()) else { + error!( + target = "simple-token-service", + "failed to decode instructions" + ); + return Vec::new(); + }; + + info!( + target = "simple-token-service", + "instructions: {:?}", instructions + ); + info!("payload: {:?}", payload); + payload +} + +#[jade::accumulate] +fn accumulate(_now: u32, _id: u32, results: Vec) -> Option { + info!("accumulate items: {}", results.len()); + let mut holders = Holders::get(); + for raw_instructions in results.into_iter().filter_map(|x| { + if let WorkExecResult::Ok(data) = x.data { + Some(data) + } else { + None + } + }) { + let instructions = codec::decode::>(&mut &raw_instructions[..]).unwrap(); + jade::info!("instructions: {:?}", instructions); + for inst in instructions { + match inst { + Instruction::Mint { to, amount } => { + info!( + target = "simple-token-service", + "minting {} tokens to {}", amount, to + ); + holders.mint(to, amount); + } + Instruction::Transfer { from, to, amount } => { + holders.transfer(from, to, amount); + } + } + } + } + + None +} diff --git a/services/stoken/src/storage.rs b/services/stoken/src/storage.rs new file mode 100644 index 0000000..47786a3 --- /dev/null +++ b/services/stoken/src/storage.rs @@ -0,0 +1,63 @@ +//! Simple Token Service Storage + +use jade::{error, host::storage, prelude::BTreeMap}; +use serde::{Deserialize, Serialize}; + +/// A map of account IDs to their balances +#[derive(Serialize, Deserialize, Default)] +pub struct Holders { + inner: BTreeMap, +} + +impl Holders { + /// Get the holders map + pub fn get() -> Self { + storage::read(Self::key()).unwrap_or_default() + } + + /// Save the holders map + pub fn save(&self) { + if let Err(e) = storage::write(Self::key(), self) { + error!("failed to save holders: {:?}", e); + } + } + + /// Get the balance of the given account + pub fn balance(&self, account: u32) -> u64 { + self.inner.get(&account).copied().unwrap_or(0) + } + + /// Get the key of the holders map + pub const fn key() -> &'static [u8] { + b"holders" + } + + /// Transfer tokens from one account to another + pub fn transfer(&mut self, from: u32, to: u32, amount: u64) { + let from_balance = self.inner.get(&from).copied().unwrap_or(0); + let to_balance = self.inner.get(&to).copied().unwrap_or(0); + + if from_balance < amount { + error!("insufficient balance"); + return; + } + + self.inner.insert( + from, + from_balance.checked_sub(amount).expect("balance overflow"), + ); + self.inner.insert( + to, + to_balance.checked_add(amount).expect("balance overflow"), + ); + self.save(); + } + + /// Mint the given amount of tokens to the given account + pub fn mint(&mut self, to: u32, amount: u64) { + let balance = self.inner.get(&to).copied().unwrap_or(0); + self.inner + .insert(to, balance.checked_add(amount).expect("balance overflow")); + self.save(); + } +} diff --git a/services/stoken/tests/main.rs b/services/stoken/tests/main.rs new file mode 100644 index 0000000..f05d36a --- /dev/null +++ b/services/stoken/tests/main.rs @@ -0,0 +1,30 @@ +//! Basic VM tests + +use jade::testing::Jam; +use stoken::{Holders, Instruction, SERVICE}; + +const AUTHORIZER_ID: u32 = 500; +const SERVICE_ID: u32 = 501; +const ALICE: u32 = 0; + +#[test] +fn test_mint() { + jade::testing::util::init_logger(); + + // Set up JAM with authorization using the null authorizer service + let mut jam = Jam::default().with_auth(AUTHORIZER_ID, nauth::SERVICE.to_vec()); + jam.add_service(SERVICE_ID, SERVICE.to_vec()); + + // 1. send a mint instruction + let amount = 100; + let instr = vec![Instruction::Mint { to: ALICE, amount }]; + let info = jam + .execute(SERVICE_ID, codec::encode(&instr).unwrap()) + .expect("failed to execute work item"); + + // 2. check the balance + let holders: Holders = info + .get_storage(SERVICE_ID, Holders::key()) + .expect("failed to get holders"); + assert_eq!(holders.balance(ALICE), amount); +}