diff --git a/Cargo.lock b/Cargo.lock index c49f15e5..9ee0bef5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1030,7 +1030,7 @@ dependencies = [ [[package]] name = "cranelift-pvm" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "anyhow", "cranelift", @@ -2787,7 +2787,7 @@ dependencies = [ [[package]] name = "pvmc" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "anyhow", "bindgen", @@ -2798,6 +2798,7 @@ dependencies = [ "dirs", "hex", "libc", + "parking_lot", "postcard", "pvm", "rayon", @@ -3323,7 +3324,7 @@ dependencies = [ [[package]] name = "serde-jam" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "anyhow", "serde", @@ -3491,7 +3492,7 @@ dependencies = [ [[package]] name = "spacejam" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "anyhow", "async-trait", @@ -3528,7 +3529,7 @@ dependencies = [ [[package]] name = "spacejam-account" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "anyhow", "serde-jam", @@ -3539,7 +3540,7 @@ dependencies = [ [[package]] name = "spacejam-core" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "anyhow", "erased-serde", @@ -3585,16 +3586,15 @@ name = "spacejam-erasure" version = "0.0.0" dependencies = [ "anyhow", - "hex", "reed-solomon-simd", - "serde_json", + "serde-jam", "specjam", "tokio", ] [[package]] name = "spacejam-network" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "anyhow", "clap", @@ -3617,7 +3617,7 @@ dependencies = [ [[package]] name = "spacejam-offchain" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "anyhow", "async-trait", @@ -3633,7 +3633,7 @@ dependencies = [ [[package]] name = "spacejam-rpc" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "anyhow", "jsonrpsee", @@ -3645,7 +3645,7 @@ dependencies = [ [[package]] name = "spacejam-runtime" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "anyhow", "hex", @@ -3666,7 +3666,7 @@ dependencies = [ [[package]] name = "spacejam-service" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "anyhow", "blake2b_simd", @@ -3680,14 +3680,14 @@ dependencies = [ [[package]] name = "spacejam-spec" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "serde", ] [[package]] name = "spacejam-testing" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "anyhow", "hex", @@ -3717,7 +3717,7 @@ dependencies = [ [[package]] name = "spacejam-testnet" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "anyhow", "clap", @@ -3728,7 +3728,7 @@ dependencies = [ [[package]] name = "spacejam-worker" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "anyhow", "pvm", @@ -3744,7 +3744,7 @@ dependencies = [ [[package]] name = "spacejson" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "anyhow", "hex", @@ -3755,7 +3755,7 @@ dependencies = [ [[package]] name = "spacejson-derive" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "proc-macro2", "quote", @@ -3764,7 +3764,7 @@ dependencies = [ [[package]] name = "spacevm" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "anyhow", "hex", @@ -3778,7 +3778,7 @@ dependencies = [ [[package]] name = "spacevm-export" -version = "0.1.2-pre.9" +version = "0.1.2" dependencies = [ "pvm", "pvmc", diff --git a/Cargo.toml b/Cargo.toml index 8a562513..062a81fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ resolver = "1" [workspace.package] edition = "2024" -version = "0.1.2-pre.9" +version = "0.1.2" homepage = "https://spacejam.app" repository = "https://github.com/spacejamapp/jade" license = "GPL-3.0" @@ -78,7 +78,11 @@ ark-ed-on-bls12-381-bandersnatch = { version = "0.5", default-features = false } ark-ff = { version = "0.5", default-features = false } ark-std = { version = "0.5", default-features = false } ark-serialize = { version = "0.5", default-features = false } -ark-vrf = { version = "=0.2.1", features = ["bandersnatch", "ring", "parallel"] } +ark-vrf = { version = "=0.2.1", features = [ + "bandersnatch", + "ring", + "parallel", +] } async-trait = "0.1.89" bindgen = "0.72" blake2 = "0.10.6" @@ -116,10 +120,22 @@ quinn = "0.11.9" quote = "1.0.40" rand = "0.8.5" rayon = "1.11.0" -rcgen = { version = "0.13.2", default-features = false, features = ["crypto", "pem", "ring"] } +rcgen = { version = "0.13.2", default-features = false, features = [ + "crypto", + "pem", + "ring", +] } reed-solomon = { package = "reed-solomon-simd", version = "3.0.1" } -rustls = { version = "0.23.31", default-features = false, features = ["std", "logging", "tls12", "ring"] } -rustls-webpki = { version = "0.102.8", default-features = false, features = ["std", "ring"] } +rustls = { version = "0.23.31", default-features = false, features = [ + "std", + "logging", + "tls12", + "ring", +] } +rustls-webpki = { version = "0.102.8", default-features = false, features = [ + "std", + "ring", +] } serde = { version = "1.0.219", features = ["derive"], default-features = false } serde_json = { version = "1.0.143", default-features = false } syn = "2.0.106" @@ -152,9 +168,5 @@ uint = "0.10.0" w3f-bls = "0.1.9" x509-parser = "0.17.0" -[patch.crates-io] -# w3f-ring-proof = { git = "https://github.com/sunhuachuang/ring-proof" } -# ark-vrf = { git = "https://github.com/sunhuachuang/ark-vrf" } - [workspace.metadata.conta] packages = ["codec", "spacejson-derive", "spacejson", "spec", "service"] diff --git a/Makefile b/Makefile index 41d1054a..94e9ccd8 100644 --- a/Makefile +++ b/Makefile @@ -89,3 +89,20 @@ dpush: fpush: dpush docker push $(DOCKER_IMAGE):int docker push $(DOCKER_IMAGE):$(VERSION)-int + +# Sample-profile `spacejam fuzz tx` on a trace directory via samply. +# Needs sudo (kernel.perf_event_paranoid=3 blocks unprivileged perf_event_open). +# Override: +# TRACE_DIR=res/foo/trace OUT=/tmp/foo.json.gz SPACEVM=true make profile-trace +TRACE_DIR ?= res/l2a/trace +OUT ?= /tmp/spacejam-trace.json.gz + +.PHONY: profile-trace +profile-trace: + CARGO_PROFILE_RELEASE_DEBUG=line-tables-only \ + cargo build --release -p spacejam --features trace + sudo SPACEVM=$(SPACEVM) $$(command -v samply) record --save-only -o $(OUT) \ + ./target/release/spacejam fuzz tx $(TRACE_DIR) + @echo "" + @echo "profile saved: $(OUT)" + @echo "view: samply load $(OUT)" diff --git a/crates/codec/erasure/Cargo.toml b/crates/codec/erasure/Cargo.toml index 65fe2990..69452b74 100644 --- a/crates/codec/erasure/Cargo.toml +++ b/crates/codec/erasure/Cargo.toml @@ -14,6 +14,5 @@ tiny = [] full = [] [dev-dependencies] -hex.workspace = true -serde_json.workspace = true +codec.workspace = true specjam.workspace = true diff --git a/crates/codec/erasure/tests/consistency.rs b/crates/codec/erasure/tests/consistency.rs index 0326f38c..37f6de1c 100644 --- a/crates/codec/erasure/tests/consistency.rs +++ b/crates/codec/erasure/tests/consistency.rs @@ -1,4 +1,3 @@ -use anyhow::Result; use spacejam_erasure as erasure; use specjam::Registry; use std::path::PathBuf; @@ -21,14 +20,7 @@ async fn run_codec(test: &str) -> anyhow::Result<()> { specjam::Scale::Tiny }; let test = registry.erasure(scale)?.test(test)?; - let mut data = hex::decode(test.input.trim_start_matches("0x"))?; - let shards = serde_json::from_str::>(&test.output)? - .into_iter() - .map(|shard| { - hex::decode(shard.trim_start_matches("0x")) - .map_err(|e| anyhow::anyhow!("Failed to decode shard: {e}")) - }) - .collect::>>()?; + let (mut data, shards) = codec::decode::<(Vec, Vec>)>(test.input.expect_bin()?)?; let n = erasure::Config::default().original; let recovery_pairs = || -> Vec<(usize, Vec)> { diff --git a/crates/crypto/Cargo.toml b/crates/crypto/Cargo.toml index cdf04894..56b992b8 100644 --- a/crates/crypto/Cargo.toml +++ b/crates/crypto/Cargo.toml @@ -22,7 +22,8 @@ w3f-bls = { workspace = true, optional = true } tracing.workspace = true [features] -default = [] +default = ["tiny"] +tiny = [] full = [] blake2 = ["dep:blake2b_simd"] blake3 = ["dep:blake3"] diff --git a/crates/crypto/src/ed25519.rs b/crates/crypto/src/ed25519.rs index 3edeb5a5..068eccf4 100644 --- a/crates/crypto/src/ed25519.rs +++ b/crates/crypto/src/ed25519.rs @@ -5,8 +5,13 @@ pub use ed25519_zebra::{batch, Signature, SigningKey, VerificationKey, Verificat use rand::rngs::OsRng; use rayon::{iter::ParallelIterator, slice::ParallelSlice}; -/// Number of signatures per parallel chunk for batch verification. -const BATCH_PAR_CHUNK: usize = 32; +/// Below this batch size, verify the whole slice as one ed25519 batch with +/// no rayon involvement. +const SEQUENTIAL_BATCH_THRESHOLD: usize = 32; + +/// Chunk size when the batch is large enough to parallelize. Each chunk is +/// itself one ed25519 batch verification. +const PAR_CHUNK_SIZE: usize = 32; /// Ed25519 key pair. #[derive(Clone)] @@ -71,11 +76,11 @@ pub fn batch_verify(items: &[(&[u8], [u8; 64], [u8; 32])]) -> anyhow::Result<()> if items.is_empty() { return Ok(()); } - if items.len() <= BATCH_PAR_CHUNK { + if items.len() <= SEQUENTIAL_BATCH_THRESHOLD { return verify_batch(items); } - items.par_chunks(BATCH_PAR_CHUNK).try_for_each(verify_batch) + items.par_chunks(PAR_CHUNK_SIZE).try_for_each(verify_batch) } fn verify_batch(items: &[(&[u8], [u8; 64], [u8; 32])]) -> anyhow::Result<()> { diff --git a/crates/crypto/src/ring.rs b/crates/crypto/src/ring.rs index c9752db5..5a00f668 100644 --- a/crates/crypto/src/ring.rs +++ b/crates/crypto/src/ring.rs @@ -1,6 +1,12 @@ //! Bandersnatch ring related primitives #![cfg(feature = "vrf")] +#[cfg(not(any(feature = "tiny", feature = "full")))] +compile_error!( + "spacejam-crypto/ring: enable one of `tiny` or `full` — \ + otherwise the size-6 SRS is silently baked in (caused docker pre-11 BadTicketProof)" +); + use crate::vrf; use ark_ec::AffineRepr; use ark_serialize::CanonicalDeserialize; diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index 1eef3d20..4b4a4c5f 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -26,5 +26,5 @@ tokio.workspace = true tracing.workspace = true [features] -default = ["timing"] -timing = [] +tiny = ["score/tiny", "crypto/tiny"] +full = ["score/full", "crypto/full"] diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index fba8b4dc..171dca10 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -21,7 +21,6 @@ mod grandpa; mod hook; mod pool; pub mod storage; -pub mod timing; pub mod tx; mod validator; diff --git a/crates/runtime/src/timing/disabled.rs b/crates/runtime/src/timing/disabled.rs deleted file mode 100644 index 17d7d872..00000000 --- a/crates/runtime/src/timing/disabled.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Disabled timing utilities for SpaceJam -#![cfg(not(feature = "timing"))] - -use super::{DefaultProfiler, Pass, Profiler}; -use std::{any::Any, boxed::Box}; - -impl Profiler for DefaultProfiler { - fn start(&self, _pass: Pass) -> DefaultTimingToken { - Box::new(()) - } -} - -pub(crate) fn start(_pass: Pass) -> DefaultTimingToken { - Box::new(()) -} - -/// dummy timing token -pub struct DefaultTimingToken; diff --git a/crates/runtime/src/timing/enabled.rs b/crates/runtime/src/timing/enabled.rs deleted file mode 100644 index 94db1b76..00000000 --- a/crates/runtime/src/timing/enabled.rs +++ /dev/null @@ -1,160 +0,0 @@ -//! Enabled timing utilities for SpaceJam -#![cfg(feature = "timing")] - -use super::{DESCRIPTIONS, DefaultProfiler, NUM_PASSES, Pass, Profiler}; -use std::{ - boxed::Box, - cell::{Cell, RefCell}, - fmt, mem, - time::{Duration, Instant}, -}; - -// Information about passes in a single thread. -thread_local! { - static PROFILER: RefCell> = RefCell::new(Box::new(DefaultProfiler)); -} - -/// Setup the timing for the current thread. -pub fn setup() { - self::set_thread_profiler(Box::new(DefaultProfiler)); -} - -/// Set the profiler for the current thread. -/// -/// Returns the old profiler. -pub fn set_thread_profiler(new_profiler: Box) -> Box { - PROFILER.with(|profiler| std::mem::replace(&mut *profiler.borrow_mut(), new_profiler)) -} - -/// Start timing `pass` as a child of the currently running pass, if any. -/// -/// This function is called by the publicly exposed pass functions. -pub fn start(pass: Pass) -> Box { - PROFILER.with(|profiler| profiler.borrow().start(pass)) -} - -/// Accumulated timing information for a single pass. -#[derive(Default, Copy, Clone)] -struct PassTime { - /// Total time spent running this pass including children. - total: Duration, - - /// Time spent running in child passes. - child: Duration, -} - -/// Accumulated timing for all passes. -pub struct PassTimes { - pass: [PassTime; NUM_PASSES], -} - -impl PassTimes { - /// Add `other` to the timings of this `PassTimes`. - pub fn add(&mut self, other: &Self) { - for (a, b) in self.pass.iter_mut().zip(&other.pass[..]) { - a.total += b.total; - a.child += b.child; - } - } - - /// Returns the total amount of time taken by all the passes measured. - pub fn total(&self) -> Duration { - self.pass.iter().map(|p| p.total - p.child).sum() - } -} - -impl Default for PassTimes { - fn default() -> Self { - Self { - pass: [Default::default(); NUM_PASSES], - } - } -} - -impl fmt::Display for PassTimes { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "======== ======== ==================================")?; - writeln!(f, " Total Self Pass")?; - writeln!(f, "-------- -------- ----------------------------------")?; - for (time, desc) in self.pass.iter().zip(&DESCRIPTIONS[..]) { - // Omit passes that haven't run. - if time.total == Duration::default() { - continue; - } - - // Write a duration as secs.millis, trailing space. - fn fmtdur(mut dur: Duration, f: &mut fmt::Formatter) -> fmt::Result { - // Round to nearest ms by adding 500us. - dur += Duration::new(0, 500_000); - let ms = dur.subsec_millis(); - write!(f, "{:4}.{:03} ", dur.as_secs(), ms) - } - - fmtdur(time.total, f)?; - if let Some(s) = time.total.checked_sub(time.child) { - fmtdur(s, f)?; - } - writeln!(f, " {desc}")?; - } - writeln!(f, "======== ======== ==================================") - } -} - -// Information about passes in a single thread. -thread_local! { - static PASS_TIME: RefCell = RefCell::new(Default::default()); -} - -/// Take the current accumulated pass timings and reset the timings for the current thread. -/// -/// Only applies when [`DefaultProfiler`] is used. -pub fn take_current() -> PassTimes { - PASS_TIME.with(|rc| mem::take(&mut *rc.borrow_mut())) -} - -// Information about passes in a single thread. -thread_local! { - static CURRENT_PASS: Cell = const { Cell::new(Pass::None) }; -} - -impl Profiler for DefaultProfiler { - fn start(&self, pass: Pass) -> Box { - let prev = CURRENT_PASS.with(|p| p.replace(pass)); - // tracing::trace!("timing: Starting {pass}, (during {prev})"); - Box::new(DefaultTimingToken { - start: Instant::now(), - pass, - prev, - }) - } -} - -/// A timing token is responsible for timing the currently running pass. Timing starts when it -/// is created and ends when it is dropped. -pub struct DefaultTimingToken { - /// Start time for this pass. - start: Instant, - - // Pass being timed by this token. - pass: Pass, - - // The previously active pass which will be restored when this token is dropped. - prev: Pass, -} - -/// Dropping a timing token indicated the end of the pass. -impl Drop for DefaultTimingToken { - fn drop(&mut self) { - let duration = self.start.elapsed(); - // tracing::trace!("timing: Ending {}: {}ms", self.pass, duration.as_millis()); - let old_cur = CURRENT_PASS.with(|p| p.replace(self.prev)); - debug_assert_eq!(self.pass, old_cur, "Timing tokens dropped out of order"); - PASS_TIME.with(|rc| { - let mut table = rc.borrow_mut(); - table.pass[self.pass.idx()].total += duration; - if let Some(parent) = table.pass.get_mut(self.prev.idx()) { - parent.child += duration; - } - }) - } -} diff --git a/crates/runtime/src/timing/mod.rs b/crates/runtime/src/timing/mod.rs deleted file mode 100644 index d1a88d5b..00000000 --- a/crates/runtime/src/timing/mod.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! This module is modified from -//! for analyzing the execution time of spacejam runtime. -//! -//! Pass timing. -//! -//! This modules provides facilities for timing the execution of individual compilation passes. - -pub use prelude::*; -use std::{boxed::Box, fmt}; - -mod disabled; -mod enabled; -mod prelude { - #[cfg(not(feature = "timing"))] - pub(crate) use super::disabled::*; - #[cfg(feature = "timing")] - pub use super::enabled::*; -} - -// Each pass that can be timed is predefined with the `define_passes!` macro. Each pass has a -// snake_case name and a plain text description used when printing out the timing report. -// -// This macro defines: -// -// - A C-style enum containing all the pass names and a `None` variant. -// - A usize constant with the number of defined passes. -// - A const array of pass descriptions. -// - A public function per pass used to start the timing of that pass. -macro_rules! define_passes { - ($($pass:ident: $desc:expr,)+) => { - /// A single profiled pass. - #[expect(non_camel_case_types, reason = "macro-generated code")] - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - pub enum Pass { - $(#[doc=$desc] $pass,)+ - /// No active pass. - None, - } - - /// The amount of profiled passes. - pub const NUM_PASSES: usize = Pass::None as usize; - - const DESCRIPTIONS: [&str; NUM_PASSES] = [ $($desc),+ ]; - - $( - #[doc=$desc] - #[must_use] - pub fn $pass() -> Box { - start(Pass::$pass) - } - )+ - } -} - -// Pass definitions. -define_passes! { - entropy: "calculating entropy", - disputes: "validating disputes", - assignments: "updating availability assignments", - assurances: "validating assurances", - safrole: "rotate validators and update safrole", - accumulate: "accumulating available work reports", - guarantees: "validating guarantees", - preimages: "validating preimages", - commit: "committing the state", -} - -impl Pass { - fn idx(self) -> usize { - self as usize - } - - /// Description of the pass. - pub fn description(self) -> &'static str { - match DESCRIPTIONS.get(self.idx()) { - Some(s) => s, - None => "", - } - } -} - -impl fmt::Display for Pass { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(self.description()) - } -} - -/// A profiler. -pub trait Profiler { - /// Start a profiling pass. - fn start(&self, pass: Pass) -> Box; -} - -/// The default profiler. You can get the results using [`take_current`]. -pub struct DefaultProfiler; diff --git a/crates/runtime/src/tx/dispute/error.rs b/crates/runtime/src/tx/dispute/error.rs index 026a41b3..faf063fc 100644 --- a/crates/runtime/src/tx/dispute/error.rs +++ b/crates/runtime/src/tx/dispute/error.rs @@ -8,8 +8,6 @@ use spacejson::Json; #[serde(rename_all = "snake_case")] pub enum Error { AlreadyJudged, - BadAuditorKey, - BadGuarantorKey, BadVoteSplit, VerdictsNotSortedUnique, JudgementsNotSortedUnique, @@ -23,6 +21,8 @@ pub enum Error { BadJudgementAge, BadValidatorIndex, BadSignature, + BadGuarantorKey, + BadAuditorKey, VerdictNotExists, NotEnoughValidators, } diff --git a/crates/runtime/src/tx/executor.rs b/crates/runtime/src/tx/executor.rs index 8e6cc464..09913242 100644 --- a/crates/runtime/src/tx/executor.rs +++ b/crates/runtime/src/tx/executor.rs @@ -8,7 +8,6 @@ use crate::{ Storage, account::Accounts, storage::Commit, - timing, tx::{assurance, block, dispute, guarantee, preimage, ticket}, }; use account::Accounts as _; @@ -161,7 +160,6 @@ impl<'a, Vm: Pvm, S: Storage> Executor<'a, Vm, S> { .statistics .merge_reports(&self.available, &self.counts); - let _guard = timing::accumulate(); let available = std::mem::take(&mut self.available); let accounts = self.accounts.take().expect("accounts present"); @@ -303,7 +301,6 @@ impl<'a, Vm: Pvm, S: Storage> Executor<'a, Vm, S> { let (reported, reporters, mut batch) = if block.extrinsic.guarantees.is_empty() { (vec![], vec![], vec![]) } else { - let _guard = timing::guarantees(); guarantee::report( state, block.header.slot, @@ -345,7 +342,6 @@ impl<'a, Vm: Pvm, S: Storage> Executor<'a, Vm, S> { new_epoch: bool, safrole_in: Safrole, ) -> Result { - let _guard = timing::safrole(); let safrole = ticket::safrole( state.timeslot, block.header.slot, diff --git a/crates/runtime/src/tx/guarantee/error.rs b/crates/runtime/src/tx/guarantee/error.rs index 9c8f42a3..9138c95a 100644 --- a/crates/runtime/src/tx/guarantee/error.rs +++ b/crates/runtime/src/tx/guarantee/error.rs @@ -13,7 +13,6 @@ use spacejson::Json; #[serde(rename_all = "snake_case")] pub enum Error { BadCoreIndex, - BannedValidator, FutureReportSlot, ReportEpochBeforeLast, InsufficientGuarantees, @@ -34,10 +33,11 @@ pub enum Error { ServiceItemGasTooLow, TooManyDependencies, SegmentRootLookupInvalid, - WorkReportTooBig, BadSignature, - MissingWorkResults, + WorkReportTooBig, + BannedValidator, LookupAnchorNotRecent, + MissingWorkResults, } impl Json for Error { diff --git a/crates/runtime/src/tx/mod.rs b/crates/runtime/src/tx/mod.rs index 5cb610da..3aedb464 100644 --- a/crates/runtime/src/tx/mod.rs +++ b/crates/runtime/src/tx/mod.rs @@ -3,7 +3,6 @@ use crate::{ Storage, storage::{Column, Commit}, - timing, }; use anyhow::Result; pub use executor::Executor; @@ -26,7 +25,6 @@ pub fn transit( storage: Arc, ) -> Result>> { let diff = self::simulate::(&mut block, storage.clone())?; - let _guard = timing::commit(); storage.commit(Column::State, &diff)?; Ok(diff) } @@ -39,7 +37,6 @@ pub fn transit_with_state( storage: Arc, ) -> Result>> { let diff = self::simulate_with_state::(&mut block, state, storage.clone())?; - let _guard = timing::commit(); storage.commit(Column::State, &diff)?; Ok(diff) } diff --git a/crates/spacejam/Cargo.toml b/crates/spacejam/Cargo.toml index cdebbb8e..e19c1841 100644 --- a/crates/spacejam/Cargo.toml +++ b/crates/spacejam/Cargo.toml @@ -55,6 +55,6 @@ toml.workspace = true [features] default = ["tiny"] dhat = ["dep:dhat"] -tiny = ["score/tiny"] -full = ["score/full"] +tiny = ["score/tiny", "crypto/tiny", "runtime/tiny", "testing?/tiny"] +full = ["score/full", "crypto/full", "runtime/full", "testing?/full"] trace = ["dep:testing"] diff --git a/crates/spacejam/bin/spacejam.rs b/crates/spacejam/bin/spacejam.rs index 2832d4bd..d46c665e 100644 --- a/crates/spacejam/bin/spacejam.rs +++ b/crates/spacejam/bin/spacejam.rs @@ -4,32 +4,12 @@ use spacejam::cmd::App; #[tokio::main] async fn main() { - self::init_rayon(); - #[cfg(feature = "dhat")] dhat::init(); App::run().await; } -/// Cap the rayon global pool at 32 -fn init_rayon() { - let threads = std::env::var("RAYON_NUM_THREADS") - .ok() - .and_then(|s| s.parse::().ok()) - .filter(|n| *n > 0) - .unwrap_or_else(|| { - std::thread::available_parallelism() - .map(|n| n.get().min(32)) - .unwrap_or(8) - }); - - let _ = rayon::ThreadPoolBuilder::new() - .num_threads(threads) - .thread_name(|i| format!("rayon-{i}")) - .build_global(); -} - #[cfg(feature = "dhat")] mod dhat { use std::sync::Mutex; @@ -37,8 +17,8 @@ mod dhat { #[global_allocator] static ALLOC: dhat::Alloc = dhat::Alloc; + /// Initialize the dhat profiler. pub fn init() { - // Leak the profiler so the signal handler task can drop it on Ctrl-C. let profiler: &'static Mutex> = Box::leak(Box::new(Mutex::new(Some(dhat::Profiler::new_heap())))); diff --git a/crates/spacejam/src/cmd/fuzz.rs b/crates/spacejam/src/cmd/fuzz.rs index e05b4ce3..9711cedf 100644 --- a/crates/spacejam/src/cmd/fuzz.rs +++ b/crates/spacejam/src/cmd/fuzz.rs @@ -44,10 +44,10 @@ pub enum Fuzz { exact: Option, }, - /// Run trace test via the given trace file + /// Run trace test via the given trace file or directory #[cfg(feature = "trace")] Tx { - /// The path to the trace file + /// The path to the trace file or directory of `.bin`/`.json` traces test: PathBuf, }, } @@ -74,7 +74,13 @@ impl Fuzz { } } #[cfg(feature = "trace")] - Self::Tx { test } => fuzz::trace::test(test).await, + Self::Tx { test } => { + if test.is_dir() { + fuzz::trace::test_dir(test).await + } else { + fuzz::trace::test(test).await + } + } } } } diff --git a/crates/spacejam/src/fuzz/fuzzer.rs b/crates/spacejam/src/fuzz/fuzzer.rs index fd07f757..3cf53839 100644 --- a/crates/spacejam/src/fuzz/fuzzer.rs +++ b/crates/spacejam/src/fuzz/fuzzer.rs @@ -35,8 +35,11 @@ pub struct Fuzzer { impl Fuzzer { /// Run the fuzzer pub fn run(socket: &Path, entry: &Path, report: &Path) -> Result<()> { - let entry = Entry::new(Section::Trace(Trace::Any), None, entry).context(format!( - "Failed to parse traces folder, {entry:?}, should be the folder of traces, \n + let entry_str = entry + .to_str() + .ok_or_else(|| anyhow::anyhow!("non-utf8 trace path: {entry:?}"))?; + let entry = Entry::seq(entry_str).context(format!( + "Failed to parse traces folder, {entry_str:?}, should be the folder of traces, for example jam-test-vectors/traces/storage" ))?; @@ -67,7 +70,8 @@ impl Fuzzer { pub fn handle(&mut self, source: Entry) -> Result<()> { let base = source.base.clone(); for test in source { - if test.name.contains("genesis") { + let stem = test.name.rsplit('_').next().unwrap_or(&test.name); + if stem.parse::().is_err() { continue; } tracing::debug!("\tProcessing test: {}", test.name); @@ -84,7 +88,7 @@ impl Fuzzer { /// Handle a new connection pub fn handle_single(&mut self, source: &Entry) -> Result<()> { let test = source.get(0).context("No test found")?; - let input = traces::TestInput::from_json(&test.input)?; + let (input, _) = Self::decode(&test)?; self.init_state(&input, &test.name)?; self.init = true; self.import_block(test) @@ -92,8 +96,7 @@ impl Fuzzer { /// Import a block pub fn import_block(&mut self, test: Test) -> Result<()> { - let input = traces::TestInput::from_json(&test.input)?; - let output = traces::TestOutput::from_json(&test.output)?; + let (input, output) = Self::decode(&test)?; if !self.init { self.init_state(&input, &test.name)?; self.init = true; @@ -230,9 +233,7 @@ impl Fuzzer { }) .collect::>() } -} -impl Fuzzer { /// Run the fuzzer with traces pub fn conformance(socket: &Path, entry: &Path, report: &Path) -> Result<()> { if !entry.is_dir() { @@ -314,4 +315,9 @@ impl Fuzzer { fuzzer.handle_single(&entry) } + + /// Decode a trace test into its input/output pair. + fn decode(test: &Test) -> Result<(traces::TestInput, traces::TestOutput)> { + Ok(traces::decode(test)?) + } } diff --git a/crates/spacejam/src/fuzz/target.rs b/crates/spacejam/src/fuzz/target.rs index e30a69f4..6b329dc7 100644 --- a/crates/spacejam/src/fuzz/target.rs +++ b/crates/spacejam/src/fuzz/target.rs @@ -35,7 +35,6 @@ pub struct Target { impl Target { /// Create a new target pub fn new(stream: UnixStream, interp: bool) -> Self { - runtime::timing::setup(); Self { stream, chain: TestChain::default(), diff --git a/crates/spacejam/src/fuzz/trace.rs b/crates/spacejam/src/fuzz/trace.rs index 67f33dca..619c92f3 100644 --- a/crates/spacejam/src/fuzz/trace.rs +++ b/crates/spacejam/src/fuzz/trace.rs @@ -1,30 +1,70 @@ //! Tester for traces +use anyhow::Context; use serde_json::Value; -use std::{fs, path::Path}; -use testing::{Runner, Scale, Section, Test, Trace}; +use std::{fs, path::Path, time::Instant}; +use testing::{Entry, Payload, Runner, Scale, Section, Test, Trace}; -/// Test traces -pub async fn test(test: &Path) -> anyhow::Result<()> { - let json: Value = serde_json::from_slice(&fs::read(test)?)?; +/// Test a single trace file (`.json` or `.bin`). +pub async fn test(file: &Path) -> anyhow::Result<()> { + Runner::step(&parse(file)?).await +} + +/// Test every trace file in a directory (sorted by filename, `.bin` preferred over `.json`). +/// Skips files whose name stem doesn't parse as a step number (e.g. `report.bin`). +pub async fn test_dir(dir: &Path) -> anyhow::Result<()> { + let path = dir.to_str().context("non-utf8 trace dir")?; + let entry = Entry::seq(path)?; + let now = Instant::now(); + let mut processed = 0usize; + for test in entry { + let stem = test.name.rsplit('_').next().unwrap_or(&test.name); + if stem.parse::().is_err() { + continue; + } + Runner::step(&test).await?; + processed += 1; + } + tracing::info!("processed {processed} traces in {:?}", now.elapsed()); + Ok(()) +} + +/// Build a `Test` from a single trace file. +fn parse(file: &Path) -> anyhow::Result { + let name = file + .file_stem() + .context("invalid file name")? + .to_string_lossy() + .to_string(); + let section = Section::Trace(Trace::Any); + let scale = Some(Scale::Tiny); + + if file.extension().and_then(|s| s.to_str()) == Some("bin") { + return Ok(Test { + input: Payload::Bin(fs::read(file)?), + output: Payload::default(), + scale, + section, + name, + }); + } + + let json: Value = serde_json::from_slice(&fs::read(file)?)?; let input = serde_json::json!({ "block": json["block"], "pre_state": json["pre_state"], }) .to_string(); - let output = serde_json::json!({ "post_state": json["post_state"], }) .to_string(); - let test = Test { - input, - output, - scale: Some(Scale::Tiny), - section: Section::Trace(Trace::Any), - name: test.file_name().unwrap().to_string_lossy().to_string(), - }; - - Runner::step(&test).await + Ok(Test { + input: Payload::Json(input), + output: Payload::Json(output), + scale, + section, + name, + }) } diff --git a/crates/testing/build.rs b/crates/testing/build.rs index 84a7659b..c08822d7 100644 --- a/crates/testing/build.rs +++ b/crates/testing/build.rs @@ -13,6 +13,7 @@ use syn::{Ident, ItemFn, parse_quote}; const REPORTS: &str = "../../res/jam-conformance/fuzz-reports/0.7.2/traces"; const TRACES: &str = "../../res/jam-test-vectors/traces"; const REPORT: &str = "../../res/report"; +const SESSION: &str = "../../res/session/trace"; fn scale() -> Scale { if env::var("CARGO_FEATURE_FULL").is_ok() { @@ -26,6 +27,7 @@ fn main() -> Result<()> { println!("cargo:rerun-if-changed=../../res/jam-test-vectors"); println!("cargo:rerun-if-changed=../../res/jam-conformance/fuzz-reports/0.7.2/traces"); println!("cargo:rerun-if-changed=../../res/report"); + println!("cargo:rerun-if-changed=../../res/session/trace"); println!("cargo:rerun-if-changed=./build.rs"); let out_dir = PathBuf::from(env::var("OUT_DIR")?); let workspace = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?).join("../../"); @@ -157,16 +159,24 @@ fn build_all_seq_test(out: &Path, scale: Scale) -> Result<()> { let path = entry.path(); if path.is_dir() { let testset = path.to_str().expect("failed to get testset"); - items.push(build_seq_test(testset)?); + let name = Path::new(testset) + .file_name() + .and_then(|s| s.to_str()) + .expect("failed to get testset name"); + items.push(build_seq_test(testset, name)?); } } } } - // REPORT dir can hold either tiny- or full-spec traces; the spec is - // declared in its report.json metadata. Only include if it matches. + // report from fuzzer if Path::new(REPORT).is_dir() && util::report_scale()? == Some(scale) { - items.push(build_seq_test(REPORT)?); + items.push(build_seq_test(REPORT, "report")?); + } + + // session from fuzzer + if Path::new(SESSION).is_dir() && util::session_scale()? == Some(scale) { + items.push(build_seq_test(SESSION, "session")?); } fs::write(out, quote::quote!(#(#items)*).to_token_stream().to_string())?; @@ -174,9 +184,8 @@ fn build_all_seq_test(out: &Path, scale: Scale) -> Result<()> { } /// Builds the sequential tests -fn build_seq_test(entry: &str) -> Result { +fn build_seq_test(entry: &str, test_name: &str) -> Result { let fentry = Entry::seq(entry)?; - let test_name = Path::new(entry).file_name().unwrap().to_str().unwrap(); let mut tests = BTreeSet::::new(); for path in &fentry.files { let name = Entry::file_name(path)?; @@ -243,14 +252,13 @@ mod util { Ok(()) } - /// Read `res/report/report.json` and return the spec scale it declares, - /// or None if the file is missing or unrecognized. - pub fn report_scale() -> Result> { - let path = Path::new(REPORT).join("report.json"); + /// Read a fuzz `report.json` at the given path and return the spec scale + /// it declares, or None if the file is missing or unrecognized. + pub fn spec_from_report(path: &Path) -> Result> { if !path.exists() { return Ok(None); } - let content = fs::read_to_string(&path)?; + let content = fs::read_to_string(path)?; // Strip whitespace so we tolerate any pretty-printing of the JSON. let stripped: String = content.chars().filter(|c| !c.is_whitespace()).collect(); if stripped.contains(r#""jam_spec":{"full""#) { @@ -262,6 +270,20 @@ mod util { } } + /// Spec declared by `res/report/report.json`, if present. + pub fn report_scale() -> Result> { + spec_from_report(&Path::new(REPORT).join("report.json")) + } + + /// Spec declared by `res/session/report/report.json`, if present. + pub fn session_scale() -> Result> { + spec_from_report( + &Path::new(SESSION) + .with_file_name("report") + .join("report.json"), + ) + } + pub fn scale_constructor() -> proc_macro2::TokenStream { if env::var("CARGO_FEATURE_FULL").is_ok() { quote::quote!(specjam::Registry::with_scale( diff --git a/crates/testing/spec/src/lib.rs b/crates/testing/spec/src/lib.rs index 34f40140..29471a9c 100644 --- a/crates/testing/spec/src/lib.rs +++ b/crates/testing/spec/src/lib.rs @@ -19,9 +19,9 @@ pub struct Test { /// The name of the test vector pub name: String, /// The input of the test vectors - pub input: String, + pub input: Payload, /// The output of the test vectors - pub output: String, + pub output: Payload, } impl Test { @@ -31,6 +31,49 @@ impl Test { } } +/// A test-vector payload. +#[derive(Debug, Clone)] +pub enum Payload { + Json(String), + Bin(Vec), +} + +impl Payload { + /// Borrow as JSON text, or `None` if binary. + pub fn as_json(&self) -> Option<&str> { + match self { + Payload::Json(s) => Some(s), + Payload::Bin(_) => None, + } + } + + /// Borrow as raw bytes, or `None` if JSON. + pub fn as_bin(&self) -> Option<&[u8]> { + match self { + Payload::Bin(b) => Some(b), + Payload::Json(_) => None, + } + } + + /// Borrow as JSON text; error if binary. + pub fn expect_json(&self) -> anyhow::Result<&str> { + self.as_json() + .ok_or_else(|| anyhow::anyhow!("expected JSON payload, got binary")) + } + + /// Borrow as raw bytes; error if JSON. + pub fn expect_bin(&self) -> anyhow::Result<&[u8]> { + self.as_bin() + .ok_or_else(|| anyhow::anyhow!("expected binary payload, got JSON")) + } +} + +impl Default for Payload { + fn default() -> Self { + Self::Json(String::new()) + } +} + /// The scale of the test vectors #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Scale { diff --git a/crates/testing/spec/src/registry/entry.rs b/crates/testing/spec/src/registry/entry.rs index 61df0852..f7656237 100644 --- a/crates/testing/spec/src/registry/entry.rs +++ b/crates/testing/spec/src/registry/entry.rs @@ -1,6 +1,6 @@ //! Test vector registry entry -use crate::{Scale, Section, Test, Trace}; +use crate::{Payload, Scale, Section, Test, Trace}; use anyhow::Result; use serde_json::Value; use std::{ @@ -89,15 +89,26 @@ impl Entry { /// Build entry from the jam-conformance repo pub fn seq(repo: &str) -> Result { - let mut files = BTreeSet::new(); let base = PathBuf::from(repo); + let mut bins = BTreeSet::new(); + let mut jsons = BTreeSet::new(); for entry in fs::read_dir(Path::new(repo))? { let path = entry?.path(); - if path.is_file() && path.extension().unwrap_or_default() == "json" { - files.insert(path); + if !path.is_file() { + continue; + } + match path.extension().and_then(|s| s.to_str()) { + Some("bin") => { + bins.insert(path); + } + Some("json") => { + jsons.insert(path); + } + _ => {} } } + let files = if bins.is_empty() { jsons } else { bins }; Ok(Self { base, section: Section::Trace(Trace::Any), @@ -124,16 +135,20 @@ impl Entry { /// Get a test vector by name pub fn test(&self, name: &str) -> Result { - let path = self.base.join(format!("{name}.json")); - self.parse(&path) + let json_path = self.base.join(format!("{name}.json")); + if json_path.exists() { + return self.parse(&json_path); + } + let bin_path = self.base.join(format!("{name}.bin")); + self.parse(&bin_path) } /// Parse a test vector from a file - pub fn parse(&self, path: &PathBuf) -> Result { + pub fn parse(&self, path: &Path) -> Result { match self.section { - Section::Accumulate => self.parse_general(path), - Section::Assurances => self.parse_general(path), - Section::Authorizations => self.parse_general(path), + Section::Accumulate => self.parse_stf(path), + Section::Assurances => self.parse_stf(path), + Section::Authorizations => self.parse_stf(path), Section::Codec => self.parse_codec(path), Section::Erasure => self.parse_erasure(path), Section::Pvm => self.parse_pvm(path), @@ -151,20 +166,20 @@ impl Entry { } Ok(parsed) } - Section::Reports => self.parse_general(path), - Section::Statistics => self.parse_general(path), - Section::Safrole => self.parse_general(path), - Section::Disputes => self.parse_general(path), - Section::History => self.parse_general(path), - Section::Preimages => self.parse_general(path), + Section::Reports => self.parse_stf(path), + Section::Statistics => self.parse_stf(path), + Section::Safrole => self.parse_stf(path), + Section::Disputes => self.parse_stf(path), + Section::History => self.parse_stf(path), + Section::Preimages => self.parse_stf(path), } } /// Parse a codec test vector from a file - fn parse_codec(&self, path: &PathBuf) -> Result { + fn parse_codec(&self, path: &Path) -> Result { let name = Self::file_name(path)?; - let input = fs::read_to_string(path)?; - let output = hex::encode(fs::read(path.with_extension("bin"))?); + let input = Payload::Json(fs::read_to_string(path)?); + let output = Payload::Bin(fs::read(path.with_extension("bin"))?); Ok(Test { input, @@ -175,41 +190,26 @@ impl Entry { }) } - /// Parse an erasure test vector from a file - fn parse_erasure(&self, path: &PathBuf) -> Result { + /// Parse an erasure test vector. The `.bin` sibling carries + /// codec-encoded `(data, shards)` and is the source of truth. + fn parse_erasure(&self, path: &Path) -> Result { let name = Self::file_name(path)?; - let json: Value = serde_json::from_slice(&fs::read(path)?)?; - let input: String = serde_json::from_value(json["data"].clone())?; - let output: Vec = serde_json::from_value(json["shards"].clone())?; - Ok(Test { - input, - output: serde_json::to_string(&output)?, + input: Payload::Bin(fs::read(path.with_extension("bin"))?), + output: Payload::default(), name, scale: self.scale, section: self.section, }) } - /// Parse a scaled test vector from a file - fn parse_general(&self, path: &PathBuf) -> Result { + /// Parse an STF test vector from its `.bin` sibling, codec-encoded as + /// `(input, pre_state, output, post_state)`. + fn parse_stf(&self, path: &Path) -> Result { let name = Self::file_name(path)?; - let json: Value = serde_json::from_slice(&fs::read(path)?)?; - let input = serde_json::json!({ - "input": json["input"], - "pre_state": json["pre_state"], - }) - .to_string(); - - let output = serde_json::json!({ - "output": json["output"], - "post_state": json["post_state"], - }) - .to_string(); - Ok(Test { - input, - output, + input: Payload::Bin(fs::read(path.with_extension("bin"))?), + output: Payload::default(), name, scale: self.scale, section: self.section, @@ -217,7 +217,7 @@ impl Entry { } /// Parse a pvm test vector from a file - fn parse_pvm(&self, path: &PathBuf) -> Result { + fn parse_pvm(&self, path: &Path) -> Result { let name = Self::file_name(path)?; let json: Value = serde_json::from_slice(&fs::read(path)?)?; @@ -244,8 +244,8 @@ impl Entry { .to_string(); Ok(Test { - input, - output, + input: Payload::Json(input), + output: Payload::Json(output), scale: self.scale, section: self.section, name, @@ -253,8 +253,17 @@ impl Entry { } /// Parse a trace test vector from a file - fn parse_trace(&self, path: &PathBuf) -> Result { + fn parse_trace(&self, path: &Path) -> Result { let name = Self::file_name(path)?; + if path.extension().and_then(|s| s.to_str()) == Some("bin") { + return Ok(Test { + input: Payload::Bin(fs::read(path)?), + output: Payload::default(), + scale: self.scale, + section: self.section, + name, + }); + } let json: Value = serde_json::from_slice(&fs::read(path)?)?; let input = serde_json::json!({ "block": json["block"], @@ -268,15 +277,15 @@ impl Entry { .to_string(); Ok(Test { - input, - output, + input: Payload::Json(input), + output: Payload::Json(output), scale: self.scale, section: self.section, name, }) } - fn parse_trie(&self, path: &PathBuf) -> Result { + fn parse_trie(&self, path: &Path) -> Result { let name = Self::file_name(path)?; let json: Value = serde_json::from_slice(&fs::read(path)?)?; let vectors = json @@ -295,15 +304,15 @@ impl Entry { } Ok(Test { - input: serde_json::to_string(&input)?, - output: serde_json::to_string(&output)?, + input: Payload::Json(serde_json::to_string(&input)?), + output: Payload::Json(serde_json::to_string(&output)?), scale: self.scale, section: self.section, name, }) } - fn parse_shuffle(&self, path: &PathBuf) -> Result { + fn parse_shuffle(&self, path: &Path) -> Result { let name = Self::file_name(path)?; let json: Value = serde_json::from_slice(&fs::read(path)?)?; @@ -324,8 +333,8 @@ impl Entry { } Ok(Test { - input: serde_json::to_string(&input)?, - output: serde_json::to_string(&output)?, + input: Payload::Json(serde_json::to_string(&input)?), + output: Payload::Json(serde_json::to_string(&output)?), scale: self.scale, section: self.section, name, diff --git a/crates/testing/src/accumulate.rs b/crates/testing/src/accumulate.rs index 71742e9c..74a28399 100644 --- a/crates/testing/src/accumulate.rs +++ b/crates/testing/src/accumulate.rs @@ -2,26 +2,27 @@ use anyhow::Result; use runtime::tx; -use score::{ - OpaqueHash, TimeSlot, - service::{WorkReport, WorkReportJson}, -}; +use score::{OpaqueHash, TimeSlot, service::WorkReport}; use serde::{Deserialize, Serialize}; -use spacejson::{Json, ResultJson}; pub use types::*; include!(concat!(env!("OUT_DIR"), "/accumulate.rs")); /// Run the accumulate test pub async fn run(test: &specjam::Test) -> Result<()> { - let input = TestInput::from_json(&test.input)?; - let output = TestOutput::from_json(&test.output)?; + let (input, pre_state, out, post_state) = + codec::decode::<(Input, State, Result, State)>(test.input.expect_bin()?)?; + let input = TestInput { input, pre_state }; + let output = TestOutput { + post_state, + output: out, + }; let accounts = input.pre_state.accounts(); // run the accumulate function let use_compiler = std::env::var("SPACEVM").is_ok_and(|v| v == "true"); let accumulation = if use_compiler { - tx::guarantee::accumulate::( + tx::guarantee::accumulate::( input.input.slot, input.pre_state.slot, input.input.reports, @@ -88,64 +89,56 @@ pub async fn run(test: &specjam::Test) -> Result<()> { } /// Accumulate test -#[derive(Debug, Serialize, Deserialize, Json)] +#[derive(Debug, Serialize, Deserialize)] pub struct Test { /// The input - #[json(nested)] pub input: TestInput, /// The output - #[json(ResultJson)] pub output: Result, } /// Test input -#[derive(Debug, Serialize, Deserialize, Json)] +#[derive(Debug, Serialize, Deserialize)] pub struct Input { /// The time slot from the block header pub slot: TimeSlot, /// The reports - #[json(nested)] pub reports: Vec, } /// Test input -#[derive(Debug, Serialize, Deserialize, Json)] +#[derive(Debug, Serialize, Deserialize)] pub struct TestInput { /// The input - #[json(nested)] pub input: Input, /// The pre-state - #[json(nested)] pub pre_state: State, } /// Test output -#[derive(Debug, Serialize, Deserialize, Json)] +#[derive(Debug, Serialize, Deserialize)] pub struct TestOutput { /// The post-state - #[json(nested)] pub post_state: State, /// The output - #[json(ResultJson)] pub output: Result, } mod types { - use crate::reports::{ServiceItem, ServiceItemJson}; + use crate::reports::ServiceItem; use ::account::{Account, Accounts}; use runtime::Accumulation; use score::{ Entropy, Gas, ServiceId, TimeSlot, - service::{AccumulatedQueue, Privileges, ReadyQueue, ReadyReportJson, ServiceAccount}, + service::{AccumulatedQueue, Privileges, ReadyQueue, ServiceAccount}, state::account, - statistic::{ServiceActivityRecord, ServiceActivityRecordJson}, + statistic::ServiceActivityRecord, }; use serde::{Deserialize, Serialize}; - use spacejson::Json; use std::collections::BTreeMap; /// Convert the accumulation to the accounts @@ -163,33 +156,27 @@ mod types { } /// State for the accumulation - #[derive(Debug, Serialize, Deserialize, Json)] + #[derive(Debug, Serialize, Deserialize)] pub struct State { /// The time slot pub slot: TimeSlot, /// The current entropy - #[json(hex)] pub entropy: Entropy, /// The ready queue - #[json(Vec>)] pub ready_queue: ReadyQueue, /// The accumulated reports - #[json(Vec>)] pub accumulated: AccumulatedQueue, /// The privileges - #[json(nested)] pub privileges: PrivilegesWrap, /// The statistics - #[json(nested)] pub statistics: Vec, /// The accounts - #[json(nested)] pub accounts: Vec, } @@ -225,24 +212,22 @@ mod types { } /// Record wrapper - #[derive(Debug, Serialize, Deserialize, Json)] + #[derive(Debug, Serialize, Deserialize)] pub struct RecordWrap { /// The service id pub id: ServiceId, /// The record - #[json(nested)] pub record: ServiceActivityRecord, } /// Privileges wrapper - #[derive(Debug, Serialize, Deserialize, Json)] + #[derive(Debug, Serialize, Deserialize)] pub struct PrivilegesWrap { /// The bless service id pub bless: ServiceId, /// The assign service id - #[json(Vec)] pub assign: score::CoreAssignments, /// The designate service id @@ -252,7 +237,6 @@ mod types { pub register: ServiceId, /// The always accumulate service ids - #[json(nested)] pub always_acc: Vec, } @@ -289,7 +273,7 @@ mod types { } /// Always accumulate service id - #[derive(Debug, Serialize, Deserialize, Json)] + #[derive(Debug, Serialize, Deserialize)] pub struct AlwaysAccumulateMapItem { /// The service id pub service: ServiceId, diff --git a/crates/testing/src/assurances.rs b/crates/testing/src/assurances.rs index ba135271..66244725 100644 --- a/crates/testing/src/assurances.rs +++ b/crates/testing/src/assurances.rs @@ -5,15 +5,14 @@ use runtime::tx::{ assurance::{Error, Result}, }; use serde::{Deserialize, Serialize}; -use spacejson::{Json, ResultJson}; use types::*; include!(concat!(env!("OUT_DIR"), "/assurances.rs")); pub fn run(test: &specjam::Test) -> anyhow::Result<()> { - let mut input = TestInput::from_json(&test.input)?; - let TestOutput { output, post_state } = TestOutput::from_json(&test.output)?; - + let (input, pre_state, output, post_state) = + codec::decode::<(Input, State, Result, State)>(test.input.expect_bin()?)?; + let mut input = TestInput { input, pre_state }; assert_eq!(input.pre_state.curr_validators, post_state.curr_validators); // validate output @@ -54,71 +53,59 @@ pub fn run(test: &specjam::Test) -> anyhow::Result<()> { } /// Test input for assurances -#[derive(Debug, Json, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct TestInput { /// Input for the assurances - #[json(nested)] pub input: Input, /// Pre-state for the assurances - #[json(nested)] pub pre_state: State, } /// Test output for assurances -#[derive(Debug, Json, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct TestOutput { /// Output for the assurances - #[json(ResultJson)] pub output: Result, /// Post-state for the assurances - #[json(nested)] pub post_state: State, } mod types { use score::{ HeaderHash, TimeSlot, - extrinsic::{AssurancesExtrinsic, AvailAssuranceJson}, - safrole::{ValidatorDataJson, ValidatorsData}, - service::{ - AvailabilityAssignmentJson, AvailabilityAssignments, WorkReport, WorkReportJson, - }, + extrinsic::AssurancesExtrinsic, + safrole::ValidatorsData, + service::{AvailabilityAssignments, WorkReport}, }; use serde::{Deserialize, Serialize}; - use spacejson::Json; /// The state of the assurance module. - #[derive(Debug, Clone, Serialize, Deserialize, Json, PartialEq, Eq)] + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct State { /// (ρ†) rho dagger, which is the pending reports (ϱ) after that any /// work report judged as uncertain or invalid has been removed from it. /// On success, mutated to get [ϱ‡]. - #[json(Vec>)] pub avail_assignments: AvailabilityAssignments, /// (κ') posterior active validators. - #[json(Vec)] pub curr_validators: ValidatorsData, } /// The input to the assurance module. - #[derive(Debug, Clone, Serialize, Deserialize, Json)] + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Input { /// [E_A] Assurances extrinsic. - #[json(Vec)] pub assurances: AssurancesExtrinsic, /// [H_t] Block's timeslot. pub slot: TimeSlot, /// [H_p] Parent hash. - #[json(hex)] pub parent: HeaderHash, } /// The output of the assurance module. - #[derive(Debug, Clone, Serialize, Deserialize, Json, PartialEq, Eq)] + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Output { - #[json(Vec)] pub reported: Vec, } } diff --git a/crates/testing/src/authorizations.rs b/crates/testing/src/authorizations.rs index 8b7899fd..b8bb874b 100644 --- a/crates/testing/src/authorizations.rs +++ b/crates/testing/src/authorizations.rs @@ -3,16 +3,34 @@ use runtime::tx; use score::{CoreIndex, OpaqueHash, State, extrinsic::ReportGuarantee}; use serde::{Deserialize, Serialize}; -use spacejson::Json; -// FIXME: the ordering of the authorization pools could be wrong in the test cases, -// note that we follow the result in the tests of traces. -// -// include!(concat!(env!("OUT_DIR"), "/authorizations.rs")); +include!(concat!(env!("OUT_DIR"), "/authorizations.rs")); + +/// The authorizations STF `State` raw layout: `(auth-pools, auth-queues)`. +type RawState = ( + score::AuthorizationPools, + score::Array, { score::CORES_COUNT }>, +); + +impl From for TestState { + fn from((pools, queues): RawState) -> Self { + TestState { + auth_pools: pools.to_vec(), + auth_queues: queues.iter().map(|q| q.to_vec()).collect(), + } + } +} pub fn run(test: &specjam::Test) -> anyhow::Result<()> { - let input = TestInput::from_json(&test.input)?; - let output = TestOutput::from_json(&test.output)?; + let (auths, pre, (), post) = + codec::decode::<(Input, RawState, (), RawState)>(test.input.expect_bin()?)?; + let input = TestInput { + input: auths, + pre_state: pre.into(), + }; + let output = TestOutput { + post_state: post.into(), + }; let state: score::State = input.pre_state.clone().into(); let post: score::State = output.post_state.clone().into(); @@ -30,11 +48,9 @@ pub fn run(test: &specjam::Test) -> anyhow::Result<()> { } /// Test state for authorizations -#[derive(Serialize, Deserialize, Json, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct TestState { - #[json(Vec>)] pub auth_pools: Vec>, - #[json(Vec>)] pub auth_queues: Vec>, } @@ -57,11 +73,10 @@ impl From for State { } } -#[derive(Serialize, Deserialize, Json, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Authorization { - #[json(hex)] - pub auth_hash: OpaqueHash, pub core: CoreIndex, + pub auth_hash: OpaqueHash, } impl From for ReportGuarantee { @@ -73,25 +88,21 @@ impl From for ReportGuarantee { } } -#[derive(Serialize, Deserialize, Json, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Input { pub slot: u32, - #[json(nested)] pub auths: Vec, } /// Test input for authorizations -#[derive(Serialize, Deserialize, Json, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct TestInput { - #[json(nested)] pub input: Input, - #[json(nested)] pub pre_state: TestState, } /// Test output for authorizations -#[derive(Serialize, Deserialize, Json, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct TestOutput { - #[json(nested)] pub post_state: TestState, } diff --git a/crates/testing/src/codec.rs b/crates/testing/src/codec.rs index a1789381..166e151b 100644 --- a/crates/testing/src/codec.rs +++ b/crates/testing/src/codec.rs @@ -24,8 +24,8 @@ macro_rules! impl_codec_tests { }; let registry = Registry::with_scale(PathBuf::from("../../res/jam-test-vectors"), scale); let test = registry.entry("codec").unwrap().test(stringify!($name)).unwrap(); - let json = test.input.to_string(); - let data = hex::decode(&test.output)?; + let json = test.input.expect_json()?.to_string(); + let data = test.output.expect_bin()?.to_vec(); (json, data) }}; diff --git a/crates/testing/src/disputes.rs b/crates/testing/src/disputes.rs index 2b630cf5..93013241 100644 --- a/crates/testing/src/disputes.rs +++ b/crates/testing/src/disputes.rs @@ -4,20 +4,24 @@ use core::result::Result; use runtime::tx::{self, dispute::error::Error}; use score::{ Ed25519Public, TimeSlot, - extrinsic::dispute::{ - DisputesExtrinsic, DisputesExtrinsicJson, DisputesRecords, DisputesRecordsJson, - }, - safrole::{ValidatorDataJson, ValidatorsData}, - service::{AvailabilityAssignmentJson, AvailabilityAssignments}, + extrinsic::dispute::{DisputesExtrinsic, DisputesRecords}, + safrole::ValidatorsData, + service::AvailabilityAssignments, }; use serde::{Deserialize, Serialize}; -use spacejson::{Json, ResultJson}; include!(concat!(env!("OUT_DIR"), "/disputes.rs")); pub fn run(test: &specjam::Test) -> anyhow::Result<()> { - let mut input = TestInput::from_json(&test.input)?; - let output = TestOutput::from_json(&test.output)?; + let (input, pre_state, out, post_state) = + codec::decode::<(Disputes, State, Result, State)>( + test.input.expect_bin()?, + )?; + let mut input = TestInput { input, pre_state }; + let output = TestOutput { + output: out, + post_state, + }; let result = tx::dispute::disputes( input.pre_state.tau, &input.pre_state.kappa, @@ -46,51 +50,41 @@ pub fn run(test: &specjam::Test) -> anyhow::Result<()> { Ok(()) } -#[derive(Debug, Json, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Disputes { - #[json(nested)] pub disputes: DisputesExtrinsic, } /// Test input. -#[derive(Debug, Json, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct TestInput { - #[json(nested)] pub input: Disputes, - #[json(nested)] pub pre_state: State, } /// Test output. -#[derive(Debug, Json, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct TestOutput { - #[json(ResultJson)] pub output: Result, - #[json(nested)] pub post_state: State, } -#[derive(Json, Serialize, Deserialize, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct OffendersMark { /// [H_o] Offenders marker - #[json(Vec)] pub offenders_mark: Vec, } -#[derive(Debug, PartialEq, Eq, Json, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] pub struct State { /// [ψ] Disputes verdicts and offenders - #[json(nested)] pub psi: DisputesRecords, /// [ρ] Availability cores assignments - #[json(Vec>)] pub rho: AvailabilityAssignments, /// [τ] Timeslot pub tau: TimeSlot, /// [κ] Validators active in the current epoch - #[json(Vec)] pub kappa: ValidatorsData, /// [λ] Validators active in the previous epoch - #[json(Vec)] pub lambda: ValidatorsData, } diff --git a/crates/testing/src/erasure.rs b/crates/testing/src/erasure.rs index e5c634b3..6d90c852 100644 --- a/crates/testing/src/erasure.rs +++ b/crates/testing/src/erasure.rs @@ -5,11 +5,7 @@ use anyhow::Result; include!(concat!(env!("OUT_DIR"), "/erasure.rs")); pub async fn run(test: &specjam::Test) -> anyhow::Result<()> { - let mut data = hex::decode(test.input.trim_start_matches("0x"))?; - let shards = serde_json::from_str::>(&test.output)? - .into_iter() - .map(|s| hex::decode(s.trim_start_matches("0x")).map_err(Into::into)) - .collect::>>()?; + let (mut data, shards) = codec::decode::<(Vec, Vec>)>(test.input.expect_bin()?)?; // test encoding let edata = data.clone(); diff --git a/crates/testing/src/history.rs b/crates/testing/src/history.rs index eb2f2fd0..365c24d5 100644 --- a/crates/testing/src/history.rs +++ b/crates/testing/src/history.rs @@ -1,21 +1,20 @@ //! history test -use anyhow::Result; use runtime::tx::block::history; -use score::{ - OpaqueHash, - block::{History, HistoryJson}, - service::{ReportedWorkPackage, ReportedWorkPackageJson}, -}; +use score::{OpaqueHash, block::History, service::ReportedWorkPackage}; use serde::{Deserialize, Serialize}; -use spacejson::Json; include!(concat!(env!("OUT_DIR"), "/history.rs")); /// Run the history test pub fn run(test: &specjam::Test) -> anyhow::Result<()> { - let input = TestInput::from_json(&test.input)?; - let output = TestOutput::from_json(&test.output)?; + let (input, pre_state, (), post_state) = + codec::decode::<(Input, State, (), State)>(test.input.expect_bin()?)?; + let input = TestInput { input, pre_state }; + let output = TestOutput { + output: None, + post_state, + }; let mut history = input.pre_state.beta.clone(); if let Some(last) = history.history.last_mut() { last.state_root = input.input.parent_state_root; @@ -30,38 +29,29 @@ pub fn run(test: &specjam::Test) -> anyhow::Result<()> { Ok(()) } -#[derive(Serialize, Deserialize, Json, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct Input { - #[json(hex)] pub header_hash: OpaqueHash, - #[json(hex)] pub parent_state_root: OpaqueHash, - #[json(hex)] pub accumulate_root: OpaqueHash, - #[json(nested)] pub work_packages: Vec, } -#[derive(Serialize, Deserialize, Json, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct State { - #[json(nested)] pub beta: History, } /// Test input for history -#[derive(Serialize, Deserialize, Json, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct TestInput { - #[json(nested)] pub input: Input, - #[json(nested)] pub pre_state: State, } /// Test output for history -#[derive(Serialize, Deserialize, Json, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct TestOutput { - #[json(Option<()>)] pub output: Option<()>, - #[json(nested)] pub post_state: State, } diff --git a/crates/testing/src/lib.rs b/crates/testing/src/lib.rs index 564f2636..fe7f07ee 100644 --- a/crates/testing/src/lib.rs +++ b/crates/testing/src/lib.rs @@ -2,7 +2,7 @@ #![allow(unused_imports)] -pub use specjam::{Entry, Scale, Section, Test, Trace}; +pub use specjam::{Entry, Payload, Scale, Section, Test, Trace}; pub mod accumulate; pub mod assurances; diff --git a/crates/testing/src/preimage.rs b/crates/testing/src/preimage.rs index f143b46c..4b5f5190 100644 --- a/crates/testing/src/preimage.rs +++ b/crates/testing/src/preimage.rs @@ -4,17 +4,21 @@ use account::{Account, Accounts}; use runtime::tx; use score::service::ServiceAccount; use serde::{Deserialize, Serialize}; -use spacejson::Json; use std::collections::BTreeMap; use types::*; -// FIXME: skipping the preimage tests since it's currently outdated. -// -// include!(concat!(env!("OUT_DIR"), "/preimages.rs")); +include!(concat!(env!("OUT_DIR"), "/preimages.rs")); pub fn run(test: &specjam::Test) -> anyhow::Result<()> { - let input = TestInput::from_json(&test.input)?; - let output = TestOutput::from_json(&test.output)?; + let (preimages, pre, _output, post) = + codec::decode::<(Input, RawState, Result<(), u8>, RawState)>(test.input.expect_bin()?)?; + let input = TestInput { + input: preimages, + pre_state: pre.into(), + }; + let output = TestOutput { + post_state: post.into(), + }; // Validate post state let mut accounts = to_accounts(input.pre_state.accounts.clone()); @@ -37,28 +41,22 @@ pub fn run(test: &specjam::Test) -> anyhow::Result<()> { } /// Test input. -#[derive(Debug, Serialize, Deserialize, Json)] +#[derive(Debug, Serialize, Deserialize)] pub struct TestInput { - #[json(nested)] pub input: Input, - #[json(nested)] pub pre_state: TState, } /// Test output. -#[derive(Debug, Serialize, Deserialize, Json)] +#[derive(Debug, Serialize, Deserialize)] pub struct TestOutput { - #[json(nested)] pub post_state: TState, } -#[derive(Debug, Serialize, Deserialize, Json)] +#[derive(Debug, Serialize, Deserialize)] pub struct Test { - #[json(nested)] pub input: Input, - #[json(nested)] pub pre_state: TState, - #[json(nested)] pub post_state: TState, } @@ -73,58 +71,45 @@ pub fn to_accounts(accs: Vec) -> BTreeMap { // TODO: clean types later mod types { - use score::{ - OpaqueHash, - extrinsic::{Preimage, PreimageJson}, - service::ServiceAccount, - }; + use score::{OpaqueHash, extrinsic::Preimage, service::ServiceAccount}; use serde::{Deserialize, Serialize}; - use spacejson::Json; - #[derive(Debug, Serialize, Deserialize, Json, PartialEq, Eq)] + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Input { - #[json(nested)] pub preimages: Vec, pub slot: u32, } - #[derive(Debug, Serialize, Deserialize, Json, Clone, PartialEq, Eq)] + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct Account { /// Account ID pub id: u32, /// Account info - #[json(nested)] pub data: AccountInfo, } - #[derive(Debug, Serialize, Deserialize, Json, Clone, PartialEq, Eq)] + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct TPreimage { - #[json(hex)] pub hash: OpaqueHash, - #[json(hex)] pub blob: Vec, } - #[derive(Debug, Serialize, Deserialize, Json, Clone, PartialEq, Eq)] + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct HistoryKey { - #[json(hex)] pub hash: OpaqueHash, pub length: u32, } - #[derive(Debug, Serialize, Deserialize, Json, Clone, PartialEq, Eq)] + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct History { - #[json(nested)] pub key: HistoryKey, pub value: Vec, } - #[derive(Debug, Serialize, Deserialize, Json, Clone, PartialEq, Eq)] + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct AccountInfo { - #[json(nested)] pub preimages: Vec, - #[json(nested)] pub lookup_meta: Vec, } @@ -136,20 +121,30 @@ mod types { } for lookup in info.lookup_meta { - let mut slots = [0; 3]; - slots[..lookup.value.len()].copy_from_slice(&lookup.value); account .lookup - .insert((lookup.key.hash, lookup.key.length), slots.to_vec()); + .insert((lookup.key.hash, lookup.key.length), lookup.value); } account } } - #[derive(Debug, Serialize, Deserialize, Json, Clone, PartialEq, Eq)] + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct TState { - #[json(nested)] pub accounts: Vec, } + + /// The preimages STF `State` raw layout: `(accounts, services-statistics)`. + /// The statistics records aren't asserted on, so they're discarded. + pub type RawState = ( + Vec, + Vec<(score::ServiceId, score::statistic::ServiceActivityRecord)>, + ); + + impl From for TState { + fn from((accounts, _stats): RawState) -> Self { + TState { accounts } + } + } } diff --git a/crates/testing/src/pvmc.rs b/crates/testing/src/pvmc.rs index e3e2ce63..efdfd07c 100644 --- a/crates/testing/src/pvmc.rs +++ b/crates/testing/src/pvmc.rs @@ -26,8 +26,8 @@ impl Runner { .with_target(false) .try_init(); - let input: TestInput = serde_json::from_str(&test.input)?; - let output: TestOutput = serde_json::from_str(&test.output)?; + let input: TestInput = serde_json::from_str(test.input.expect_json()?)?; + let output: TestOutput = serde_json::from_str(test.output.expect_json()?)?; let mut initial_registers = [0u64; pvm::REGISTER_COUNT]; initial_registers.copy_from_slice(&input.initial_regs); @@ -71,10 +71,11 @@ impl Runner { meta: Default::default(), })?; + let hash = crypto::blake3(&input.program); let mut ctx = pvm::Context { registers: initial_registers, gas: input.initial_gas as i64, - memory: spacevm::Memory::new(&memory).expect("failed to create memory"), + memory: spacevm::Memory::new(hash, &memory).expect("failed to create memory"), ctx: &mut (), }; diff --git a/crates/testing/src/pvmi.rs b/crates/testing/src/pvmi.rs index e547fe76..cc19fda7 100644 --- a/crates/testing/src/pvmi.rs +++ b/crates/testing/src/pvmi.rs @@ -9,8 +9,8 @@ include!(concat!(env!("OUT_DIR"), "/pvm.rs")); /// Run the PVM test pub fn run(test: &specjam::Test) -> anyhow::Result<()> { - let input: TestInput = serde_json::from_str(&test.input)?; - let output: TestOutput = serde_json::from_str(&test.output)?; + let input: TestInput = serde_json::from_str(test.input.expect_json()?)?; + let output: TestOutput = serde_json::from_str(test.output.expect_json()?)?; let mut registers = [0; 13]; registers.copy_from_slice(&input.initial_regs); diff --git a/crates/testing/src/reports.rs b/crates/testing/src/reports.rs index dc1cca2e..ea4219be 100644 --- a/crates/testing/src/reports.rs +++ b/crates/testing/src/reports.rs @@ -5,21 +5,20 @@ use runtime::tx::{ guarantee::error::{Error, Result}, }; use score::{ - Block, Ed25519Public, OpaqueHash, TimeSlot, - block::{History, HistoryJson}, - extrinsic::{GuaranteesExtrinsic, ReportGuaranteeJson}, - service::{ReportedWorkPackage, ReportedWorkPackageJson}, + Block, Ed25519Public, OpaqueHash, TimeSlot, extrinsic::GuaranteesExtrinsic, + service::ReportedWorkPackage, }; use serde::{Deserialize, Serialize}; -use spacejson::{Json, ResultJson}; pub use types::*; include!(concat!(env!("OUT_DIR"), "/reports.rs")); /// Run the reports test pub fn run(test: &specjam::Test) -> anyhow::Result<()> { - let TestInput { input, pre_state } = TestInput::from_json(&test.input)?; - let TestOutput { output, post_state } = TestOutput::from_json(&test.output)?; + let (input, pre, output, post) = + codec::decode::<(Input, RawState, Result, RawState)>(test.input.expect_bin()?)?; + let pre_state: State = pre.into(); + let post_state: State = post.into(); assert_eq!(pre_state.curr_validators, post_state.curr_validators); assert_eq!(pre_state.prev_validators, post_state.prev_validators); @@ -57,30 +56,24 @@ pub fn run(test: &specjam::Test) -> anyhow::Result<()> { } /// Test input. -#[derive(Debug, Clone, Serialize, Deserialize, Json)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct TestInput { - #[json(nested)] pub input: Input, - #[json(nested)] pub pre_state: State, } /// Test output. -#[derive(Debug, Serialize, Deserialize, Json, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct TestOutput { - #[json(ResultJson)] pub output: Result, - #[json(nested)] pub post_state: State, } /// Input of the reporting module. -#[derive(Debug, Clone, Serialize, Deserialize, Json)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Input { - pub slot: TimeSlot, - #[json(Vec)] pub guarantees: GuaranteesExtrinsic, - #[json(Vec)] + pub slot: TimeSlot, pub known_packages: Vec, } @@ -94,66 +87,108 @@ impl From for Block { } /// Output of the reporting module. -#[derive(Debug, Clone, Serialize, Deserialize, Json, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Output { - #[json(nested)] pub reported: Vec, - #[json(Vec)] pub reporters: Vec, } mod types { use score::{ - CORES_COUNT, Ed25519Public, EntropyBuffer, OpaqueHash, ServiceId, - block::{History, HistoryJson}, - safrole::{ValidatorDataJson, ValidatorsData}, - service::{ - AvailabilityAssignmentJson, AvailabilityAssignments, ServiceAccount, ServiceInfo, - ServiceInfoJson, - }, + Ed25519Public, EntropyBuffer, OpaqueHash, ServiceId, + block::History, + safrole::ValidatorsData, + service::{AvailabilityAssignments, ServiceAccount, ServiceInfo}, }; use serde::{Deserialize, Serialize}; - use spacejson::Json; use std::collections::BTreeMap; - #[derive(Debug, Clone, Serialize, Deserialize, Json, PartialEq, Eq)] + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct State { /// (ρ‡) Intermediate pending reports after that any work report judged as /// uncertain or invalid has been removed from it (ϱ†), and the availability /// assurances are processed. Mutated to ϱ'. - #[json(Vec>)] pub avail_assignments: AvailabilityAssignments, /// (κ') Posterior active validators. - #[json(Vec)] pub curr_validators: ValidatorsData, /// (λ') Posterior previous validators. - #[json(Vec)] pub prev_validators: ValidatorsData, /// (η') Posterior entropy buffer. - #[json(Vec)] pub entropy: EntropyBuffer, /// (ψ'_o) Posterior offenders. - #[json(Vec)] pub offenders: Vec, /// (β) Recent blocks. - #[json(nested)] pub recent_blocks: History, /// (α') Authorization pools. - #[json(Vec>)] pub auth_pools: score::AuthorizationPools, /// (δ) Encoded services dictionary. Refer to T(σ) in Appendix D. - #[json(nested)] #[serde(alias = "accounts")] pub services: Vec, } + /// The reports STF `State` raw layout (reports.asn): the eight modelled + /// fields, then a minimal `(id, ServiceInfo)` accounts list, then the + /// `cores-statistics` and `services-statistics` records. + pub type RawState = ( + AvailabilityAssignments, + ValidatorsData, + ValidatorsData, + EntropyBuffer, + Vec, + History, + score::AuthorizationPools, + Vec<(ServiceId, ServiceInfo)>, + score::statistic::CoreStats, + Vec<(ServiceId, score::statistic::ServiceActivityRecord)>, + ); + + /// Build from the raw tuple, dropping the statistics records the reports + /// test doesn't assert on. + impl From for State { + fn from(w: RawState) -> Self { + let ( + avail_assignments, + curr_validators, + prev_validators, + entropy, + offenders, + recent_blocks, + auth_pools, + accounts, + _cores, + _services, + ) = w; + State { + avail_assignments, + curr_validators, + prev_validators, + entropy, + offenders, + recent_blocks, + auth_pools, + services: accounts + .into_iter() + .map(|(id, service)| ServiceItem { + id, + data: ServiceAccountData { + service, + storage: vec![], + preimages: vec![], + preimage_requests: vec![], + }, + }) + .collect(), + } + } + } + impl State { /// Apply the state to the score state fn apply(self, state: &mut score::State) { @@ -207,39 +242,34 @@ mod types { } /// Represents a service item. - #[derive(Debug, Clone, Serialize, Deserialize, Json, PartialEq, Eq)] + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ServiceItem { /// The id of the service item pub id: ServiceId, /// The info of the service item - #[json(nested)] pub data: ServiceAccountData, } /// Represents the service account data. - #[derive(Debug, Clone, Serialize, Deserialize, Json, PartialEq, Eq)] + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ServiceAccountData { /// The service account state - #[json(nested)] pub service: ServiceInfo, + /// The storage + #[serde(default)] + pub storage: Vec, + /// (a_p) The preimages #[serde(default)] - #[json(nested)] #[serde(alias = "preimage_blobs")] pub preimages: Vec, /// The preimage status #[serde(default)] - #[json(nested)] #[serde(alias = "preimage_requests")] pub preimage_requests: Vec, - - /// The storage - #[serde(default)] - #[json(nested)] - pub storage: Vec, } impl From<&ServiceAccount> for ServiceAccountData { @@ -307,22 +337,19 @@ mod types { } /// Represents a service preimage. - #[derive(Debug, Clone, Serialize, Deserialize, Json, PartialEq, Eq)] + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ServicePreimage { /// The hash of the preimage - #[json(hex)] pub hash: OpaqueHash, /// The blob of the preimage - #[json(hex)] pub blob: Vec, } /// Represents a service preimage. - #[derive(Debug, Clone, Serialize, Deserialize, Json, PartialEq, Eq)] + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ServicePreimageRequest { /// The key of the preimage - #[json(nested)] pub key: ServicePreimageRequestKey, /// The status of the preimage @@ -330,10 +357,9 @@ mod types { } /// Represents a service preimage. - #[derive(Debug, Clone, Serialize, Deserialize, Json, PartialEq, Eq)] + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ServicePreimageRequestKey { /// The hash of the preimage - #[json(hex)] pub hash: OpaqueHash, /// The length of the preimage @@ -341,14 +367,12 @@ mod types { } /// Represents a service storage. - #[derive(Debug, Clone, Serialize, Deserialize, Json, PartialEq, Eq)] + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ServiceStorage { /// The key of the storage - #[json(hex)] pub key: Vec, /// The value of the storage - #[json(hex)] pub value: Vec, } } diff --git a/crates/testing/src/safrole.rs b/crates/testing/src/safrole.rs index 95b5a1f5..4da510c2 100644 --- a/crates/testing/src/safrole.rs +++ b/crates/testing/src/safrole.rs @@ -3,22 +3,25 @@ use runtime::tx::{self, ticket::Error}; use score::{ BandersnatchRingCommitment, Ed25519Public, EntropyBuffer, OpaqueHash, - block::header::{EpochMark, EpochMarkJson, TicketsMark}, - extrinsic::ticket::{ - TicketBodyJson, TicketEnvelopeJson, TicketsAccumulator, TicketsExtrinsic, TicketsOrKeys, - TicketsOrKeysJson, - }, - safrole::{Safrole, ValidatorDataJson, Validators, ValidatorsData}, + block::header::{EpochMark, TicketsMark}, + extrinsic::ticket::{TicketsAccumulator, TicketsExtrinsic, TicketsOrKeys}, + safrole::{Safrole, Validators, ValidatorsData}, }; use serde::{Deserialize, Serialize}; -use spacejson::{Json, ResultJson}; include!(concat!(env!("OUT_DIR"), "/safrole.rs")); /// Run the safrole test pub fn run(test: &specjam::Test) -> anyhow::Result<()> { - let mut input = TestInput::from_json(&test.input)?; - let output = TestOutput::from_json(&test.output)?; + let (input, pre_state, out, post_state) = + codec::decode::<(Input, State, std::result::Result, State)>( + test.input.expect_bin()?, + )?; + let mut input = TestInput { input, pre_state }; + let output = TestOutput { + output: out, + post_state, + }; let result = input.pre_state.enact(&input.input); assert_eq!(result, output.output); @@ -31,78 +34,61 @@ pub fn run(test: &specjam::Test) -> anyhow::Result<()> { } /// Test input. -#[derive(Deserialize, Serialize, Json, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct Input { pub slot: u32, - #[json(hex)] pub entropy: OpaqueHash, - #[json(Vec)] pub extrinsic: TicketsExtrinsic, } /// Test input. -#[derive(Deserialize, Serialize, Json, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct TestInput { - #[json(nested)] pub input: Input, - #[json(nested)] pub pre_state: State, } /// Test output. -#[derive(Deserialize, Serialize, Json, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct TestOutput { - #[json(ResultJson)] pub output: std::result::Result, - #[json(nested)] pub post_state: State, } /// Represents the Output marks -#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq, Json)] +#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq)] pub struct Markers { /// New epoch marker - #[json(nested)] pub epoch_mark: Option, /// New tickets marker - #[json(Option>)] pub tickets_mark: Option, } /// Represents the State structure. -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Json, Clone)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct State { /// Most recent block's timeslot. pub tau: u32, /// Entropy accumulator and epochal randomness. /// /// graypaper reference: 6.21 - #[json(Vec)] pub eta: EntropyBuffer, /// Previous epoch's validators - #[json(Vec)] pub lambda: ValidatorsData, /// Current epoch's validators - #[json(Vec)] pub kappa: ValidatorsData, - /// Validators to be drawn from next - #[json(Vec)] - pub iota: ValidatorsData, /// Next epoch's validators - #[json(Vec)] pub gamma_k: ValidatorsData, + /// Validators to be drawn from next + pub iota: ValidatorsData, + /// Sealing-key contest ticket accumulator + pub gamma_a: TicketsAccumulator, + /// Sealing-key series of the current epoch + pub gamma_s: TicketsOrKeys, /// Bandersnatch ring commitment #[serde(with = "codec::bytes")] - #[json(hex)] pub gamma_z: BandersnatchRingCommitment, - /// Sealing-key series of the current epoch - #[json(nested)] - pub gamma_s: TicketsOrKeys, - /// Sealing-key contest ticket accumulator - #[json(Vec)] - pub gamma_a: TicketsAccumulator, /// Offenders - #[json(Vec)] pub post_offenders: Vec, } diff --git a/crates/testing/src/seq.rs b/crates/testing/src/seq.rs index 022db3a7..427e5417 100644 --- a/crates/testing/src/seq.rs +++ b/crates/testing/src/seq.rs @@ -3,9 +3,7 @@ use crate::traces::{self, TestInput, TestOutput}; use anyhow::Result; use runtime::tx::block::TestChain; -use score::{OpaqueHash, TimeSlot}; use specjam::Test; -use std::{collections::BTreeMap, sync::Arc, time::Instant}; include!(concat!(env!("OUT_DIR"), "/traces_seq.rs")); @@ -17,8 +15,7 @@ pub struct Processor { impl Processor { /// Process a test pub async fn process(&mut self, test: Test) -> Result<()> { - let input = TestInput::from_json(&test.input)?; - let output = TestOutput::from_json(&test.output)?; + let (input, output) = decode_trace(&test)?; if !self.chain.initialized() { self.chain.init(input.pre_state.keyvals())?; } @@ -26,7 +23,7 @@ impl Processor { let block = input.block.clone(); let data = self.chain.prepare(&input.block); let is_ok = if std::env::var("SPACEVM").is_ok_and(|v| v == "true") { - traces::run_single::(data.clone(), input, output).await? + traces::run_single::(data.clone(), input, output).await? } else { traces::run_single::(data.clone(), input, output).await? }; @@ -55,3 +52,7 @@ impl Default for Processor { } } } + +fn decode_trace(test: &Test) -> Result<(TestInput, TestOutput)> { + traces::decode(test) +} diff --git a/crates/testing/src/shuffle.rs b/crates/testing/src/shuffle.rs index e6b7e2d8..1c7542bd 100644 --- a/crates/testing/src/shuffle.rs +++ b/crates/testing/src/shuffle.rs @@ -20,8 +20,8 @@ fn tests() -> anyhow::Result<()> { // grab `shuffle_tests.json` let registry = Registry::new(PathBuf::from("../../res/jam-test-vectors")); let test = registry.shuffle()?.get(0)?; - let input: Vec = serde_json::from_str(&test.input)?; - let output: Vec = serde_json::from_str(&test.output)?; + let input: Vec = serde_json::from_str(test.input.expect_json()?)?; + let output: Vec = serde_json::from_str(test.output.expect_json()?)?; for (source, target) in input.into_iter().zip(output) { let mut input = vec![0; source.input as usize]; diff --git a/crates/testing/src/statistics.rs b/crates/testing/src/statistics.rs index 0aae34cb..e3b87603 100644 --- a/crates/testing/src/statistics.rs +++ b/crates/testing/src/statistics.rs @@ -1,71 +1,55 @@ //! Statistics tests use score::{ - TimeSlot, ValidatorIndex, - extrinsic::{Extrinsic, ExtrinsicJson}, - safrole::{ValidatorDataJson, ValidatorsData}, - statistic::{Statistics, StatisticsJson}, + EPOCH_LENGTH, TimeSlot, ValidatorIndex, + extrinsic::Extrinsic, + safrole::{ValidatorIter, ValidatorsData}, + statistic::{Statistics, ValidatorStats}, }; use serde::{Deserialize, Serialize}; -use spacejson::Json; -// FIXME: skipping the statistics tests since it's currently outdated. -// -// include!(concat!(env!("OUT_DIR"), "/statistics.rs")); +include!(concat!(env!("OUT_DIR"), "/statistics.rs")); + +/// The statistics STF `State` raw layout: +/// `(vals-curr-stats, vals-last-stats, slot, curr-validators)`. +type RawState = (ValidatorStats, ValidatorStats, TimeSlot, ValidatorsData); /// Run the statistics test -pub fn run(_test: &specjam::Test) -> anyhow::Result<()> { - // let input = TestInput::from_json(&test.input)?; - // let output = TestOutput::from_json(&test.output)?; - // - // // validate - // let state = input.pre_state.statistics.update( - // input.input.slot, - // input.input.author_index, - // &input.input.extrinsic, - // ); - // assert_eq!(state, output.post_state.statistics); +pub fn run(test: &specjam::Test) -> anyhow::Result<()> { + // The statistics STF `Output` is ASN.1 `NULL` (zero raw bytes). + let (input, pre, (), post) = + codec::decode::<(Input, RawState, (), RawState)>(test.input.expect_bin()?)?; + let new_epoch = input.slot / EPOCH_LENGTH > pre.2 / EPOCH_LENGTH; + let validators = pre.3; + let mut stats = Statistics { + vals_current: pre.0, + vals_last: pre.1, + ..Default::default() + }; + stats.update(new_epoch, input.author_index, &input.extrinsic)?; + + // Per-validator guarantee credit comes from the reporters (the validators + // that signed each guarantee), mirroring the executor's `merge_reporters`. + let reporters: Vec<_> = input + .extrinsic + .guarantees + .iter() + .flat_map(|g| { + g.signatures + .iter() + .map(|s| validators[s.validator_index as usize].ed25519) + }) + .collect(); + stats.merge_reporters(&reporters, &validators.ed25519())?; + + assert_eq!(stats.vals_current, post.0); + assert_eq!(stats.vals_last, post.1); Ok(()) } -#[derive(Debug, PartialEq, Eq, Json, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Input { pub slot: TimeSlot, pub author_index: ValidatorIndex, - #[json(nested)] pub extrinsic: Extrinsic, } - -/// Test input. -#[derive(Deserialize, Serialize, Json, Debug)] -pub struct TestInput { - #[json(nested)] - pub input: Input, - #[json(nested)] - pub pre_state: State, -} - -/// Test output. -#[derive(Deserialize, Serialize, Json, Debug)] -pub struct TestOutput { - #[json(nested)] - pub post_state: State, -} - -/// State of the stats -/// -/// NOTE: this should be moved to storage in the future -#[derive(Json, Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] -pub struct State { - /// The statistics state - #[json(nested)] - #[serde(flatten)] - pub statistics: Statistics, - - /// The current time slot - pub slot: TimeSlot, - - /// The current validators - #[json(Vec)] - pub curr_validators: ValidatorsData, -} diff --git a/crates/testing/src/traces.rs b/crates/testing/src/traces.rs index 18d017aa..6b3a4632 100644 --- a/crates/testing/src/traces.rs +++ b/crates/testing/src/traces.rs @@ -7,7 +7,7 @@ use runtime::{ tx::{self, block::header, ticket::lazy}, }; use score::{ - EntropyBuffer, OpaqueHash, + EntropyBuffer, OpaqueHash, TrieKey, block::{Block, BlockInfo, BlockJson, Header, History, Mmr}, safrole::{Safrole, ValidatorIter, ValidatorsData}, service::{AccumulatedQueue, Privileges, ReadyQueue, ServiceInfo}, @@ -52,12 +52,13 @@ mod storage_light { } pub async fn run(test: &specjam::Test) -> anyhow::Result { - if test.input.len() == 31 { + if let Some(s) = test.input.as_json() + && s.len() == 31 + { return Ok(false); } let memdb = Arc::new(MemoryDb::default()); - let input = TestInput::from_json(&test.input)?; - let output = TestOutput::from_json(&test.output)?; + let (input, output) = decode(test)?; for keyval in input.pre_state.keyvals.clone() { memdb .state_set(keyval.key, keyval.value) @@ -66,7 +67,7 @@ pub async fn run(test: &specjam::Test) -> anyhow::Result { let use_compiler = std::env::var("SPACEVM").is_ok_and(|v| v == "true"); if use_compiler { - self::run_single::(memdb, input, output).await + self::run_single::(memdb, input, output).await } else { self::run_single::(memdb, input, output).await } @@ -262,3 +263,76 @@ pub struct KeyValue { #[json(hex)] pub value: Vec, } + +/// Decode a `Test` into its input/output. +pub fn decode(test: &specjam::Test) -> anyhow::Result<(TestInput, TestOutput)> { + if let Some(bytes) = test.input.as_bin() { + return from_bin(bytes); + } + let input = test + .input + .as_json() + .ok_or_else(|| anyhow::anyhow!("trace input: expected JSON or bin"))?; + let output = test + .output + .as_json() + .ok_or_else(|| anyhow::anyhow!("trace output: expected JSON"))?; + Ok((TestInput::from_json(input)?, TestOutput::from_json(output)?)) +} + +/// Decode a binary trace into test input/output. +pub fn from_bin(bytes: &[u8]) -> anyhow::Result<(TestInput, TestOutput)> { + let bin: bin::BinTrace = codec::decode(bytes)?; + Ok(( + TestInput { + pre_state: bin.pre_state.into_state(), + block: bin.block, + }, + TestOutput { + post_state: bin.post_state.into_state(), + }, + )) +} + +mod bin { + use super::*; + + /// A binary trace + #[derive(Deserialize)] + pub struct BinTrace { + pub pre_state: BinState, + pub block: Block, + pub post_state: BinState, + } + + /// A binary state + #[derive(Deserialize)] + pub struct BinState { + pub state_root: OpaqueHash, + pub keyvals: Vec, + } + + /// A binary key-value + #[derive(Deserialize)] + pub struct BinKeyValue { + pub key: TrieKey, + pub value: Vec, + } + + /// Convert a binary state into a state + impl BinState { + pub fn into_state(self) -> State { + State { + state_root: self.state_root, + keyvals: self + .keyvals + .into_iter() + .map(|kv| KeyValue { + key: kv.key.to_vec(), + value: kv.value, + }) + .collect(), + } + } + } +} diff --git a/crates/testing/src/trie.rs b/crates/testing/src/trie.rs index c47e8b00..b8825168 100644 --- a/crates/testing/src/trie.rs +++ b/crates/testing/src/trie.rs @@ -22,9 +22,11 @@ fn trie() { let test = registry.trie().unwrap().get(0).unwrap(); let tests: Vec = - serde_json::from_str(&test.input).expect("failed to parse trie test input"); + serde_json::from_str(test.input.expect_json().expect("trie input must be JSON")) + .expect("failed to parse trie test input"); let output: Vec = - serde_json::from_str(&test.output).expect("failed to parse trie test output"); + serde_json::from_str(test.output.expect_json().expect("trie output must be JSON")) + .expect("failed to parse trie test output"); for (input, output) in tests .into_iter() diff --git a/crates/vm/compiler/Cargo.toml b/crates/vm/compiler/Cargo.toml index df07fc89..7b06b881 100644 --- a/crates/vm/compiler/Cargo.toml +++ b/crates/vm/compiler/Cargo.toml @@ -19,6 +19,7 @@ cranelift = { workspace = true, features = [ "object", ] } cranelift-codegen = { workspace = true, features = ["incremental-cache"] } +parking_lot.workspace = true tracing.workspace = true libc.workspace = true diff --git a/crates/vm/compiler/src/compiler.rs b/crates/vm/compiler/src/compiler.rs index 3199d915..c4a89538 100644 --- a/crates/vm/compiler/src/compiler.rs +++ b/crates/vm/compiler/src/compiler.rs @@ -14,7 +14,7 @@ impl Pvm for Compiler {} impl Invocation for Compiler { fn invoke2( mut ctx: X, - _hash: OpaqueHash, + hash: OpaqueHash, code: Vec, args: Vec, gas: Gas, @@ -26,7 +26,7 @@ impl Invocation for Compiler { let mut context = pvm::Context { registers: program.registers, gas: gas as i64, - memory: Memory::new(&program.memory).expect("failed to create memory"), + memory: Memory::new(hash, &program.memory).expect("failed to create memory"), ctx: &mut ctx, }; diff --git a/crates/vm/compiler/src/memory/image.rs b/crates/vm/compiler/src/memory/image.rs new file mode 100644 index 00000000..3ea31175 --- /dev/null +++ b/crates/vm/compiler/src/memory/image.rs @@ -0,0 +1,95 @@ +//! Per-program memfd image cache for guest memory init data. +//! +//! ref: https://github.com/bytecodealliance/wasmtime/pull/3697 +#![cfg(target_os = "linux")] + +use anyhow::Result; +use pvm::{Cache, score::OpaqueHash}; +use std::{ + io, + os::fd::{AsRawFd, FromRawFd, OwnedFd}, + sync::{Arc, LazyLock}, +}; + +/// Process-wide cache of per-program memory images. +static IMAGES: LazyLock> = LazyLock::new(Cache::default); + +/// Memfd-backed snapshot of a program's RO + RW initial bytes. +pub struct MemoryImage { + memfd: OwnedFd, + /// The size of the read-only data. + pub ro_size: usize, + /// The size of the read-write data. + pub rw_size: usize, +} + +impl MemoryImage { + /// Look up the image for `hash`, building from `pmemory` on miss. + pub fn get_or_build(hash: OpaqueHash, pmemory: &pvm::Memory) -> Result> { + if let Some(image) = IMAGES.get(&hash) { + return Ok(image); + } + let image = Arc::new(Self::build(pmemory)?); + IMAGES.put(hash, image.clone()); + Ok(image) + } + + /// Get the raw file descriptor of the memory image. + pub fn raw_fd(&self) -> libc::c_int { + self.memfd.as_raw_fd() + } + + fn build(pmemory: &pvm::Memory) -> Result { + let ro = pmemory.ro_data()?; + let rw = pmemory.rw_data()?; + let page = pvm::PAGE_SIZE as usize; + let ro_size = ro.len().next_multiple_of(page); + let rw_size = rw.len().next_multiple_of(page); + let total = ro_size + rw_size; + let name = c"spacejam-pvm-image"; + let raw = unsafe { libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC) }; + if raw < 0 { + anyhow::bail!("memfd_create failed: {}", io::Error::last_os_error()); + } + + let memfd = unsafe { OwnedFd::from_raw_fd(raw) }; + if total > 0 { + if unsafe { libc::ftruncate(memfd.as_raw_fd(), total as libc::off_t) } != 0 { + anyhow::bail!("ftruncate failed: {}", io::Error::last_os_error()); + } + if !ro.is_empty() { + Self::pwrite_all(&memfd, &ro, 0)?; + } + if !rw.is_empty() { + Self::pwrite_all(&memfd, &rw, ro_size as libc::off_t)?; + } + } + + Ok(Self { + memfd, + ro_size, + rw_size, + }) + } + + fn pwrite_all(fd: &OwnedFd, buf: &[u8], mut offset: libc::off_t) -> Result<()> { + let mut remaining = buf.len(); + let mut ptr = buf.as_ptr(); + while remaining > 0 { + let written = + unsafe { libc::pwrite(fd.as_raw_fd(), ptr as *const _, remaining, offset) }; + if written < 0 { + let err = io::Error::last_os_error(); + if err.kind() == io::ErrorKind::Interrupted { + continue; + } + anyhow::bail!("pwrite failed: {err}"); + } + let n = written as usize; + remaining -= n; + offset += n as libc::off_t; + ptr = unsafe { ptr.add(n) }; + } + Ok(()) + } +} diff --git a/crates/vm/compiler/src/memory/mmap.rs b/crates/vm/compiler/src/memory/mmap.rs index cb2519af..63a98527 100644 --- a/crates/vm/compiler/src/memory/mmap.rs +++ b/crates/vm/compiler/src/memory/mmap.rs @@ -1,16 +1,22 @@ //! Memory management for PVM programs using mmap for efficient virtual memory #![cfg(target_os = "linux")] +use crate::memory::image::MemoryImage; use anyhow::Result; -use libc::{MAP_ANONYMOUS, MAP_NORESERVE, MAP_PRIVATE, PROT_NONE, PROT_READ, PROT_WRITE}; -use pvm::MemoryLike; -use std::{collections::BTreeMap, io, ptr}; +use libc::{ + MAP_ANONYMOUS, MAP_FIXED, MAP_NORESERVE, MAP_PRIVATE, PROT_NONE, PROT_READ, PROT_WRITE, +}; +use parking_lot::Mutex; +use pvm::{MemoryLike, score::OpaqueHash}; +use std::{collections::BTreeMap, io, ptr, sync::LazyLock}; + +/// Process-wide pool of pre-reserved 4 GB virtual regions. +static REGION_POOL: LazyLock>> = LazyLock::new(|| Mutex::new(Vec::new())); /// memory for PVM programs -#[derive(Debug, Clone)] pub struct Memory { - /// Base pointer to the virtual memory region - base: *mut u8, + /// Backing 4 GB virtual region, returned to the pool on drop. + region: Region, /// Heap pointer /// @@ -20,80 +26,63 @@ pub struct Memory { impl Memory { /// Create a new memory instance from parser Memory - pub fn new(pmemory: &pvm::Memory) -> Result { - let base = unsafe { - libc::mmap( - ptr::null_mut(), - pvm::PVM_MEMORY_SIZE as usize, - PROT_NONE, - MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, - -1, - 0, - ) - }; - - if base == libc::MAP_FAILED { - anyhow::bail!( - "Failed to mmap virtual memory: {}", - std::io::Error::last_os_error() - ); - } - + pub fn new(hash: OpaqueHash, pmemory: &pvm::Memory) -> Result { + let image = MemoryImage::get_or_build(hash, pmemory)?; + let region = Region::acquire()?; let memory = Memory { - base: base as *mut u8, + region, heap_ptr: pmemory.heap_ptr, }; - - memory.init(pmemory)?; + memory.init(pmemory, &image)?; Ok(memory) } + fn base(&self) -> *mut u8 { + self.region.as_ptr() + } + /// Initialize memory regions from parser memory - fn init(&self, memory: &pvm::Memory) -> Result<()> { + fn init(&self, memory: &pvm::Memory, image: &MemoryImage) -> Result<()> { + let base = self.base(); unsafe { - // Set up read-only data region - { - let read = memory.ro_data()?; + // Bind RO range CoW from the image + if image.ro_size > 0 { let start = memory.info.read.start as usize; - let size = read.len(); - if libc::mprotect(self.base.add(start) as *mut _, size, PROT_READ | PROT_WRITE) != 0 - { - anyhow::bail!( - "Failed to make read region writable: {}", - io::Error::last_os_error() - ); - } - - ptr::copy_nonoverlapping(read.as_ptr(), self.base.add(start), size); - if libc::mprotect(self.base.add(start) as *mut _, size, PROT_READ) != 0 { - anyhow::bail!( - "Failed to set read region read-only: {}", - io::Error::last_os_error() - ); + let bound = libc::mmap( + base.add(start) as *mut _, + image.ro_size, + PROT_READ, + MAP_FIXED | MAP_PRIVATE, + image.raw_fd(), + 0, + ); + if bound == libc::MAP_FAILED { + anyhow::bail!("Failed to bind RO range: {}", io::Error::last_os_error()); } } - // Set up write region as read-write - { - let write = memory.rw_data()?; + // Bind RW range CoW from the image — writes hit private pages + if image.rw_size > 0 { let start = memory.info.write.start as usize; - let size = write.len(); - if libc::mprotect(self.base.add(start) as *mut _, size, PROT_READ | PROT_WRITE) != 0 - { - anyhow::bail!( - "Failed to set write region protection: {}", - io::Error::last_os_error() - ); + let bound = libc::mmap( + base.add(start) as *mut _, + image.rw_size, + PROT_READ | PROT_WRITE, + MAP_FIXED | MAP_PRIVATE, + image.raw_fd(), + image.ro_size as libc::off_t, + ); + if bound == libc::MAP_FAILED { + anyhow::bail!("Failed to bind RW range: {}", io::Error::last_os_error()); } - - ptr::copy_nonoverlapping(write.as_ptr(), self.base.add(start), size); } - // Set up stack region as read-write + // Stack: anonymous CoW from the slot reservation, just unlock perms { let start = memory.info.stack.start as usize; let size = (memory.info.stack.end - memory.info.stack.start) as usize; - if libc::mprotect(self.base.add(start) as *mut _, size, PROT_READ | PROT_WRITE) != 0 + if size > 0 + && libc::mprotect(base.add(start) as *mut _, size, PROT_READ | PROT_WRITE) != 0 { anyhow::bail!( "Failed to set stack region protection: {}", @@ -102,25 +91,26 @@ impl Memory { } } - // Set up args region as read-only + // Args vary per invocation — copy in, then lock read-only { let args = memory.args()?; let start = memory.info.args.start as usize; let size = args.len(); - if libc::mprotect(self.base.add(start) as *mut _, size, PROT_READ | PROT_WRITE) != 0 - { - anyhow::bail!( - "Failed to make args region writable: {}", - io::Error::last_os_error() - ); - } - - ptr::copy_nonoverlapping(args.as_ptr(), self.base.add(start), size); - if libc::mprotect(self.base.add(start) as *mut _, size, PROT_READ) != 0 { - anyhow::bail!( - "Failed to set args region read-only: {}", - io::Error::last_os_error() - ); + if size > 0 { + if libc::mprotect(base.add(start) as *mut _, size, PROT_READ | PROT_WRITE) != 0 + { + anyhow::bail!( + "Failed to make args region writable: {}", + io::Error::last_os_error() + ); + } + ptr::copy_nonoverlapping(args.as_ptr(), base.add(start), size); + if libc::mprotect(base.add(start) as *mut _, size, PROT_READ) != 0 { + anyhow::bail!( + "Failed to set args region read-only: {}", + io::Error::last_os_error() + ); + } } } } @@ -131,26 +121,27 @@ impl Memory { /// Read bytes from memory #[inline] pub fn read_bytes(&self, addr: u32, len: u32) -> &[u8] { - unsafe { std::slice::from_raw_parts(self.base.add(addr as usize), len as usize) } + unsafe { std::slice::from_raw_parts(self.base().add(addr as usize), len as usize) } } /// Write bytes to memory #[inline] pub fn write_bytes(&mut self, addr: u32, data: &[u8]) { unsafe { - ptr::copy_nonoverlapping(data.as_ptr(), self.base.add(addr as usize), data.len()); + ptr::copy_nonoverlapping(data.as_ptr(), self.base().add(addr as usize), data.len()); } } /// Convert the virtual memory back to pvm::Memory structure pub fn fill(&self, original: &pvm::Memory) -> pvm::Memory { + let base = self.base(); let mut memory_map = BTreeMap::new(); for (&page_num, (_, perms)) in &original.memory { let page_addr = (page_num as usize) * (pvm::PAGE_SIZE as usize); let mut page_data = vec![0u8; pvm::PAGE_SIZE as usize]; unsafe { ptr::copy_nonoverlapping( - self.base.add(page_addr), + base.add(page_addr), page_data.as_mut_ptr(), pvm::PAGE_SIZE as usize, ); @@ -170,16 +161,6 @@ impl Memory { } } -impl Drop for Memory { - fn drop(&mut self) { - unsafe { - if !self.base.is_null() { - libc::munmap(self.base as *mut _, pvm::PVM_MEMORY_SIZE as usize); - } - } - } -} - unsafe impl Send for Memory {} unsafe impl Sync for Memory {} @@ -205,11 +186,12 @@ impl MemoryLike for Memory { return Ok(()); } + let base = self.base(); for page_num in page..(page + count) { let page_addr = (page_num as usize) * (pvm::PAGE_SIZE as usize); unsafe { if libc::mprotect( - self.base.add(page_addr) as *mut _, + base.add(page_addr) as *mut _, pvm::PAGE_SIZE as usize, PROT_READ | PROT_WRITE, ) != 0 @@ -235,3 +217,63 @@ impl MemoryLike for Memory { self.heap_ptr = heap_ptr; } } + +/// Reusable 4 GB virtual region. +struct Region(*mut u8); + +// Safety: a `Region` is only handed out to one owner at a time. +unsafe impl Send for Region {} + +impl Region { + /// Pop from the pool, or `mmap` a fresh 4 GB reservation if empty. + fn acquire() -> Result { + if let Some(region) = REGION_POOL.lock().pop() { + return Ok(region); + } + let base = unsafe { + libc::mmap( + ptr::null_mut(), + pvm::PVM_MEMORY_SIZE as usize, + PROT_NONE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, + -1, + 0, + ) + }; + if base == libc::MAP_FAILED { + anyhow::bail!( + "Failed to mmap virtual memory: {}", + std::io::Error::last_os_error() + ); + } + Ok(Region(base as *mut u8)) + } + + fn as_ptr(&self) -> *mut u8 { + self.0 + } +} + +impl Drop for Region { + fn drop(&mut self) { + let ptr = std::mem::replace(&mut self.0, ptr::null_mut()); + if ptr.is_null() { + return; + } + + // Reset the region to a known state + let size = pvm::PVM_MEMORY_SIZE as usize; + let madv_ok = unsafe { libc::madvise(ptr as *mut _, size, libc::MADV_DONTNEED) } == 0; + let mp_ok = unsafe { libc::mprotect(ptr as *mut _, size, PROT_NONE) } == 0; + if madv_ok && mp_ok { + REGION_POOL.lock().push(Region(ptr)); + } else { + tracing::error!( + "Region reset failed (madvise_ok={madv_ok}, mprotect_ok={mp_ok}); dropping slot to avoid cross-invocation data leak" + ); + unsafe { + libc::munmap(ptr as *mut _, size); + } + } + } +} diff --git a/crates/vm/compiler/src/memory/mod.rs b/crates/vm/compiler/src/memory/mod.rs index b050e4b1..b0f4c6c4 100644 --- a/crates/vm/compiler/src/memory/mod.rs +++ b/crates/vm/compiler/src/memory/mod.rs @@ -17,5 +17,7 @@ pub use mmap::Memory; #[cfg(target_os = "macos")] pub use range::Memory; +#[cfg(target_os = "linux")] +mod image; mod mmap; mod range; diff --git a/crates/vm/compiler/src/memory/range.rs b/crates/vm/compiler/src/memory/range.rs index e2e4d688..0ddb78a9 100644 --- a/crates/vm/compiler/src/memory/range.rs +++ b/crates/vm/compiler/src/memory/range.rs @@ -41,7 +41,7 @@ pub struct Memory { impl Memory { /// Create a new memory instance from parser Memory - pub fn new(pmemory: &pvm::Memory) -> Result { + pub fn new(_hash: pvm::score::OpaqueHash, pmemory: &pvm::Memory) -> Result { tracing::debug!("memory info: {:?}", pmemory.info); let mut data = vec![]; data.extend_from_slice(&pmemory.ro_data()?); diff --git a/crates/vm/compiler/tests/memory.rs b/crates/vm/compiler/tests/memory.rs index d202e87f..2a1cb6c5 100644 --- a/crates/vm/compiler/tests/memory.rs +++ b/crates/vm/compiler/tests/memory.rs @@ -1,5 +1,6 @@ //! Tests for the Memory module +use pvm::score::OpaqueHash; use pvmc::{Memory, MemoryLike, trap}; const INIT_VALUE: u8 = 1; @@ -8,6 +9,14 @@ const REGION_START: u32 = pvm::ZONE_SIZE as u32; const REGION_END: u32 = REGION_START + REGION_SIZE as u32; const UNALLOCATED_ADDR: u32 = pvm::ZONE_SIZE as u32 * 2; +const HASH_READ: OpaqueHash = [0x11; 32]; +const HASH_WRITE: OpaqueHash = [0x22; 32]; +const HASH_STACK: OpaqueHash = [0x33; 32]; +const HASH_ARGS: OpaqueHash = [0x44; 32]; +const HASH_HEAP: OpaqueHash = [0x55; 32]; +const HASH_REUSE_RW: OpaqueHash = [0x66; 32]; +const HASH_REUSE_HEAP: OpaqueHash = [0x77; 32]; + /// Generate a read test for the given memory fn gen_read(mut memory: Memory) -> anyhow::Result<()> { let data = memory.read_bytes(REGION_START, REGION_SIZE as u32); @@ -30,17 +39,6 @@ fn gen_read(mut memory: Memory) -> anyhow::Result<()> { assert!(info.signal == libc::SIGSEGV || info.signal == libc::SIGBUS); } - /* // try accessing unallocated memory near the allocated memory - { - let Err(info) = trap::with(|| { - let slice = memory.read_bytes(REGION_END, 1); - slice[0] - }) else { - panic!("should trap on reading unallocated memory (REGION_END + 1)"); - }; - assert!(info.signal == libc::SIGSEGV || info.signal == libc::SIGBUS); - } */ - Ok(()) } @@ -74,6 +72,7 @@ fn gen_write(mut memory: Memory) -> anyhow::Result<()> { #[test] fn test_read() -> anyhow::Result<()> { let memory = Memory::new( + HASH_READ, &pvm::Memory::default().with_ro_data(vec![INIT_VALUE; REGION_SIZE], REGION_START), )?; gen_read(memory) @@ -82,6 +81,7 @@ fn test_read() -> anyhow::Result<()> { #[test] fn test_write() -> anyhow::Result<()> { let memory = Memory::new( + HASH_WRITE, &pvm::Memory::default().with_rw_data(vec![INIT_VALUE; REGION_SIZE], REGION_START), )?; gen_write(memory) @@ -89,7 +89,10 @@ fn test_write() -> anyhow::Result<()> { #[test] fn test_stack() -> anyhow::Result<()> { - let mut memory = Memory::new(&pvm::Memory::default().with_stack(REGION_START..REGION_END))?; + let mut memory = Memory::new( + HASH_STACK, + &pvm::Memory::default().with_stack(REGION_START..REGION_END), + )?; memory.write_bytes(REGION_START, &[INIT_VALUE; REGION_SIZE]); gen_write(memory) } @@ -97,6 +100,7 @@ fn test_stack() -> anyhow::Result<()> { #[test] fn test_args() -> anyhow::Result<()> { let memory = Memory::new( + HASH_ARGS, &pvm::Memory::default().with_args(vec![INIT_VALUE; REGION_SIZE], REGION_START), )?; gen_read(memory) @@ -104,10 +108,137 @@ fn test_args() -> anyhow::Result<()> { #[test] fn test_heap() -> anyhow::Result<()> { - let mut memory = Memory::new(&pvm::Memory::default().with_heap(REGION_START..REGION_END))?; + let mut memory = Memory::new( + HASH_HEAP, + &pvm::Memory::default().with_heap(REGION_START..REGION_END), + )?; // allocate takes page number, not address let page_num = REGION_START / pvm::PAGE_SIZE as u32; memory.allocate(page_num, 1)?; memory.write_bytes(REGION_START, &[INIT_VALUE; REGION_SIZE]); gen_write(memory) } + +#[cfg(target_os = "linux")] +mod pool { + use super::*; + + #[test] + fn slot_reuse_rw_writes() -> anyhow::Result<()> { + let initial = vec![0xAA_u8; REGION_SIZE]; + let pmem = pvm::Memory::default().with_rw_data(initial.clone(), REGION_START); + + // Phase 1: overwrite initial state with a recognizable pattern. + { + let mut memory = Memory::new(HASH_REUSE_RW, &pmem)?; + memory.write_bytes(REGION_START, &[0xCC; REGION_SIZE]); + assert_eq!( + memory.read_bytes(REGION_START, REGION_SIZE as u32), + &[0xCC; REGION_SIZE][..] + ); + } + + // Phase 2: same layout. Must see freshly-initialized data, not phase 1's writes. + { + let memory = Memory::new(HASH_REUSE_RW, &pmem)?; + assert_eq!( + memory.read_bytes(REGION_START, REGION_SIZE as u32), + initial.as_slice(), + "rw region must be reset to initial state on slot reuse" + ); + } + + Ok(()) + } + + #[test] + fn slot_reuse_heap_allocations() -> anyhow::Result<()> { + let pmem = pvm::Memory::default().with_heap(REGION_START..REGION_END); + let page_num = REGION_START / pvm::PAGE_SIZE as u32; + + // Phase 1: allocate the page, write a pattern, drop. + { + let mut memory = Memory::new(HASH_REUSE_HEAP, &pmem)?; + memory.allocate(page_num, 1)?; + memory.write_bytes(REGION_START, &[0xCC; REGION_SIZE]); + assert_eq!( + memory.read_bytes(REGION_START, REGION_SIZE as u32), + &[0xCC; REGION_SIZE][..] + ); + } + + // Phase 2: same layout, but no `allocate` call. The page must be PROT_NONE again. + { + let memory = Memory::new(HASH_REUSE_HEAP, &pmem)?; + let Err(info) = trap::with(|| memory.read_bytes(REGION_START, 1)[0]) else { + panic!("heap page should be unmapped on slot reuse"); + }; + assert!(info.signal == libc::SIGSEGV || info.signal == libc::SIGBUS); + } + + Ok(()) + } + + #[test] + fn image_cache_hit_preserves_ro_init() -> anyhow::Result<()> { + const HASH: OpaqueHash = [0x88; 32]; + let initial = vec![0xAA_u8; REGION_SIZE]; + let pmem = pvm::Memory::default().with_ro_data(initial.clone(), REGION_START); + + // Phase 1: cold image build. + { + let memory = Memory::new(HASH, &pmem)?; + assert_eq!( + memory.read_bytes(REGION_START, REGION_SIZE as u32), + initial.as_slice() + ); + } + + // Phase 2: cache hit. Same memfd, fresh slot binding. + { + let mut memory = Memory::new(HASH, &pmem)?; + assert_eq!( + memory.read_bytes(REGION_START, REGION_SIZE as u32), + initial.as_slice(), + "ro init must be visible via image cache hit" + ); + + let Err(info) = trap::with(|| memory.write_bytes(REGION_START, &[0; REGION_SIZE])) + else { + panic!("ro range must trap on write"); + }; + assert!(info.signal == libc::SIGSEGV || info.signal == libc::SIGBUS); + } + + Ok(()) + } + + #[test] + fn slot_reuse_swaps_image() -> anyhow::Result<()> { + const HASH_A: OpaqueHash = [0x99; 32]; + const HASH_B: OpaqueHash = [0xBB; 32]; + let init_a = vec![0xAA_u8; REGION_SIZE]; + let init_b = vec![0xCD_u8; REGION_SIZE]; + let pmem_a = pvm::Memory::default().with_rw_data(init_a, REGION_START); + let pmem_b = pvm::Memory::default().with_rw_data(init_b.clone(), REGION_START); + + // Phase 1: bind program A, dirty its RW range, drop. + { + let mut memory = Memory::new(HASH_A, &pmem_a)?; + memory.write_bytes(REGION_START, &[0xCC; REGION_SIZE]); + } + + // Phase 2: different program → different image. Must see B's init bytes, + // not A's dirty writes or A's init bytes. + { + let memory = Memory::new(HASH_B, &pmem_b)?; + assert_eq!( + memory.read_bytes(REGION_START, REGION_SIZE as u32), + init_b.as_slice(), + "second program's rw init must override prior slot binding" + ); + } + + Ok(()) + } +} diff --git a/crates/vm/spacevm/src/lib.rs b/crates/vm/spacevm/src/lib.rs index 0f93d15c..49aa4446 100644 --- a/crates/vm/spacevm/src/lib.rs +++ b/crates/vm/spacevm/src/lib.rs @@ -43,7 +43,7 @@ impl Invocation for SpaceVM { let mut context = pvm::Context { registers: program.registers, gas: gas as i64, - memory: Memory::new(&program.memory).expect("failed to create memory"), + memory: Memory::new(hash, &program.memory).expect("failed to create memory"), ctx: &mut ctx, }; diff --git a/crates/vm/src/cache.rs b/crates/vm/src/cache.rs index 7b296fc7..7d1094e7 100644 --- a/crates/vm/src/cache.rs +++ b/crates/vm/src/cache.rs @@ -1,25 +1,47 @@ -//! Lock-free read-mostly cache keyed by program hash. +//! Lock-free read-mostly cache keyed by program hash, bounded by FIFO eviction. use arc_swap::ArcSwap; use score::OpaqueHash; -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::{HashMap, VecDeque}, + sync::Arc, +}; -/// Lock-free hash-keyed cache holding `Arc`. +/// Default cache capacity when neither `with_capacity` nor `SPACEJAM_CACHE_CAPACITY` is set. +const DEFAULT_CAPACITY: usize = 32; + +/// Lock-free hash-keyed cache holding `Arc` with FIFO eviction at capacity. pub struct Cache { - inner: ArcSwap>>, + inner: ArcSwap>, } impl Cache { + /// Construct a cache with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self { + inner: ArcSwap::from_pointee(Inner::new(capacity)), + } + } + /// Look up by hash; returns a cheap `Arc` clone on hit. pub fn get(&self, hash: &OpaqueHash) -> Option> { - self.inner.load().get(hash).cloned() + self.inner.load().map.get(hash).cloned() } /// Insert or replace. pub fn put(&self, hash: OpaqueHash, value: Arc) { self.inner.rcu(|prev| { - let mut next = (**prev).clone(); - next.insert(hash, value.clone()); + let mut next = prev.clone_shape(); + let exists = next.map.contains_key(&hash); + if !exists && next.map.len() >= next.capacity { + if let Some(evict) = next.order.pop_front() { + next.map.remove(&evict); + } + } + next.map.insert(hash, value.clone()); + if !exists { + next.order.push_back(hash); + } next }); } @@ -27,8 +49,39 @@ impl Cache { impl Default for Cache { fn default() -> Self { + Self::with_capacity(default_capacity()) + } +} + +struct Inner { + map: HashMap>, + order: VecDeque, + capacity: usize, +} + +impl Inner { + fn new(capacity: usize) -> Self { + let capacity = capacity.max(1); + Self { + map: HashMap::with_capacity(capacity), + order: VecDeque::with_capacity(capacity), + capacity, + } + } + + fn clone_shape(&self) -> Self { Self { - inner: ArcSwap::from_pointee(HashMap::new()), + map: self.map.clone(), + order: self.order.clone(), + capacity: self.capacity, } } } + +fn default_capacity() -> usize { + std::env::var("SPACEJAM_CACHE_CAPACITY") + .ok() + .and_then(|s| s.parse::().ok()) + .filter(|&n| n > 0) + .unwrap_or(DEFAULT_CAPACITY) +}