From 1b968f11501bd4c85d2ad8a5f1f3c6a555f71f04 Mon Sep 17 00:00:00 2001 From: clearloop Date: Fri, 22 May 2026 22:06:34 +0800 Subject: [PATCH 01/23] feat(runtime): batch signature verifications for guarantees and assurances --- crates/crypto/src/ed25519.rs | 46 ++++++++++++------ crates/runtime/src/tx/assurance/mod.rs | 27 +++++------ crates/runtime/src/tx/guarantee/mod.rs | 8 +++- crates/runtime/src/tx/guarantee/validator.rs | 40 ++++++++-------- crates/runtime/src/tx/mod.rs | 50 ++++++++++---------- crates/testing/src/assurances.rs | 6 ++- crates/testing/src/reports.rs | 7 ++- 7 files changed, 106 insertions(+), 78 deletions(-) diff --git a/crates/crypto/src/ed25519.rs b/crates/crypto/src/ed25519.rs index 00d773be..3edeb5a5 100644 --- a/crates/crypto/src/ed25519.rs +++ b/crates/crypto/src/ed25519.rs @@ -2,8 +2,12 @@ #![cfg(feature = "ed25519")] pub use ed25519_zebra::{batch, Signature, SigningKey, VerificationKey, VerificationKeyBytes}; +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; + /// Ed25519 key pair. #[derive(Clone)] pub struct KeyPair { @@ -36,30 +40,45 @@ pub fn verify(message: &[u8], signature: [u8; 64], key: [u8; 32]) -> anyhow::Res key.verify(&signature, message).map_err(Into::into) } -/// Number of signatures per parallel chunk for batch verification. -const BATCH_PAR_CHUNK: usize = 32; +/// Owned signature item for deferred verification. +pub struct SigItem { + pub message: Vec, + pub signature: [u8; 64], + pub key: [u8; 32], +} + +impl SigItem { + /// Verify a single signature. + pub fn verify(&self) -> anyhow::Result<()> { + verify(&self.message, self.signature, self.key) + } + + /// Batch verify a slice of items. + pub fn batch_verify(items: &[Self]) -> anyhow::Result<()> { + if items.is_empty() { + return Ok(()); + } + let view: Vec<(&[u8], [u8; 64], [u8; 32])> = items + .iter() + .map(|i| (i.message.as_slice(), i.signature, i.key)) + .collect(); + batch_verify(&view) + } +} /// Batch verify a set of Ed25519 signatures. -/// -/// Uses ZIP-215 batch verification (semantically identical to single-verify) and -/// parallelizes across rayon's pool for batches larger than [`BATCH_PAR_CHUNK`]. -/// -/// Returns `Ok(())` iff every `(message, signature, key)` triple is valid. pub fn batch_verify(items: &[(&[u8], [u8; 64], [u8; 32])]) -> anyhow::Result<()> { if items.is_empty() { return Ok(()); } if items.len() <= BATCH_PAR_CHUNK { - return verify_one_batch(items); + return verify_batch(items); } - items - .par_chunks(BATCH_PAR_CHUNK) - .try_for_each(verify_one_batch) + items.par_chunks(BATCH_PAR_CHUNK).try_for_each(verify_batch) } -fn verify_one_batch(items: &[(&[u8], [u8; 64], [u8; 32])]) -> anyhow::Result<()> { - use rand::rngs::OsRng; +fn verify_batch(items: &[(&[u8], [u8; 64], [u8; 32])]) -> anyhow::Result<()> { let mut batch = batch::Verifier::new(); for (msg, sig, key) in items { batch.queue(( @@ -71,7 +90,6 @@ fn verify_one_batch(items: &[(&[u8], [u8; 64], [u8; 32])]) -> anyhow::Result<()> batch.verify(OsRng).map_err(Into::into) } -#[cfg(feature = "rand")] impl Default for KeyPair { fn default() -> Self { use rand::{rngs::OsRng, Rng}; diff --git a/crates/runtime/src/tx/assurance/mod.rs b/crates/runtime/src/tx/assurance/mod.rs index 470e2260..f1546d20 100644 --- a/crates/runtime/src/tx/assurance/mod.rs +++ b/crates/runtime/src/tx/assurance/mod.rs @@ -29,17 +29,21 @@ pub fn reports( reports } -/// (W) Handle assurances input and return newly available reports +/// (W) Handle assurances input and return newly available reports. pub fn available( reports: &AvailabilityAssignments, validators: &[ValidatorData], parent: OpaqueHash, assurances: &[AvailAssurance], -) -> Result<(Vec, [u32; CORES_COUNT])> { +) -> Result<( + Vec, + [u32; CORES_COUNT], + Vec, +)> { // Track assurance count per core let mut core_assurance_counts = [0u32; CORES_COUNT]; - // Check for engaged reports: cheap checks first, then batch verify sigs. + // Semantic checks; sig verification is deferred to the caller's batch. let mut assuror = None; for assurance in assurances.iter() { if assurance.validator_index >= VALIDATORS_COUNT { @@ -73,19 +77,14 @@ pub fn available( } } - let messages: Vec> = assurances.iter().map(|a| a.singing_message()).collect(); - let verify_items: Vec<_> = assurances + let triples: Vec = assurances .iter() - .zip(messages.iter()) - .map(|(a, m)| { - ( - m.as_slice(), - a.signature, - validators[a.validator_index as usize].ed25519, - ) + .map(|a| crypto::ed25519::SigItem { + message: a.singing_message(), + signature: a.signature, + key: validators[a.validator_index as usize].ed25519, }) .collect(); - crypto::ed25519::batch_verify(&verify_items).map_err(|_| Error::BadSignature)?; // Check which cores reached 2/3 majority let mut available = Vec::new(); @@ -97,5 +96,5 @@ pub fn available( } } - Ok((available, core_assurance_counts)) + Ok((available, core_assurance_counts, triples)) } diff --git a/crates/runtime/src/tx/guarantee/mod.rs b/crates/runtime/src/tx/guarantee/mod.rs index 9f6192de..33b7d37a 100644 --- a/crates/runtime/src/tx/guarantee/mod.rs +++ b/crates/runtime/src/tx/guarantee/mod.rs @@ -249,13 +249,17 @@ pub fn pools( new_pools } -/// (p of β') Report the work packages +/// (p of β') Report the work packages. pub fn report( state: &score::State, slot: TimeSlot, services: &impl Accounts, guarantees: &GuaranteesExtrinsic, -) -> Result<(Vec, Vec)> { +) -> Result<( + Vec, + Vec, + Vec, +)> { let mut validator = validator::GuaranteeValidator::new(state, services); validator.validate(slot, guarantees) } diff --git a/crates/runtime/src/tx/guarantee/validator.rs b/crates/runtime/src/tx/guarantee/validator.rs index 851f6297..440b8caa 100644 --- a/crates/runtime/src/tx/guarantee/validator.rs +++ b/crates/runtime/src/tx/guarantee/validator.rs @@ -2,6 +2,7 @@ use crate::tx::guarantee::error::{Error, Result}; use account::{Account, Accounts}; +use crypto::ed25519::SigItem; use score::{ CORES_COUNT, CoreIndex, EPOCH_LENGTH, Ed25519Public, Entropy, MAX_DEPENDENCY_COUNT, MAX_WORK_REPORT_OUTPUT_SIZE, OpaqueHash, ROTATION_PERIOD, State, TimeSlot, VALIDATORS_COUNT, @@ -39,26 +40,27 @@ impl<'s, R: Accounts> GuaranteeValidator<'s, R> { } } - /// Validate work reports according to the guarantees extrinsic + /// Validate work reports according to the guarantees extrinsic. #[tracing::instrument(skip_all, name = "guarantee")] pub fn validate( &mut self, slot: TimeSlot, guarantees: &GuaranteesExtrinsic, - ) -> Result<(Vec, Vec)> { + ) -> Result<(Vec, Vec, Vec)> { self.init_deps(guarantees); self.timeslot = slot; // Prepare for reporting let mut reported = Vec::new(); let mut reporters = BTreeSet::new(); + let mut triples = Vec::new(); // Process each guarantee for guarantee in guarantees.iter() { self.validate_results(guarantee)?; self.validate_block(guarantee)?; self.validate_deps(guarantee)?; - let guarantors = self.validate_guarantee(guarantee)?; + let (guarantors, mut g_triples) = self.validate_guarantee(guarantee)?; // Record reported package reported.push(ReportedWorkPackage { @@ -68,11 +70,12 @@ impl<'s, R: Accounts> GuaranteeValidator<'s, R> { // Record reporters (guarantors) reporters.extend(guarantors); + triples.append(&mut g_triples); } // Sort the reported work packages and reporters reported.sort_by_key(|a| a.hash); - Ok((reported, reporters.into_iter().collect())) + Ok((reported, reporters.into_iter().collect(), triples)) } fn init_deps(&mut self, guarantees: &GuaranteesExtrinsic) { @@ -171,7 +174,10 @@ impl<'s, R: Accounts> GuaranteeValidator<'s, R> { Ok(()) } - fn validate_guarantee(&mut self, guarantee: &ReportGuarantee) -> Result> { + fn validate_guarantee( + &mut self, + guarantee: &ReportGuarantee, + ) -> Result<(Vec, Vec)> { // 1. validate the rotation let guarantors = self.validate_rotation(guarantee)?; @@ -186,10 +192,10 @@ impl<'s, R: Accounts> GuaranteeValidator<'s, R> { return Err(Error::InsufficientGuarantees); } - // 4. validate the signatures: cheap checks first, then batch verify. + // 4. Semantic checks; collect triples for the caller to batch-verify. let message = guarantee.signing_message(); let mut guarantor = None; - let mut to_verify = Vec::with_capacity(guarantee.signatures.len()); + let mut triples = Vec::with_capacity(guarantee.signatures.len()); for sig in guarantee.signatures.iter() { let validator_index = sig.validator_index as usize; if validator_index >= VALIDATORS_COUNT as usize { @@ -211,27 +217,23 @@ impl<'s, R: Accounts> GuaranteeValidator<'s, R> { return Err(Error::BannedValidator); } - to_verify.push((message.as_slice(), sig.signature, *key)); + triples.push(SigItem { + message: message.clone(), + signature: sig.signature, + key: *key, + }); guarantor = Some(validator_index); } - crypto::ed25519::batch_verify(&to_verify) - .inspect_err(|_| { - tracing::debug!( - "failed to verify guarantee signatures for slot={}", - guarantee.slot, - ) - }) - .map_err(|_| Error::BadSignature)?; - self.processed.insert(guarantee.report.core_index); // Return only the validators who actually provided signatures (reporters) - Ok(guarantee + let reporters = guarantee .signatures .iter() .map(|sig| guarantors[&(sig.validator_index as usize)]) - .collect()) + .collect(); + Ok((reporters, triples)) } fn validate_results(&self, guarantee: &ReportGuarantee) -> Result<()> { diff --git a/crates/runtime/src/tx/mod.rs b/crates/runtime/src/tx/mod.rs index 3a7e24cd..fe9ce653 100644 --- a/crates/runtime/src/tx/mod.rs +++ b/crates/runtime/src/tx/mod.rs @@ -72,7 +72,7 @@ pub fn simulate_with_state( // (E_P) Validate preimages against the prior state (12.6) preimage::validate(&mut accounts, &block.extrinsic.preimages)?; - let (mut reports, reported, reporters) = { + let (mut reports, reported, reporters, available, assurances) = { // (η') Update entropy (6.22) let entropy = crypto::vrf::ietf_output(block.header.entropy_source).unwrap_or_default(); state.entropy = ticket::eta(new_epoch, &state.entropy, entropy); @@ -110,32 +110,24 @@ pub fn simulate_with_state( last.state_root = block.header.parent_state_root; } - // (p of β') validate the guarantees - let (mut reported, mut reporters) = (vec![], vec![]); - if !block.extrinsic.guarantees.is_empty() { - (reported, reporters) = { - let _guard = timing::guarantees(); - guarantee::report( - &state, - block.header.slot, - &accounts, - &block.extrinsic.guarantees, - )? - } + // (p of β') collect guarantee semantic results + triples (no verify yet) + 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, + &accounts, + &block.extrinsic.guarantees, + )? }; // (ρ†) Update availability assignments based on verdicts (V) (10.15) - ( - dispute::reports(&marks, &state.reports), - reported, - reporters, - ) - }; + let reports = dispute::reports(&marks, &state.reports); - // Round 2 computation - let (available, assurances) = { - // (W) the sequence of new available work reports (11.16) - let (available, assurances) = self::assurance::available( + // (W) the sequence of new available work reports (11.16); semantic only + let (available, assurances, a_triples) = self::assurance::available( &reports, if new_epoch { &state.validators.previous @@ -146,14 +138,20 @@ pub fn simulate_with_state( &block.extrinsic.assurances, )?; + batch.extend(a_triples); + crypto::ed25519::SigItem::batch_verify(&batch)?; + (reports, reported, reporters, available, assurances) + }; + + // Round 2 computation: apply state effects now that sigs have been verified. + { // (ρ‡) Update availability assignments based on assurances (11.17) - reports = self::assurance::reports(block.header.slot, &available, reports.clone()); + reports = assurance::reports(block.header.slot, &available, reports); // (ρ') Update availability assignments based on guarantees (11.43) state.reports = guarantee::reports(block.header.slot, &reports, &block.extrinsic.guarantees)?; - (available, assurances) - }; + } // Round 3 computation let (root, accounts) = { diff --git a/crates/testing/src/assurances.rs b/crates/testing/src/assurances.rs index 187b3ad0..ba135271 100644 --- a/crates/testing/src/assurances.rs +++ b/crates/testing/src/assurances.rs @@ -22,7 +22,11 @@ pub fn run(test: &specjam::Test) -> anyhow::Result<()> { &input.pre_state.curr_validators, input.input.parent, &input.input.assurances, - ); + ) + .and_then(|(available, counts, triples)| { + crypto::ed25519::SigItem::batch_verify(&triples).map_err(|_| Error::BadSignature)?; + Ok((available, counts)) + }); assert_eq!(result.clone().map(|(a, _)| a), output.map(|s| s.reported)); // validate post state diff --git a/crates/testing/src/reports.rs b/crates/testing/src/reports.rs index 8cc19b93..dc1cca2e 100644 --- a/crates/testing/src/reports.rs +++ b/crates/testing/src/reports.rs @@ -33,8 +33,11 @@ pub fn run(test: &specjam::Test) -> anyhow::Result<()> { let result = tx::guarantee::reports(input.slot, &pre_state.avail_assignments, &input.guarantees) .and_then(|assignments| { - tx::guarantee::report(&state, input.slot, &state.accounts, &input.guarantees) - .map(|(reported, reporters)| (reported, reporters, assignments)) + let (reported, reporters, triples) = + tx::guarantee::report(&state, input.slot, &state.accounts, &input.guarantees)?; + crypto::ed25519::SigItem::batch_verify(&triples) + .map_err(|_| Error::BadSignature)?; + Ok((reported, reporters, assignments)) }); assert_eq!( From c5d166ccdf03a71f99c458ddc79e86f99be0672a Mon Sep 17 00:00:00 2001 From: clearloop Date: Fri, 22 May 2026 22:45:40 +0800 Subject: [PATCH 02/23] chore(spacejam): clean the feature gate --- Makefile | 4 +-- crates/spacejam/Cargo.toml | 44 ++++++++--------------------- crates/spacejam/src/node/builder.rs | 22 ++++++--------- 3 files changed, 22 insertions(+), 48 deletions(-) diff --git a/Makefile b/Makefile index 952f109f..dd2a49b4 100644 --- a/Makefile +++ b/Makefile @@ -53,13 +53,13 @@ linux-amd64: # build linux-amd64 with full-spec constants linux-amd64-full: - cargo b --profile prod --target x86_64-unknown-linux-gnu --no-default-features --features bin,serde,full + cargo b --profile prod --target x86_64-unknown-linux-gnu --no-default-features --features full # build both tiny and full binaries for docker linux-amd64-both: cargo b --profile prod -p spacejam --target x86_64-unknown-linux-gnu cp target/x86_64-unknown-linux-gnu/prod/spacejam target/x86_64-unknown-linux-gnu/prod/spacejam-tiny - cargo b --profile prod -p spacejam --target x86_64-unknown-linux-gnu --no-default-features --features bin,serde,full + cargo b --profile prod -p spacejam --target x86_64-unknown-linux-gnu --no-default-features --features full cp target/x86_64-unknown-linux-gnu/prod/spacejam target/x86_64-unknown-linux-gnu/prod/spacejam-full # build the docker image, tagging both :latest and :$(VERSION) diff --git a/crates/spacejam/Cargo.toml b/crates/spacejam/Cargo.toml index 3d572a91..ca745129 100644 --- a/crates/spacejam/Cargo.toml +++ b/crates/spacejam/Cargo.toml @@ -6,33 +6,32 @@ edition.workspace = true [[bin]] name = "spacejam" path = "bin/spacejam.rs" -required-features = ["node"] [dependencies] account.workspace = true anyhow.workspace = true async-trait.workspace = true -clap = { workspace = true, features = ["derive", "env"], optional = true } +clap = { workspace = true, features = ["derive", "env"] } codec.workspace = true -crypto.workspace = true +crypto = { workspace = true, features = ["bls", "ed25519", "vrf"] } dhat = { workspace = true, optional = true } dirs.workspace = true hex.workspace = true -network.workspace = true +network = { workspace = true, features = ["cmd"] } offchain.workspace = true parity-db.workspace = true spacevm.workspace = true rand.workspace = true rayon.workspace = true runtime.workspace = true -score = { workspace = true } -serde = { workspace = true, optional = true } -serde_json = { workspace = true, optional = true } -spacejson = { workspace = true, optional = true } +score.workspace = true +serde.workspace = true +serde_json.workspace = true +spacejson.workspace = true sysinfo.workspace = true testing.workspace = true -time = { workspace = true, optional = true, features = ["local-offset"] } -toml = { workspace = true, optional = true } +time = { workspace = true, features = ["local-offset"] } +toml.workspace = true tokio = { workspace = true, features = [ "rt", "rt-multi-thread", @@ -42,10 +41,7 @@ tokio = { workspace = true, features = [ "signal", ] } tracing.workspace = true -tracing-subscriber = { workspace = true, optional = true, features = [ - "fmt", - "time", -] } +tracing-subscriber = { workspace = true, features = ["fmt", "time"] } [dev-dependencies] temp-dir.workspace = true @@ -56,25 +52,7 @@ proc-macro2.workspace = true quote.workspace = true [features] -bin = ["node"] -cmd = [ - "clap", - "network/cmd", - "serde", - "spacejson", - "time", - "tracing-subscriber", -] +default = ["tiny"] dhat = ["dep:dhat"] -default = ["bin", "serde", "tiny"] -node = ["cmd"] -serde = [ - "dep:serde", - "serde_json", - "toml", - "crypto/bls", - "crypto/ed25519", - "crypto/vrf", -] tiny = ["score/tiny"] full = ["score/full"] diff --git a/crates/spacejam/src/node/builder.rs b/crates/spacejam/src/node/builder.rs index cbb27cdf..34b1168f 100644 --- a/crates/spacejam/src/node/builder.rs +++ b/crates/spacejam/src/node/builder.rs @@ -8,42 +8,38 @@ use network::Network; use std::{fs, net::SocketAddr, path::PathBuf, sync::Arc}; /// Spacejam node builder -#[derive(Clone)] -#[cfg_attr(feature = "cmd", derive(clap::Parser))] +#[derive(Clone, clap::Parser)] pub struct Builder { /// The genesis path - #[cfg_attr(feature = "cmd", arg(long, env = "CHAIN"))] + #[arg(long, env = "CHAIN")] pub chain: Option, /// The data path - #[cfg_attr(feature = "cmd", arg(short, long, default_value_t = default::data_path(), env = "DATA_PATH"))] + #[arg(short, long, default_value_t = default::data_path(), env = "DATA_PATH")] pub data_path: String, /// Whether running in dev mode - #[cfg_attr(feature = "cmd", arg(long, env = "DEV"))] + #[arg(long, env = "DEV")] pub dev: bool, /// Whether running in light mode - #[cfg_attr(feature = "cmd", arg(long, env = "LIGHT"))] + #[arg(long, env = "LIGHT")] pub light: bool, /// The network configuration - #[cfg_attr(feature = "cmd", command(flatten))] + #[command(flatten)] pub network: network::Config, /// Whether pruning the data directory before running - #[cfg_attr(feature = "cmd", arg(short, long, env = "PRUNE"))] + #[arg(short, long, env = "PRUNE")] pub prune: bool, /// The RPC address - #[cfg_attr( - feature = "cmd", - arg(short, long, default_value = "0.0.0.0:6789", env = "RPC") - )] + #[arg(short, long, default_value = "0.0.0.0:6789", env = "RPC")] pub rpc: SocketAddr, /// The seed of the validator - #[cfg_attr(feature = "cmd", arg(long, env = "VALIDATOR"))] + #[arg(long, env = "VALIDATOR")] pub validator: Option, } From 6bb889383638a7a60d379fa8dfee8531b4d2c417 Mon Sep 17 00:00:00 2001 From: clearloop Date: Fri, 22 May 2026 23:47:32 +0800 Subject: [PATCH 03/23] feat(runtime): introduce block executor --- crates/runtime/src/tx/executor.rs | 381 ++++++++++++++++++++++++++++++ crates/runtime/src/tx/mod.rs | 203 +--------------- 2 files changed, 387 insertions(+), 197 deletions(-) create mode 100644 crates/runtime/src/tx/executor.rs diff --git a/crates/runtime/src/tx/executor.rs b/crates/runtime/src/tx/executor.rs new file mode 100644 index 00000000..15ebedfc --- /dev/null +++ b/crates/runtime/src/tx/executor.rs @@ -0,0 +1,381 @@ +//! Block state-transition executor. +//! +//! Wraps the four-round STF pipeline into a single struct with one method per +//! round. Guarantee/assurance ed25519 batch verify runs in parallel with the +//! ticket::safrole ring-VRF via `rayon::join`. + +use crate::{ + Storage, + account::Accounts, + storage::Commit, + timing, + tx::{assurance, block, dispute, guarantee, preimage, ticket}, +}; +use account::Accounts as _; +use anyhow::Result; +use pvm::Pvm; +use score::{ + Block, CORES_COUNT, EPOCH_LENGTH, Ed25519Public, OpaqueHash, State, TrieKey, + block::header::{EpochMark, TicketsMark}, + extrinsic::dispute::DisputesRecords, + safrole::{Safrole, ValidatorIter}, + service::{AvailabilityAssignments, ReportedWorkPackage, WorkReport}, +}; +use std::{marker::PhantomData, sync::Arc, thread}; + +/// Four-round block state-transition executor. +pub struct Executor<'a, Vm: Pvm, S: Storage> { + block: &'a mut Block, + state: State, + accounts: Option>, + new_epoch: bool, + + // round-to-round handoff + dispute_records: DisputesRecords, + reports: AvailabilityAssignments, + reported: Vec, + reporters: Vec, + available: Vec, + counts: [u32; CORES_COUNT], + root: OpaqueHash, + _vm: PhantomData, +} + +impl<'a, Vm: Pvm, S: Storage> Executor<'a, Vm, S> { + /// Initialize the executor for a block + prior state. + pub fn new(block: &'a mut Block, state: State, storage: Arc) -> Self { + let new_epoch = block.header.epoch() > (state.timeslot / EPOCH_LENGTH); + Self { + block, + state, + accounts: Some(Accounts::new(storage)), + new_epoch, + dispute_records: DisputesRecords::default(), + reports: AvailabilityAssignments::default(), + reported: vec![], + reporters: vec![], + available: vec![], + counts: [0u32; CORES_COUNT], + root: [0u8; 32], + _vm: PhantomData, + } + } + + /// Run the four-round STF and emit the resulting state diff. + #[tracing::instrument(skip_all, name = "stf")] + pub fn run(mut self) -> Result>> { + self.validate_extrinsics()?; + self.update_reports()?; + self.accumulate()?; + self.finalize() + } + + /// Round 1 — validate the block against prior state. + /// + /// - extrinsic hash check + /// - preimages (E_P) (12.6) + /// - entropy update (η') (6.22) + /// - disputes (ψ') (10.4) + /// - validator rotation (λ', κ') on epoch change (6.13) + /// - last-block state-root patch + /// - `rayon::join`: guarantee/assurance ed25519 batch ∥ ticket::safrole + /// ring-VRF (γ') (12.10) + fn validate_extrinsics(&mut self) -> Result<()> { + if self.block.extrinsic.hash() != self.block.header.extrinsic_hash { + anyhow::bail!("extrinsic hash mismatch"); + } + + // (E_P) Validate preimages against prior state (12.6) + let accounts = self.accounts.as_mut().expect("accounts present"); + preimage::validate(accounts, &self.block.extrinsic.preimages)?; + + // (η') Update entropy (6.22) + let entropy = + crypto::vrf::ietf_output(self.block.header.entropy_source).unwrap_or_default(); + self.state.entropy = ticket::eta(self.new_epoch, &self.state.entropy, entropy); + + // (ψ') Update disputes against prior validator sets (10.4) + self.dispute_records = if self.block.extrinsic.disputes.is_empty() { + if !self.block.header.offenders_mark.is_empty() { + anyhow::bail!("offenders mark is not empty"); + } + DisputesRecords::default() + } else { + let (next_psi, records) = dispute::disputes( + self.state.timeslot, + &self.state.validators.current, + &self.state.validators.previous, + &self.state.disputes, + &self.block.extrinsic.disputes, + )?; + self.state.disputes = next_psi; + self.block.header.offenders_mark = records.offenders.clone(); + records + }; + + // (λ', κ') Update validator state on epoch change (6.13) + if self.new_epoch { + self.state.validators.previous = std::mem::replace( + &mut self.state.validators.current, + self.state.safrole.validators.clone(), + ); + } + + // Patch the parent-state-root field on the last block of history + if let Some(last) = self.state.recent_blocks.history.last_mut() { + last.state_root = self.block.header.parent_state_root; + } + + self.sigs_and_safrole_parallel() + } + + /// Round 2 — apply availability and guarantee outcomes to report assignments. + /// + /// - availability outcomes (ρ‡) (11.17) + /// - new guarantees (ρ') (11.43) + fn update_reports(&mut self) -> Result<()> { + let reports = std::mem::take(&mut self.reports); + let reports = assurance::reports(self.block.header.slot, &self.available, reports); + self.state.reports = guarantee::reports( + self.block.header.slot, + &reports, + &self.block.extrinsic.guarantees, + )?; + Ok(()) + } + + /// Round 3 — statistics and accumulation. + /// + /// - statistics update (π') + /// - accumulate available work reports via the PVM + /// - merge accumulation result into state fields + /// - spawn `ticket::lazy::drawn` warmer for the next safrole candidate + fn accumulate(&mut self) -> Result<()> { + self.state.statistics.update( + self.new_epoch, + self.block.header.author_index, + &self.block.extrinsic, + )?; + self.state + .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"); + + let accumulation = guarantee::accumulate::( + self.block.header.slot, + self.state.timeslot, + available, + &self.state.queue, + &self.state.history, + &self.state.privileges, + &self.state.validators.drawn, + &self.state.authorization, + accounts, + self.state.entropy, + )?; + + self.state.privileges = accumulation.privileges; + self.state.queue = accumulation.ready_queue; + self.state.history = accumulation.accumulated_queue; + self.state.validators.drawn = accumulation.validators; + self.state.authorization = accumulation.authorization; + + let candidate = self + .state + .safrole + .next(&self.state.validators.drawn, &self.state.disputes.offenders); + thread::spawn(move || ticket::lazy::drawn(&candidate)); + + self.state.statistics.merge_services(accumulation.records); + self.state.logs = accumulation.logs; + self.root = accumulation.root; + self.accounts = Some(accumulation.accounts); + Ok(()) + } + + /// Round 4 — commit block and emit the state diff. + /// + /// - block history (β') + /// - reporter statistics merge + /// - preimage integration (δ') + /// - authorization pools (α') (12.13) + /// - timeslot (τ') + /// - flush state pairs into the final diff + fn finalize(mut self) -> Result>> { + let mut diff = Commit::default(); + + // (β') Update the block history + block::history::import( + &mut self.state.recent_blocks, + self.block.header.hash(), + self.root, + std::mem::take(&mut self.reported), + ); + + if !self.reporters.is_empty() { + self.state + .statistics + .merge_reporters(&self.reporters, &self.state.validators.current.ed25519())?; + } + + // (δ') Integrate preimages into the post-transfer state + let accounts = self.accounts.take().expect("accounts present"); + let accounts = preimage::accounts( + self.block.header.slot, + &self.block.extrinsic.preimages, + accounts, + ); + let (updates, removals) = accounts.diff(); + diff.extend_iter(updates, removals); + + // (α') Update the authorization pools (12.13) + self.state.pools = guarantee::pools( + self.block.header.slot, + &self.state.pools, + &self.state.authorization, + &self.block.extrinsic.guarantees, + ); + + // (τ') Update the timeslot + self.state.timeslot = self.block.header.slot; + + diff.update + .extend(self.state.pairs(self.new_epoch, &self.block.extrinsic)); + Ok(diff) + } + + /// Run guarantee/assurance sig collect + batch_verify in parallel with + /// ticket::safrole ring-VRF. + fn sigs_and_safrole_parallel(&mut self) -> Result<()> { + let state_view: &State = &self.state; + let block_view: &Block = &*self.block; + let accounts_view = self.accounts.as_ref().expect("accounts present"); + let dispute_records_view = &self.dispute_records; + let new_epoch = self.new_epoch; + let (sigs_res, safrole_res) = rayon::join( + || { + Self::sigs_branch( + state_view, + accounts_view, + block_view, + dispute_records_view, + new_epoch, + ) + }, + || Self::safrole_branch(state_view, block_view, new_epoch), + ); + + let out = sigs_res?; + self.reported = out.reported; + self.reporters = out.reporters; + self.available = out.available; + self.counts = out.counts; + self.reports = out.reports; + + if let Some(s) = safrole_res? { + self.state.safrole = s.safrole; + self.block.header.epoch_mark = s.epoch_mark; + self.block.header.tickets_mark = s.tickets_mark; + } + + Ok(()) + } + + /// Collect guarantee + assurance ed25519 triples and batch-verify them. + fn sigs_branch( + state: &State, + accounts: &Accounts, + block: &Block, + dispute_records: &DisputesRecords, + new_epoch: bool, + ) -> Result { + // (p of β') Collect guarantee triples + 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, + accounts, + &block.extrinsic.guarantees, + )? + }; + + // (ρ†) Update availability assignments based on verdicts (10.15) + let reports = dispute::reports(dispute_records, &state.reports); + + // (W) Collect assurance triples (11.16) + let (available, counts, a_triples) = assurance::available( + &reports, + if new_epoch { + &state.validators.previous + } else { + &state.validators.current + }, + block.header.parent, + &block.extrinsic.assurances, + )?; + + batch.extend(a_triples); + crypto::ed25519::SigItem::batch_verify(&batch)?; + Ok(SigsOutput { + reported, + reporters, + available, + counts, + reports, + }) + } + + /// Compute next safrole state via ring-VRF and derive header marks. + fn safrole_branch( + state: &State, + block: &Block, + new_epoch: bool, + ) -> Result> { + if block.extrinsic.tickets.is_empty() && !new_epoch { + return Ok(None); + } + let _guard = timing::safrole(); + let safrole = ticket::safrole( + state.timeslot, + block.header.slot, + state.entropy, + &state.disputes.offenders, + &state.safrole, + &state.validators, + &block.extrinsic.tickets, + )?; + let epoch_mark = if new_epoch { + safrole.epoch_mark(&state.entropy) + } else { + None + }; + let tickets_mark = safrole.tickets_mark(state.timeslot, block.header.slot); + Ok(Some(SafroleOutput { + safrole, + epoch_mark, + tickets_mark, + })) + } +} + +/// Output of the guarantee/assurance sigs branch. +struct SigsOutput { + reported: Vec, + reporters: Vec, + available: Vec, + counts: [u32; CORES_COUNT], + reports: AvailabilityAssignments, +} + +/// Output of the ticket::safrole branch. +struct SafroleOutput { + safrole: Safrole, + epoch_mark: Option, + tickets_mark: Option, +} diff --git a/crates/runtime/src/tx/mod.rs b/crates/runtime/src/tx/mod.rs index fe9ce653..a7f40ca0 100644 --- a/crates/runtime/src/tx/mod.rs +++ b/crates/runtime/src/tx/mod.rs @@ -2,19 +2,19 @@ use crate::{ Storage, - account::Accounts, storage::{Column, Commit}, timing, }; -use account::Accounts as _; use anyhow::Result; +pub use executor::Executor; use pvm::Pvm; -use score::{Block, TrieKey, safrole::ValidatorIter}; -use std::{sync::Arc, thread}; +use score::{Block, TrieKey}; +use std::sync::Arc; pub mod assurance; pub mod block; pub mod dispute; +pub mod executor; pub mod guarantee; pub mod preimage; pub mod ticket; @@ -56,199 +56,8 @@ pub fn simulate( /// Simulate state transition with new block pub fn simulate_with_state( block: &mut Block, - mut state: score::State, + state: score::State, storage: Arc, ) -> Result>> { - let epoch = block.header.epoch(); - let new_epoch: bool = epoch > (state.timeslot / score::EPOCH_LENGTH); - - // validate the extrinsic hash - if block.extrinsic.hash() != block.header.extrinsic_hash { - anyhow::bail!("extrinsic hash mismatch"); - } - - // The first round computation - let mut accounts = Accounts::new(storage); - - // (E_P) Validate preimages against the prior state (12.6) - preimage::validate(&mut accounts, &block.extrinsic.preimages)?; - let (mut reports, reported, reporters, available, assurances) = { - // (η') Update entropy (6.22) - let entropy = crypto::vrf::ietf_output(block.header.entropy_source).unwrap_or_default(); - state.entropy = ticket::eta(new_epoch, &state.entropy, entropy); - - // (ψ') Update disputes against the prior validator sets (10.4) - let marks = if block.extrinsic.disputes.is_empty() { - if !block.header.offenders_mark.is_empty() { - anyhow::bail!("offenders mark is not empty"); - } - Default::default() - } else { - let (disputes, marks) = self::dispute::disputes( - state.timeslot, - &state.validators.current, - &state.validators.previous, - &state.disputes, - &block.extrinsic.disputes, - )?; - - state.disputes = disputes; - block.header.offenders_mark = marks.offenders.clone(); - marks - }; - - if new_epoch { - // (λ', κ') Update validator state (6.13) - state.validators.previous = std::mem::replace( - &mut state.validators.current, - state.safrole.validators.clone(), - ); - } - - // complete the state root of the last block in the history - if let Some(last) = state.recent_blocks.history.last_mut() { - last.state_root = block.header.parent_state_root; - } - - // (p of β') collect guarantee semantic results + triples (no verify yet) - 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, - &accounts, - &block.extrinsic.guarantees, - )? - }; - - // (ρ†) Update availability assignments based on verdicts (V) (10.15) - let reports = dispute::reports(&marks, &state.reports); - - // (W) the sequence of new available work reports (11.16); semantic only - let (available, assurances, a_triples) = self::assurance::available( - &reports, - if new_epoch { - &state.validators.previous - } else { - &state.validators.current - }, - block.header.parent, - &block.extrinsic.assurances, - )?; - - batch.extend(a_triples); - crypto::ed25519::SigItem::batch_verify(&batch)?; - (reports, reported, reporters, available, assurances) - }; - - // Round 2 computation: apply state effects now that sigs have been verified. - { - // (ρ‡) Update availability assignments based on assurances (11.17) - reports = assurance::reports(block.header.slot, &available, reports); - - // (ρ') Update availability assignments based on guarantees (11.43) - state.reports = - guarantee::reports(block.header.slot, &reports, &block.extrinsic.guarantees)?; - } - - // Round 3 computation - let (root, accounts) = { - // (γ') Update the sealing-key series (12.10) - if !block.extrinsic.tickets.is_empty() || new_epoch { - let _guard = timing::safrole(); - state.safrole = ticket::safrole( - state.timeslot, - block.header.slot, - state.entropy, - &state.disputes.offenders, - &state.safrole, - &state.validators, - &block.extrinsic.tickets, - )?; - - { - if new_epoch { - block.header.epoch_mark = state.safrole.epoch_mark(&state.entropy); - } - block.header.tickets_mark = state - .safrole - .tickets_mark(state.timeslot, block.header.slot); - } - } - - // (π') Update the statistic - state - .statistics - .update(new_epoch, block.header.author_index, &block.extrinsic)?; - state.statistics.merge_reports(&available, &assurances); - - // (..., C) Accumulate the available work reports - let _guard = timing::accumulate(); - let accumulation = guarantee::accumulate::( - block.header.slot, - state.timeslot, - available, - &state.queue, - &state.history, - &state.privileges, - &state.validators.drawn, - &state.authorization, - accounts, - state.entropy, - )?; - - // update state fields - state.privileges = accumulation.privileges; - state.queue = accumulation.ready_queue; - state.history = accumulation.accumulated_queue; - state.validators.drawn = accumulation.validators; - state.authorization = accumulation.authorization; - let candidate = state - .safrole - .next(&state.validators.drawn, &state.disputes.offenders); - thread::spawn(move || ticket::lazy::drawn(&candidate)); - - state.statistics.merge_services(accumulation.records); - state.logs = accumulation.logs; - (accumulation.root, accumulation.accounts) - }; - - // Round 4 computation - let mut diff = Commit::default(); - { - // (β') Update the block history - block::history::import( - &mut state.recent_blocks, - block.header.hash(), - root, - reported, - ); - - if !reporters.is_empty() { - state - .statistics - .merge_reporters(&reporters, &state.validators.current.ed25519())?; - } - - // (δ') Integrate preimages into the post-transfer state - let accounts = preimage::accounts(block.header.slot, &block.extrinsic.preimages, accounts); - let (updates, removals) = accounts.diff(); - diff.extend_iter(updates, removals); - - // (α') Update the authorization pools (12.13) - state.pools = guarantee::pools( - block.header.slot, - &state.pools, - &state.authorization, - &block.extrinsic.guarantees, - ); - - // (τ') Update the timeslot - state.timeslot = block.header.slot; - } - - diff.update.extend(state.pairs(new_epoch, &block.extrinsic)); - Ok(diff) + Executor::::new(block, state, storage).run() } From 334a00d620b9b182c9f1e5733f28cc7eb9c2ca41 Mon Sep 17 00:00:00 2001 From: clearloop Date: Fri, 22 May 2026 23:52:23 +0800 Subject: [PATCH 04/23] fix(erasure): correct the feature gate --- crates/codec/erasure/tests/consistency.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/codec/erasure/tests/consistency.rs b/crates/codec/erasure/tests/consistency.rs index d796572a..0326f38c 100644 --- a/crates/codec/erasure/tests/consistency.rs +++ b/crates/codec/erasure/tests/consistency.rs @@ -15,7 +15,12 @@ async fn ec_4096() -> anyhow::Result<()> { async fn run_codec(test: &str) -> anyhow::Result<()> { let registry = Registry::new(PathBuf::from("../../../res/jam-test-vectors")); - let test = registry.erasure(specjam::Scale::Tiny)?.test(test)?; + let scale = if cfg!(feature = "full") { + specjam::Scale::Full + } else { + 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() @@ -25,12 +30,19 @@ async fn run_codec(test: &str) -> anyhow::Result<()> { }) .collect::>>()?; + let n = erasure::Config::default().original; + let recovery_pairs = || -> Vec<(usize, Vec)> { + let mut pairs: Vec<_> = (0..n - 1).map(|i| (i, shards[i].clone())).collect(); + pairs.push((n, shards[n].clone())); + pairs + }; + // testing sync { let encoded = erasure::encode_sync(data.clone())?; assert_eq!(encoded, shards); - let decoded = erasure::decode_sync(vec![(0, shards[0].clone()), (2, shards[2].clone())])?; + let decoded = erasure::decode_sync(recovery_pairs())?; data.resize(decoded.len(), 0); assert_eq!(decoded, data); } @@ -40,7 +52,7 @@ async fn run_codec(test: &str) -> anyhow::Result<()> { let encoded = erasure::encode(data.clone()).await?; assert_eq!(encoded, shards); - let decoded = erasure::decode(vec![(0, shards[0].clone()), (2, shards[2].clone())]).await?; + let decoded = erasure::decode(recovery_pairs()).await?; data.resize(decoded.len(), 0); assert_eq!(decoded, data); } From b7ba35fc3fc8da3494db9656f225e06058225d9a Mon Sep 17 00:00:00 2001 From: clearloop Date: Sat, 23 May 2026 00:46:09 +0800 Subject: [PATCH 05/23] perf(runtime): incremental root for TestChain import/apply via MultiTree --- crates/runtime/src/storage/commit.rs | 53 +++++++++++ crates/runtime/src/tx/block/mod.rs | 127 +++++++++------------------ crates/spacejam/src/fuzz/target.rs | 2 +- crates/testing/src/seq.rs | 2 +- 4 files changed, 97 insertions(+), 87 deletions(-) diff --git a/crates/runtime/src/storage/commit.rs b/crates/runtime/src/storage/commit.rs index 0551d86b..244bd028 100644 --- a/crates/runtime/src/storage/commit.rs +++ b/crates/runtime/src/storage/commit.rs @@ -78,6 +78,59 @@ where } } +impl Commit { + /// Sorted, deduplicated union of all keys this commit touches. + pub fn dirty_keys(&self) -> Vec { + let mut keys: Vec = self + .update + .keys() + .copied() + .chain(self.removal.iter().copied()) + .collect(); + keys.sort_unstable(); + keys.dedup(); + keys + } +} + +impl Commit +where + Key: Ord + Copy, + Value: AsRef<[u8]>, +{ + /// Merge-walk `base` ∪ `self.update` in key order, skipping `self.removal`. + pub fn merge_with<'a>(&'a self, base: &'a BTreeMap) -> Vec<(Key, &'a [u8])> { + let mut kvs: Vec<(Key, &[u8])> = Vec::with_capacity(base.len() + self.update.len()); + let mut base_iter = base.iter(); + let mut diff_iter = self.update.iter(); + let mut b = base_iter.next(); + let mut d = diff_iter.next(); + while let Some((key, value)) = match (b, d) { + (Some((bk, _)), Some((dk, dv))) if dk <= bk => { + if dk == bk { + b = base_iter.next(); + } + d = diff_iter.next(); + Some((*dk, dv.as_ref())) + } + (Some((bk, bv)), _) => { + b = base_iter.next(); + Some((*bk, bv.as_ref())) + } + (None, Some((dk, dv))) => { + d = diff_iter.next(); + Some((*dk, dv.as_ref())) + } + (None, None) => None, + } { + if !self.removal.contains(&key) { + kvs.push((key, value)); + } + } + kvs + } +} + impl From<(U, R)> for Commit where U: IntoIterator, diff --git a/crates/runtime/src/tx/block/mod.rs b/crates/runtime/src/tx/block/mod.rs index f0adff44..2b4c55e2 100644 --- a/crates/runtime/src/tx/block/mod.rs +++ b/crates/runtime/src/tx/block/mod.rs @@ -6,7 +6,6 @@ use crate::{ tx, }; use anyhow::Result; -use crypto::merkle; use pvm::Pvm; use score::{Block, OpaqueHash, TrieKey, state::StateKeyLike}; use std::{ @@ -14,11 +13,13 @@ use std::{ sync::Arc, }; +pub mod header; +pub mod history; + /// Zero hash sentinel — `current_root` before [`TestChain::init`]. const EMPTY_ROOT: OpaqueHash = [0; 32]; -pub mod header; -pub mod history; +type Fork = Branch; /// DEVELOPMENT: process the block with given state storage. pub fn process(block: Block, storage: Arc) -> Result<()> { @@ -39,8 +40,6 @@ pub fn process(block: Block, storage: Arc) -> Result<()> } } -type Fork = Branch; - /// DEVELOPMENT: A test chain for processing fuzz blocks. pub struct TestChain { /// The finalized head of the chain. @@ -49,8 +48,9 @@ pub struct TestChain { /// The data of the chain. pub data: Arc, - /// The forks and their states (diff only). - pub forks: HashMap, + /// The forks and their states (diff overlay + the state root computed + /// at import time via [`MultiTree::apply`]). + pub forks: HashMap, /// The state root corresponding to `data`. Tracked incrementally via the /// multitree column so we can skip the O(N log N) full retrie per block. @@ -63,34 +63,38 @@ impl TestChain { self.finalized != [0; 32] } - /// Finalize a parent fork by committing its diff into self.data. - fn finalize_fork(&mut self, parent: OpaqueHash) -> anyhow::Result<()> { - if let Some(fork) = self.forks.remove(&parent) { + /// Compute and persist the post-block state root incrementally. + fn compute_fork_root(&self, commit: &Commit>) -> Result { + let prev = (self.current_root != EMPTY_ROOT).then_some(self.current_root); + let dirty = commit.dirty_keys(); + self.data + .with_data(|base| self.data.apply(prev, &commit.merge_with(base), &dirty))? + } + + /// Commit a fork's diff into `self.data` and release orphan siblings. + fn finalize_fork(&mut self, parent: OpaqueHash) -> Result<()> { + if let Some((fork, parent_root)) = self.forks.remove(&parent) { let commit = fork .commit .read() .map_err(|_| anyhow::anyhow!("lock poisoned"))? .clone(); - let dirty = collect_dirty_keys(&commit); self.data.commit(Column::State, commit)?; - let prev = (self.current_root != EMPTY_ROOT).then_some(self.current_root); - let new_root = self.data.with_data(|data| { - let kvs: Vec<(TrieKey, &[u8])> = - data.iter().map(|(k, v)| (*k, v.as_slice())).collect(); - self.data.apply(prev, &kvs, &dirty) - })??; - root::set(parent, new_root); - self.current_root = new_root; + // Release sibling trees that won't be finalized. + for (_, sibling_root) in self.forks.values() { + self.data.dereference_tree(*sibling_root)?; + } self.finalized = parent; + self.current_root = parent_root; self.forks.clear(); } Ok(()) } /// Import a new block to the chain. - pub fn import(&mut self, block: Block) -> anyhow::Result { + pub fn import(&mut self, block: Block) -> Result { let head = block.header.hash(); let parent = block.header.parent; @@ -102,21 +106,19 @@ impl TestChain { let guard = Arc::new(Branch::checkout(self.data.clone())); self::process::(block, guard.clone())?; - // Compute root from base HashMap + overlay diff (no clone of base) + // Compute the post-block root incrementally from the overlay diff. let state_root = { let commit = guard .commit .read() .map_err(|_| anyhow::anyhow!("lock poisoned"))?; - self.data - .with_data(|data| handle_root_with_diff(head, data, &commit))? + self.compute_fork_root(&commit)? }; + root::set(head, state_root); - // Store Branch (diff only) in forks - self.forks.insert( - head, - Arc::try_unwrap(guard).unwrap_or_else(|arc| (*arc).clone()), - ); + // Store Branch (diff only) + its root in forks + let fork = Arc::try_unwrap(guard).unwrap_or_else(|arc| (*arc).clone()); + self.forks.insert(head, (fork, state_root)); Ok(state_root) } @@ -130,12 +132,19 @@ impl TestChain { } /// Apply the block to the chain. - pub fn apply(&mut self, block: &Block, guard: Arc) { + pub fn apply(&mut self, block: &Block, guard: Arc) -> Result<()> { let head = block.header.hash(); - self.forks.insert( - head, - Arc::try_unwrap(guard).unwrap_or_else(|arc| (*arc).clone()), - ); + let state_root = { + let commit = guard + .commit + .read() + .map_err(|_| anyhow::anyhow!("lock poisoned"))?; + self.compute_fork_root(&commit)? + }; + root::set(head, state_root); + let fork = Arc::try_unwrap(guard).unwrap_or_else(|arc| (*arc).clone()); + self.forks.insert(head, (fork, state_root)); + Ok(()) } /// Initialize the chain with the given block. @@ -175,55 +184,3 @@ impl Default for TestChain { } } -/// Sorted, deduplicated union of the keys an overlay commit touches. -fn collect_dirty_keys(commit: &Commit>) -> Vec { - let mut keys: Vec = commit - .update - .keys() - .copied() - .chain(commit.removal.iter().copied()) - .collect(); - keys.sort_unstable(); - keys.dedup(); - keys -} - -/// Compute the state root from base data + overlay diff via merge-walk. -fn handle_root_with_diff( - head: OpaqueHash, - base: &BTreeMap>, - diff: &crate::storage::Commit>, -) -> OpaqueHash { - let mut kvs: Vec<(TrieKey, &[u8])> = Vec::with_capacity(base.len() + diff.update.len()); - let mut base_iter = base.iter(); - let mut diff_iter = diff.update.iter(); - let mut b = base_iter.next(); - let mut d = diff_iter.next(); - - while let Some((key, value)) = match (b, d) { - (Some((bk, _)), Some((dk, dv))) if dk <= bk => { - if dk == bk { - b = base_iter.next(); - } - d = diff_iter.next(); - Some((dk, dv.as_slice())) - } - (Some((bk, bv)), _) => { - b = base_iter.next(); - Some((bk, bv.as_slice())) - } - (None, Some((dk, dv))) => { - d = diff_iter.next(); - Some((dk, dv.as_slice())) - } - (None, None) => None, - } { - if !diff.removal.contains(key) { - kvs.push((*key, value)); - } - } - - let state_root = merkle::trie31(&kvs); - root::set(head, state_root); - state_root -} diff --git a/crates/spacejam/src/fuzz/target.rs b/crates/spacejam/src/fuzz/target.rs index 9f6e25b0..87a6e712 100644 --- a/crates/spacejam/src/fuzz/target.rs +++ b/crates/spacejam/src/fuzz/target.rs @@ -155,7 +155,7 @@ impl Target { pub fn get_state(&mut self, hash: OpaqueHash) -> Result<()> { let mut state = Vec::new(); let iter: Box, Vec)>>> = - if let Some(fork) = self.chain.forks.get(&hash) { + if let Some((fork, _)) = self.chain.forks.get(&hash) { Box::new(fork.iter(Column::State)?) } else { Box::new(self.chain.data.iter(Column::State)?) diff --git a/crates/testing/src/seq.rs b/crates/testing/src/seq.rs index c29987ef..022db3a7 100644 --- a/crates/testing/src/seq.rs +++ b/crates/testing/src/seq.rs @@ -32,7 +32,7 @@ impl Processor { }; if is_ok { - self.chain.apply(&block, data); + self.chain.apply(&block, data)?; } Ok(()) } From b5fa21c1e023acd0c8f34360d529fcb5b90329d3 Mon Sep 17 00:00:00 2001 From: clearloop Date: Sat, 23 May 2026 00:56:26 +0800 Subject: [PATCH 06/23] perf(runtime): batch-verify dispute sigs via SigItem at the executor --- crates/runtime/src/tx/dispute/mod.rs | 82 ++++++++++++++-------------- crates/runtime/src/tx/executor.rs | 3 +- crates/testing/src/disputes.rs | 6 +- 3 files changed, 49 insertions(+), 42 deletions(-) diff --git a/crates/runtime/src/tx/dispute/mod.rs b/crates/runtime/src/tx/dispute/mod.rs index 890c611d..8d022987 100644 --- a/crates/runtime/src/tx/dispute/mod.rs +++ b/crates/runtime/src/tx/dispute/mod.rs @@ -3,6 +3,7 @@ //! 1. update judgements on work-reports and validators (ψ) //! 2. update pending reports (ρ) use super::dispute; +use crypto::ed25519::SigItem; pub use error::{Error, Result}; use score::{ EPOCH_LENGTH, Ed25519Public, OpaqueHash, TimeSlot, VALIDATORS_COUNT, VALIDATORS_SUPER_MAJORITY, @@ -14,16 +15,17 @@ use std::collections::{BTreeMap, HashSet}; pub mod error; -/// (ψ) Update disputes verdicts and offenders +/// (ψ) Update disputes verdicts and offenders. pub fn disputes( timeslot: TimeSlot, kappa: &ValidatorsData, lambda: &ValidatorsData, psi: &DisputesRecords, extrinsic: &DisputesExtrinsic, -) -> Result<(DisputesRecords, DisputesRecords)> { +) -> Result<(DisputesRecords, DisputesRecords, Vec)> { let mut next_psi = psi.clone(); - let mut records = dispute::verdicts(timeslot, kappa, lambda, &extrinsic.verdicts)?; + let (mut records, mut triples) = + dispute::verdicts(timeslot, kappa, lambda, &extrinsic.verdicts)?; // get validators for the current slot let validators: HashSet = [kappa.ed25519(), lambda.ed25519()] @@ -32,12 +34,16 @@ pub fn disputes( .collect(); // handle culprits - let offenders = dispute::culprits(&validators, psi, &records.bad, &extrinsic.culprits)?; - records.offenders.extend(&offenders); + let (culprit_offenders, culprit_triples) = + dispute::culprits(&validators, psi, &records.bad, &extrinsic.culprits)?; + records.offenders.extend(&culprit_offenders); + triples.extend(culprit_triples); // handle faults - let offenders = dispute::faults(&validators, psi, &records.good, &extrinsic.faults)?; - records.offenders.extend(&offenders); + let (fault_offenders, fault_triples) = + dispute::faults(&validators, psi, &records.good, &extrinsic.faults)?; + records.offenders.extend(&fault_offenders); + triples.extend(fault_triples); // update psi { @@ -50,7 +56,7 @@ pub fn disputes( next_psi.offenders.sort(); } - Ok((next_psi, records)) + Ok((next_psi, records, triples)) } /// (ρ†) Update availability assignments based on verdicts (ψ') @@ -74,14 +80,15 @@ pub fn reports( next_assignments } -// Update goodset, badset, wonkyset based on verdicts +// Update goodset, badset, wonkyset based on verdicts; collect sig triples. fn verdicts( timeslot: TimeSlot, kappa: &ValidatorsData, lambda: &ValidatorsData, verdicts: &[Verdict], -) -> Result { +) -> Result<(DisputesRecords, Vec)> { let mut records = DisputesRecords::default(); + let mut triples: Vec = Vec::new(); let mut last_target: Option = None; for verdict in verdicts { if verdict.votes.len() != VALIDATORS_SUPER_MAJORITY as usize { @@ -107,33 +114,28 @@ fn verdicts( return Err(Error::BadJudgementAge); }; - let mut verify_items = Vec::with_capacity(verdict.votes.len()); for (index, judgement) in verdict.votes.iter().enumerate() { if index != judgement.index as usize { return Err(Error::JudgementsNotSortedUnique); } let message = if judgement.vote { - aye_message.as_slice() + aye_message.clone() } else { - nay_message.as_slice() + nay_message.clone() }; - verify_items.push(( + triples.push(SigItem { message, - judgement.signature, - validators[judgement.index as usize].ed25519, - )); + signature: judgement.signature, + key: validators[judgement.index as usize].ed25519, + }); if judgement.vote { aye += 1; } } - crypto::ed25519::batch_verify(&verify_items) - .inspect_err(|e| tracing::debug!("Invalid verdict signature: {e}")) - .map_err(|_| Error::BadSignature)?; - match aye { aye if aye == VALIDATORS_SUPER_MAJORITY => records.good.push(verdict.target), aye if aye == VALIDATORS_COUNT / 3 => records.wonky.push(verdict.target), @@ -145,31 +147,31 @@ fn verdicts( } } - Ok(records) + Ok((records, triples)) } -/// (ψ) Update offenders based on verdicts +/// (ψ) Update offenders based on culprits; collect sig triples. fn culprits( validators: &HashSet, records: &DisputesRecords, bad: &[OpaqueHash], culprits: &[Culprit], -) -> Result> { +) -> Result<(Vec, Vec)> { let mut last_culprit = None; let mut bad_verdicts = bad.iter().map(|v| (v, 0)).collect::>(); let mut offenders = vec![]; + let mut triples: Vec = Vec::new(); for culprit in culprits { if !validators.contains(&culprit.key) { return Err(Error::BadGuarantorKey); } - if let Err(e) = - crypto::ed25519::verify(&culprit.signature_message(), culprit.signature, culprit.key) - { - tracing::debug!("Invalid signature in culprit: {e}"); - return Err(Error::BadSignature); - } + triples.push(SigItem { + message: culprit.signature_message().to_vec(), + signature: culprit.signature, + key: culprit.key, + }); if records.good.contains(&culprit.target) || records.bad.contains(&culprit.target) @@ -204,19 +206,20 @@ fn culprits( return Err(Error::NotEnoughCulprits); } - Ok(offenders) + Ok((offenders, triples)) } -/// (ψ) Update offenders based on verdicts +/// (ψ) Update offenders based on faults; collect sig triples. fn faults( validators: &HashSet, records: &DisputesRecords, good: &[OpaqueHash], faults: &[Fault], -) -> Result> { +) -> Result<(Vec, Vec)> { let mut last_fault = None; let mut verdicts = good.iter().map(|v| (v, 0)).collect::>(); let mut new_offenders = vec![]; + let mut triples: Vec = Vec::new(); for fault in faults { if !validators.contains(&fault.key) { @@ -234,12 +237,11 @@ fn faults( return Err(Error::OffenderAlreadyReported); } - if let Err(e) = - crypto::ed25519::verify(&fault.singing_message(), fault.signature, fault.key) - { - tracing::debug!("Invalid signature in fault: {e}"); - return Err(Error::BadSignature); - } + triples.push(SigItem { + message: fault.singing_message(), + signature: fault.signature, + key: fault.key, + }); if let Some(last_fault) = last_fault && fault < last_fault @@ -266,5 +268,5 @@ fn faults( return Err(Error::NotEnoughFaults); } - Ok(new_offenders) + Ok((new_offenders, triples)) } diff --git a/crates/runtime/src/tx/executor.rs b/crates/runtime/src/tx/executor.rs index 15ebedfc..65ca4d19 100644 --- a/crates/runtime/src/tx/executor.rs +++ b/crates/runtime/src/tx/executor.rs @@ -101,13 +101,14 @@ impl<'a, Vm: Pvm, S: Storage> Executor<'a, Vm, S> { } DisputesRecords::default() } else { - let (next_psi, records) = dispute::disputes( + let (next_psi, records, triples) = dispute::disputes( self.state.timeslot, &self.state.validators.current, &self.state.validators.previous, &self.state.disputes, &self.block.extrinsic.disputes, )?; + crypto::ed25519::SigItem::batch_verify(&triples)?; self.state.disputes = next_psi; self.block.header.offenders_mark = records.offenders.clone(); records diff --git a/crates/testing/src/disputes.rs b/crates/testing/src/disputes.rs index d01bdb91..14d3f400 100644 --- a/crates/testing/src/disputes.rs +++ b/crates/testing/src/disputes.rs @@ -24,7 +24,11 @@ pub fn run(test: &specjam::Test) -> anyhow::Result<()> { &input.pre_state.lambda, &input.pre_state.psi, &input.input.disputes, - ); + ) + .and_then(|(next_psi, records, triples)| { + crypto::ed25519::SigItem::batch_verify(&triples).map_err(|_| Error::BadSignature)?; + Ok((next_psi, records)) + }); // check offenders mark assert_eq!( From 5aeb7d2c3eeca2b466adf66a2b6b68301ebcd40a Mon Sep 17 00:00:00 2001 From: clearloop Date: Sat, 23 May 2026 02:05:14 +0800 Subject: [PATCH 07/23] feat(pvm): dispatch PVM par_iter to NUMA-narrow rayon pool --- Cargo.lock | 1 + crates/runtime/src/tx/block/mod.rs | 1 - crates/runtime/src/tx/guarantee/exec.rs | 38 ++++---- crates/spacejam/bin/spacejam.rs | 27 +++--- crates/vm/compiler/Cargo.toml | 1 + crates/vm/compiler/src/compiler.rs | 12 ++- crates/vm/compiler/src/lib.rs | 1 + crates/vm/compiler/src/numa/linux.rs | 112 ++++++++++++++++++++++++ crates/vm/compiler/src/numa/mod.rs | 76 ++++++++++++++++ crates/vm/interpreter/src/pvmi.rs | 4 +- crates/vm/spacevm/src/lib.rs | 14 ++- crates/vm/src/invocation/mod.rs | 1 + crates/vm/src/lib.rs | 13 ++- 13 files changed, 265 insertions(+), 36 deletions(-) create mode 100644 crates/vm/compiler/src/numa/linux.rs create mode 100644 crates/vm/compiler/src/numa/mod.rs diff --git a/Cargo.lock b/Cargo.lock index c3e1e9c0..eeedb8e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2772,6 +2772,7 @@ dependencies = [ "libc", "postcard", "pvm", + "rayon", "serde", "serde_json", "spacejam-crypto", diff --git a/crates/runtime/src/tx/block/mod.rs b/crates/runtime/src/tx/block/mod.rs index 2b4c55e2..625cd8fb 100644 --- a/crates/runtime/src/tx/block/mod.rs +++ b/crates/runtime/src/tx/block/mod.rs @@ -183,4 +183,3 @@ impl Default for TestChain { } } } - diff --git a/crates/runtime/src/tx/guarantee/exec.rs b/crates/runtime/src/tx/guarantee/exec.rs index 8cd6e8fc..35e92519 100644 --- a/crates/runtime/src/tx/guarantee/exec.rs +++ b/crates/runtime/src/tx/guarantee/exec.rs @@ -111,24 +111,26 @@ pub fn parallel( let designate = context.privileges.designate; let validators_ref = &*validators; - let mut results = services - .par_iter() - .map(|service| { - let v = if *service == designate { - validators_ref.clone() - } else { - Default::default() - }; - let transfers = transfers - .par_iter() - .filter(|t| t.recipient == *service) - .cloned() - .collect(); - let result = - self::once::(context.clone(), v, transfers, reports, table, *service); - (*service, result) - }) - .collect::>>(); + let mut results = V::install(|| { + services + .par_iter() + .map(|service| { + let v = if *service == designate { + validators_ref.clone() + } else { + Default::default() + }; + let transfers = transfers + .par_iter() + .filter(|t| t.recipient == *service) + .cloned() + .collect(); + let result = + self::once::(context.clone(), v, transfers, reports, table, *service); + (*service, result) + }) + .collect::>>() + }); // Helper function R(o, a, b) from graypaper: if manager changed it (a != o), use a; else use b let r = |old: ServiceId, mgr: ServiceId, svc: ServiceId| -> ServiceId { diff --git a/crates/spacejam/bin/spacejam.rs b/crates/spacejam/bin/spacejam.rs index 5116feab..3c42af80 100644 --- a/crates/spacejam/bin/spacejam.rs +++ b/crates/spacejam/bin/spacejam.rs @@ -2,18 +2,27 @@ use spacejam::cmd::App; -#[cfg(feature = "dhat")] -#[global_allocator] -static ALLOC: dhat::Alloc = dhat::Alloc; - #[tokio::main] async fn main() { + let _ = spacevm::numa::init(); + #[cfg(feature = "dhat")] - { + dhat::init(); + + App::run().await; +} + +#[cfg(feature = "dhat")] +mod dhat { + use std::sync::Mutex; + + #[global_allocator] + static ALLOC: dhat::Alloc = dhat::Alloc; + + pub fn init() { // Leak the profiler so the signal handler task can drop it on Ctrl-C. - let profiler: &'static std::sync::Mutex> = Box::leak(Box::new( - std::sync::Mutex::new(Some(dhat::Profiler::new_heap())), - )); + let profiler: &'static Mutex> = + Box::leak(Box::new(Mutex::new(Some(dhat::Profiler::new_heap())))); tokio::spawn(async move { tokio::signal::ctrl_c().await.ok(); @@ -22,6 +31,4 @@ async fn main() { std::process::exit(0); }); } - - App::run().await; } diff --git a/crates/vm/compiler/Cargo.toml b/crates/vm/compiler/Cargo.toml index 59ae1890..df07fc89 100644 --- a/crates/vm/compiler/Cargo.toml +++ b/crates/vm/compiler/Cargo.toml @@ -10,6 +10,7 @@ dirs.workspace = true hex.workspace = true postcard.workspace = true pvm.workspace = true +rayon.workspace = true translator.workspace = true cranelift = { workspace = true, features = [ "native", diff --git a/crates/vm/compiler/src/compiler.rs b/crates/vm/compiler/src/compiler.rs index a515a7e5..db115701 100644 --- a/crates/vm/compiler/src/compiler.rs +++ b/crates/vm/compiler/src/compiler.rs @@ -2,13 +2,23 @@ use crate::{Memory, ModuleLike}; use pvm::{ - Argument, Invocation, Invoked, State, parser, + Argument, Invocation, Invoked, Pvm, State, parser, score::{Gas, OpaqueHash}, }; /// Cranelift JIT module builder pub struct Compiler; +impl Pvm for Compiler { + fn install(f: F) -> R + where + F: FnOnce() -> R + Send, + R: Send, + { + crate::numa::pool().install(f) + } +} + impl Invocation for Compiler { fn invoke2( mut ctx: X, diff --git a/crates/vm/compiler/src/lib.rs b/crates/vm/compiler/src/lib.rs index 596ad8a0..2d180d79 100644 --- a/crates/vm/compiler/src/lib.rs +++ b/crates/vm/compiler/src/lib.rs @@ -20,6 +20,7 @@ mod exec; pub mod host; pub mod memory; pub mod module; +pub mod numa; pub mod trap; #[cfg(target_os = "macos")] diff --git a/crates/vm/compiler/src/numa/linux.rs b/crates/vm/compiler/src/numa/linux.rs new file mode 100644 index 00000000..475d2076 --- /dev/null +++ b/crates/vm/compiler/src/numa/linux.rs @@ -0,0 +1,112 @@ +//! Linux NUMA detection. +//! +//! Picks the largest CCD-local CPU bucket from `/sys`, intersected with the +//! cgroup-allowed set. The process is left unpinned: the rayon "narrow" pool +//! pins its own workers via [`set_affinity`] so other parallelism (sig +//! batches, merkle, etc.) keeps using all allowed CPUs. + +use crate::numa::{NumaPlan, fallback}; +use std::collections::BTreeSet; + +/// Detect NUMA topology and pick a node for code-local execution. +pub fn detect() -> NumaPlan { + let allowed: BTreeSet = match get_allowed_cpus() { + Some(set) if !set.is_empty() => set, + _ => return fallback(), + }; + + let buckets: Vec<(u32, Vec)> = read_node_cpulists() + .into_iter() + .map(|(id, cpus)| { + let kept: Vec = cpus.into_iter().filter(|c| allowed.contains(c)).collect(); + (id, kept) + }) + .filter(|(_, cpus)| !cpus.is_empty()) + .collect(); + + if buckets.len() <= 1 { + let cpus: Vec = allowed.into_iter().collect(); + let num_threads = cpus.len().max(1); + return NumaPlan { + node: None, + cpus, + num_threads, + }; + } + + let (node, cpus) = buckets.into_iter().max_by_key(|(_, c)| c.len()).unwrap(); + let num_threads = cpus.len().max(1); + tracing::info!("numa: chose node {node} ({num_threads} cpus) for code-local execution"); + NumaPlan { + node: Some(node), + cpus, + num_threads, + } +} + +fn get_allowed_cpus() -> Option> { + let mut set: libc::cpu_set_t = unsafe { std::mem::zeroed() }; + let size = std::mem::size_of::(); + if unsafe { libc::sched_getaffinity(0, size, &mut set) } != 0 { + return None; + } + let mut allowed = BTreeSet::new(); + for cpu in 0..(libc::CPU_SETSIZE as usize) { + if unsafe { libc::CPU_ISSET(cpu, &set) } { + allowed.insert(cpu); + } + } + Some(allowed) +} + +fn read_node_cpulists() -> Vec<(u32, Vec)> { + let Ok(entries) = std::fs::read_dir("/sys/devices/system/node") else { + return Vec::new(); + }; + let mut nodes = Vec::new(); + for entry in entries.flatten() { + let name = entry.file_name(); + let name = name.to_string_lossy(); + let Some(rest) = name.strip_prefix("node") else { + continue; + }; + let Ok(id) = rest.parse::() else { + continue; + }; + let Ok(text) = std::fs::read_to_string(entry.path().join("cpulist")) else { + continue; + }; + nodes.push((id, parse_cpulist(text.trim()))); + } + nodes +} + +fn parse_cpulist(s: &str) -> Vec { + let mut out = Vec::new(); + for part in s.split(',') { + let part = part.trim(); + if part.is_empty() { + continue; + } + if let Some((a, b)) = part.split_once('-') { + if let (Ok(a), Ok(b)) = (a.parse::(), b.parse::()) { + out.extend(a..=b); + } + } else if let Ok(n) = part.parse::() { + out.push(n); + } + } + out +} + +pub(super) fn set_affinity(cpus: &[usize]) -> std::io::Result<()> { + let mut set: libc::cpu_set_t = unsafe { std::mem::zeroed() }; + for &cpu in cpus { + unsafe { libc::CPU_SET(cpu, &mut set) }; + } + let size = std::mem::size_of::(); + if unsafe { libc::sched_setaffinity(0, size, &set) } != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) +} diff --git a/crates/vm/compiler/src/numa/mod.rs b/crates/vm/compiler/src/numa/mod.rs new file mode 100644 index 00000000..6fdbceb3 --- /dev/null +++ b/crates/vm/compiler/src/numa/mod.rs @@ -0,0 +1,76 @@ +//! NUMA-aware process placement for the AOT compiler. + +use std::sync::OnceLock; + +#[cfg(target_os = "linux")] +mod linux; + +static PLAN: OnceLock = OnceLock::new(); +static NARROW: OnceLock = OnceLock::new(); + +/// Topology decision applied at startup. +#[derive(Debug, Clone)] +pub struct NumaPlan { + /// Chosen NUMA node, or `None` on UMA / non-Linux / on failure to pin. + pub node: Option, + /// CPUs the process is allowed to run on after pinning. + pub cpus: Vec, + /// Suggested worker count for thread pools (`cpus.len()`, never zero). + pub num_threads: usize, +} + +/// Detect topology, pin the process, cache and return the plan. +pub fn init() -> &'static NumaPlan { + PLAN.get_or_init(detect) +} + +/// Chosen NUMA node, if any. `None` before [`init`] runs. +pub fn chosen_node() -> Option { + PLAN.get().and_then(|p| p.node) +} + +/// Rayon pool whose workers are pinned to the chosen node's CPUs. +pub fn pool() -> &'static rayon::ThreadPool { + NARROW.get_or_init(|| { + let plan = init(); + let cpus = plan.cpus.clone(); + rayon::ThreadPoolBuilder::new() + .num_threads(plan.num_threads) + .thread_name(|i| format!("numa-narrow-{i}")) + .start_handler(move |_| pin_worker(&cpus)) + .build() + .expect("build narrow rayon pool") + }) +} + +#[cfg(target_os = "linux")] +fn pin_worker(cpus: &[usize]) { + if let Err(err) = linux::set_affinity(cpus) { + tracing::warn!("numa: narrow worker pin failed: {err}"); + } +} + +#[cfg(not(target_os = "linux"))] +fn pin_worker(_cpus: &[usize]) {} + +#[cfg(target_os = "linux")] +fn detect() -> NumaPlan { + linux::detect() +} + +#[cfg(not(target_os = "linux"))] +fn detect() -> NumaPlan { + fallback() +} + +/// Fallback NUMA plan for non-Linux platforms. +pub fn fallback() -> NumaPlan { + let num_threads = std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(1); + NumaPlan { + node: None, + cpus: (0..num_threads).collect(), + num_threads, + } +} diff --git a/crates/vm/interpreter/src/pvmi.rs b/crates/vm/interpreter/src/pvmi.rs index 97f40eb6..675037e7 100644 --- a/crates/vm/interpreter/src/pvmi.rs +++ b/crates/vm/interpreter/src/pvmi.rs @@ -5,7 +5,7 @@ use lru::LruCache; use parser::{program, reader::Offset, Instruction}; use pvm::{ score::{Gas, OpaqueHash}, - Argument, Invocation, Invoked, + Argument, Invocation, Invoked, Pvm, }; use std::{ num::NonZeroUsize, @@ -64,3 +64,5 @@ impl Invocation for Interpreter { Self::invoke(program, hash, ctx, gas, pc).expect("fix me later") } } + +impl Pvm for Interpreter {} diff --git a/crates/vm/spacevm/src/lib.rs b/crates/vm/spacevm/src/lib.rs index ef5d5407..eba27cec 100644 --- a/crates/vm/spacevm/src/lib.rs +++ b/crates/vm/spacevm/src/lib.rs @@ -4,10 +4,10 @@ use anyhow::Result; use lru::LruCache; pub use pvm; use pvm::{ - Argument, Invocation, Invoked, State, parser, + Argument, Invocation, Invoked, Pvm, State, parser, score::{Gas, OpaqueHash}, }; -pub use pvmc::{Artifact, Compiler, Memory, ModuleLike, SPACEJAM_CACHE_DIR}; +pub use pvmc::{Artifact, Compiler, Memory, ModuleLike, SPACEJAM_CACHE_DIR, numa}; pub use pvmi::Interpreter; use std::{ collections::BTreeSet, @@ -35,6 +35,16 @@ pub static SPACEVM_LOCKS: LazyLock>> = /// SpaceVM - JAM virtual machine pub struct SpaceVM; +impl Pvm for SpaceVM { + fn install(f: F) -> R + where + F: FnOnce() -> R + Send, + R: Send, + { + pvmc::numa::pool().install(f) + } +} + impl Invocation for SpaceVM { fn invoke2( mut ctx: X, diff --git a/crates/vm/src/invocation/mod.rs b/crates/vm/src/invocation/mod.rs index c35dc491..bba65d6a 100644 --- a/crates/vm/src/invocation/mod.rs +++ b/crates/vm/src/invocation/mod.rs @@ -348,3 +348,4 @@ pub trait Invocation { } impl Invocation for () {} +impl crate::Pvm for () {} diff --git a/crates/vm/src/lib.rs b/crates/vm/src/lib.rs index 024fda7c..e7187f89 100644 --- a/crates/vm/src/lib.rs +++ b/crates/vm/src/lib.rs @@ -38,6 +38,13 @@ pub const REGISTER_COUNT: usize = 13; pub const MAX_FUNCTIONS: usize = 512; /// The PVM interface -pub trait Pvm: Invocation {} - -impl Pvm for T where T: Invocation {} +pub trait Pvm: Invocation { + /// Run `f` on the worker pool if needed. + fn install(f: F) -> R + where + F: FnOnce() -> R + Send, + R: Send, + { + f() + } +} From 5b8d3daffb4e61365b0cbc7d75cbf0365f584e9e Mon Sep 17 00:00:00 2001 From: clearloop Date: Sat, 23 May 2026 02:08:42 +0800 Subject: [PATCH 08/23] feat(pvmc): hint AOT code pages with hugepage and mbind to node --- crates/vm/compiler/src/exec.rs | 1 + crates/vm/compiler/src/numa/linux.rs | 63 ++++++++++++++++++++++------ crates/vm/compiler/src/numa/mod.rs | 58 +++++++++++++++---------- 3 files changed, 88 insertions(+), 34 deletions(-) diff --git a/crates/vm/compiler/src/exec.rs b/crates/vm/compiler/src/exec.rs index f51d0235..a81a6986 100644 --- a/crates/vm/compiler/src/exec.rs +++ b/crates/vm/compiler/src/exec.rs @@ -90,6 +90,7 @@ impl Executable { return Err(anyhow::anyhow!("Failed to allocate memory")); } + crate::numa::hint_code_pages(self.memory, self.size); Ok(()) } diff --git a/crates/vm/compiler/src/numa/linux.rs b/crates/vm/compiler/src/numa/linux.rs index 475d2076..9b9cd229 100644 --- a/crates/vm/compiler/src/numa/linux.rs +++ b/crates/vm/compiler/src/numa/linux.rs @@ -1,9 +1,4 @@ //! Linux NUMA detection. -//! -//! Picks the largest CCD-local CPU bucket from `/sys`, intersected with the -//! cgroup-allowed set. The process is left unpinned: the rayon "narrow" pool -//! pins its own workers via [`set_affinity`] so other parallelism (sig -//! batches, merkle, etc.) keeps using all allowed CPUs. use crate::numa::{NumaPlan, fallback}; use std::collections::BTreeSet; @@ -44,6 +39,33 @@ pub fn detect() -> NumaPlan { } } +pub fn set_affinity(cpus: &[usize]) -> std::io::Result<()> { + let mut set: libc::cpu_set_t = unsafe { std::mem::zeroed() }; + for &cpu in cpus { + unsafe { libc::CPU_SET(cpu, &mut set) }; + } + let size = std::mem::size_of::(); + if unsafe { libc::sched_setaffinity(0, size, &set) } != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) +} + +/// Hint that the AOT code mapping should use huge pages, and bind its +/// not-yet-faulted pages to the chosen node (if any). +pub fn hint_code_pages(addr: *mut u8, size: usize) { + if unsafe { libc::madvise(addr.cast(), size, libc::MADV_HUGEPAGE) } != 0 { + tracing::warn!( + "numa: madvise(MADV_HUGEPAGE) on AOT code buffer failed: {}", + std::io::Error::last_os_error() + ); + } + + if let Some(node) = super::chosen_node() { + bind_to_node(addr, size, node); + } +} + fn get_allowed_cpus() -> Option> { let mut set: libc::cpu_set_t = unsafe { std::mem::zeroed() }; let size = std::mem::size_of::(); @@ -99,14 +121,29 @@ fn parse_cpulist(s: &str) -> Vec { out } -pub(super) fn set_affinity(cpus: &[usize]) -> std::io::Result<()> { - let mut set: libc::cpu_set_t = unsafe { std::mem::zeroed() }; - for &cpu in cpus { - unsafe { libc::CPU_SET(cpu, &mut set) }; +fn bind_to_node(addr: *mut u8, size: usize, node: u32) { + // MPOL_BIND from linux/mempolicy.h; not exposed by the libc crate. + const MPOL_BIND: libc::c_int = 2; + if node >= 64 { + tracing::warn!("numa: chosen node {node} out of mbind range, skipping"); + return; } - let size = std::mem::size_of::(); - if unsafe { libc::sched_setaffinity(0, size, &set) } != 0 { - return Err(std::io::Error::last_os_error()); + let mask: u64 = 1u64 << node; + let ret = unsafe { + libc::syscall( + libc::SYS_mbind, + addr, + size as libc::c_ulong, + MPOL_BIND, + &mask as *const u64, + 64u64, + 0u32, + ) + }; + if ret != 0 { + tracing::warn!( + "numa: mbind AOT code buffer to node {node} failed: {}", + std::io::Error::last_os_error() + ); } - Ok(()) } diff --git a/crates/vm/compiler/src/numa/mod.rs b/crates/vm/compiler/src/numa/mod.rs index 6fdbceb3..f823a8e8 100644 --- a/crates/vm/compiler/src/numa/mod.rs +++ b/crates/vm/compiler/src/numa/mod.rs @@ -1,4 +1,12 @@ //! NUMA-aware process placement for the AOT compiler. +//! +//! The process is left unpinned so default rayon (sig batches, merkle, etc.) +//! keeps using every cgroup-allowed CPU. [`pool`] returns a dedicated rayon +//! pool whose workers are pinned to one NUMA node — used by PVM dispatch so +//! AOT code-cache locality is preserved across nested `par_iter`s. +//! +//! Note: only the AOT ([`crate::exec::Executable`]) path is hinted; cranelift's +//! JIT manages its own code memory and bypasses [`hint_code_pages`]. use std::sync::OnceLock; @@ -6,7 +14,7 @@ use std::sync::OnceLock; mod linux; static PLAN: OnceLock = OnceLock::new(); -static NARROW: OnceLock = OnceLock::new(); +static POOL: OnceLock = OnceLock::new(); /// Topology decision applied at startup. #[derive(Debug, Clone)] @@ -31,38 +39,29 @@ pub fn chosen_node() -> Option { /// Rayon pool whose workers are pinned to the chosen node's CPUs. pub fn pool() -> &'static rayon::ThreadPool { - NARROW.get_or_init(|| { + POOL.get_or_init(|| { let plan = init(); let cpus = plan.cpus.clone(); rayon::ThreadPoolBuilder::new() .num_threads(plan.num_threads) - .thread_name(|i| format!("numa-narrow-{i}")) + .thread_name(|i| format!("numa-{i}")) .start_handler(move |_| pin_worker(&cpus)) .build() - .expect("build narrow rayon pool") + .expect("build numa rayon pool") }) } -#[cfg(target_os = "linux")] -fn pin_worker(cpus: &[usize]) { - if let Err(err) = linux::set_affinity(cpus) { - tracing::warn!("numa: narrow worker pin failed: {err}"); +/// Hint that an AOT code mmap should use huge pages and bind to the chosen +/// node. Call before the first byte is written. No-op on non-Linux. +pub fn hint_code_pages(addr: *mut u8, size: usize) { + #[cfg(target_os = "linux")] + linux::hint_code_pages(addr, size); + #[cfg(not(target_os = "linux"))] + { + let _ = (addr, size); } } -#[cfg(not(target_os = "linux"))] -fn pin_worker(_cpus: &[usize]) {} - -#[cfg(target_os = "linux")] -fn detect() -> NumaPlan { - linux::detect() -} - -#[cfg(not(target_os = "linux"))] -fn detect() -> NumaPlan { - fallback() -} - /// Fallback NUMA plan for non-Linux platforms. pub fn fallback() -> NumaPlan { let num_threads = std::thread::available_parallelism() @@ -74,3 +73,20 @@ pub fn fallback() -> NumaPlan { num_threads, } } + +fn pin_worker(cpus: &[usize]) { + #[cfg(target_os = "linux")] + if let Err(err) = linux::set_affinity(cpus) { + tracing::warn!("numa: worker pin failed: {err}"); + } + #[cfg(not(target_os = "linux"))] + let _ = cpus; +} + +fn detect() -> NumaPlan { + #[cfg(target_os = "linux")] + return linux::detect(); + + #[cfg(not(target_os = "linux"))] + fallback() +} From dec8fc4168ccd13cc521cb9069f75ee852894d7f Mon Sep 17 00:00:00 2001 From: clearloop Date: Sat, 23 May 2026 02:47:52 +0800 Subject: [PATCH 09/23] chore(runtime): use ref for commit diff --- crates/runtime/src/chain/finalizer.rs | 2 +- crates/runtime/src/chain/fork.rs | 4 ++-- crates/runtime/src/chain/importer.rs | 5 +++-- crates/runtime/src/storage/archive.rs | 4 ++-- crates/runtime/src/storage/branch.rs | 11 ++++++++--- crates/runtime/src/storage/kv.rs | 4 ++-- crates/runtime/src/tx/block/history.rs | 2 +- crates/runtime/src/tx/block/mod.rs | 4 ++-- crates/runtime/src/tx/mod.rs | 4 ++-- crates/spacejam/src/storage/parity.rs | 15 +++++++++------ 10 files changed, 32 insertions(+), 23 deletions(-) diff --git a/crates/runtime/src/chain/finalizer.rs b/crates/runtime/src/chain/finalizer.rs index b5886365..b3bb8b93 100644 --- a/crates/runtime/src/chain/finalizer.rs +++ b/crates/runtime/src/chain/finalizer.rs @@ -45,7 +45,7 @@ impl Chain { let mut finalized = BTreeSet::new(); while let Some((slot, (block, commit))) = chain.blocks.pop_first() { let head = block.header.head(); - self.state.commit(Column::State, commit.clone())?; + self.state.commit(Column::State, &commit)?; // finalize the block in storage let root = self.state.root()?; diff --git a/crates/runtime/src/chain/fork.rs b/crates/runtime/src/chain/fork.rs index 164cfc7e..95ff9c29 100644 --- a/crates/runtime/src/chain/fork.rs +++ b/crates/runtime/src/chain/fork.rs @@ -101,7 +101,7 @@ impl Fork { chain.insert(this.header.head()); blocks.insert(*slot, (this.clone(), commit.clone())); - branch.commit(Column::State, commit.clone())?; + branch.commit(Column::State, &commit)?; } // import the block @@ -144,7 +144,7 @@ impl Fork { tracing::trace!("transiting block"); let head = block.header.head(); let diff = tx::simulate::(&mut block.clone(), self.state.clone())?; - self.state.commit(Column::State, diff.clone())?; + self.state.commit(Column::State, &diff)?; tracing::info!( "imported block#{}@{}, previous block#{}@{}", block.header.slot, diff --git a/crates/runtime/src/chain/importer.rs b/crates/runtime/src/chain/importer.rs index aec55625..c64e3db8 100644 --- a/crates/runtime/src/chain/importer.rs +++ b/crates/runtime/src/chain/importer.rs @@ -3,7 +3,7 @@ use crate::{ Chain, Config, chain::fork::Fork, - storage::{Branch, Column, KVStorage, StateStorage, SyncStorage}, + storage::{Branch, Column, Commit, KVStorage, StateStorage, SyncStorage}, }; use anyhow::{Context, Result}; use score::{ @@ -158,7 +158,8 @@ impl Chain { } let root = self.state.root()?; - self.state.commit(Column::State, (kvs, vec![]).into())?; + self.state + .commit(Column::State, &Commit::from((kvs, vec![])))?; self.state.finalize( &Block { header, diff --git a/crates/runtime/src/storage/archive.rs b/crates/runtime/src/storage/archive.rs index de013d70..28fcdf10 100644 --- a/crates/runtime/src/storage/archive.rs +++ b/crates/runtime/src/storage/archive.rs @@ -18,7 +18,7 @@ pub trait ArchiveStorage: KVStorage + Send + Sync + 'static { commit.set(key, value); } - self.commit(Column::Archive, commit)?; + self.commit(Column::Archive, &commit)?; Ok(()) } } @@ -40,7 +40,7 @@ impl Archive { } impl KVStorage for Archive { - fn commit(&self, _column: Column, _commit: Commit>) -> Result<()> { + fn commit(&self, _column: Column, _commit: &Commit>) -> Result<()> { anyhow::bail!("commit is not allowed on archive") } diff --git a/crates/runtime/src/storage/branch.rs b/crates/runtime/src/storage/branch.rs index 7c35bd72..02dea0f4 100644 --- a/crates/runtime/src/storage/branch.rs +++ b/crates/runtime/src/storage/branch.rs @@ -37,14 +37,19 @@ impl Branch { } impl KVStorage for Branch { - fn commit(&self, _column: Column, new_commit: Commit>) -> Result<()> { + fn commit(&self, _column: Column, new_commit: &Commit>) -> Result<()> { let mut commit = self .commit .write() .map_err(|_| anyhow::anyhow!("Failed to acquire commit lock"))?; - // Merge the new commit with the existing one - commit.extend(new_commit); + // Merge the new commit with the existing oneqq + for (k, v) in new_commit.iset() { + commit.set(*k, v.clone()); + } + for k in new_commit.iremoval() { + commit.remove(*k); + } Ok(()) } diff --git a/crates/runtime/src/storage/kv.rs b/crates/runtime/src/storage/kv.rs index e7692540..d97c339c 100644 --- a/crates/runtime/src/storage/kv.rs +++ b/crates/runtime/src/storage/kv.rs @@ -12,7 +12,7 @@ use std::{ /// Key-value storage pub trait KVStorage: Send + Sync + 'static { /// Batch write a set of key-value pairs to the storage - fn commit(&self, column: Column, commit: Commit>) -> Result<()>; + fn commit(&self, column: Column, commit: &Commit>) -> Result<()>; /// Set a key-value pair with column specified fn set(&self, column: Column, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) -> Result<()>; @@ -67,7 +67,7 @@ impl MemoryDb { } impl KVStorage for MemoryDb { - fn commit(&self, _column: Column, commit: Commit>) -> Result<()> { + fn commit(&self, _column: Column, commit: &Commit>) -> Result<()> { let mut data = self .data .write() diff --git a/crates/runtime/src/tx/block/history.rs b/crates/runtime/src/tx/block/history.rs index 80c75099..4d21e9ae 100644 --- a/crates/runtime/src/tx/block/history.rs +++ b/crates/runtime/src/tx/block/history.rs @@ -23,7 +23,7 @@ pub fn import( reported, }; - history.history.push(new_block.clone()); + history.history.push(new_block); return; }; diff --git a/crates/runtime/src/tx/block/mod.rs b/crates/runtime/src/tx/block/mod.rs index 625cd8fb..08351b93 100644 --- a/crates/runtime/src/tx/block/mod.rs +++ b/crates/runtime/src/tx/block/mod.rs @@ -34,7 +34,7 @@ pub fn process(block: Block, storage: Arc) -> Result<()> match (vresult, sresult) { (Err(e), _) | (_, Err(e)) => Err(e), (Ok(()), Ok(diff)) => { - storage.commit(Column::State, diff)?; + storage.commit(Column::State, &diff)?; Ok(()) } } @@ -79,7 +79,7 @@ impl TestChain { .read() .map_err(|_| anyhow::anyhow!("lock poisoned"))? .clone(); - self.data.commit(Column::State, commit)?; + self.data.commit(Column::State, &commit)?; // Release sibling trees that won't be finalized. for (_, sibling_root) in self.forks.values() { diff --git a/crates/runtime/src/tx/mod.rs b/crates/runtime/src/tx/mod.rs index a7f40ca0..5cb610da 100644 --- a/crates/runtime/src/tx/mod.rs +++ b/crates/runtime/src/tx/mod.rs @@ -27,7 +27,7 @@ pub fn transit( ) -> Result>> { let diff = self::simulate::(&mut block, storage.clone())?; let _guard = timing::commit(); - storage.commit(Column::State, diff.clone())?; + storage.commit(Column::State, &diff)?; Ok(diff) } @@ -40,7 +40,7 @@ pub fn transit_with_state( ) -> Result>> { let diff = self::simulate_with_state::(&mut block, state, storage.clone())?; let _guard = timing::commit(); - storage.commit(Column::State, diff.clone())?; + storage.commit(Column::State, &diff)?; Ok(diff) } diff --git a/crates/spacejam/src/storage/parity.rs b/crates/spacejam/src/storage/parity.rs index 2a21e329..e8121b76 100644 --- a/crates/spacejam/src/storage/parity.rs +++ b/crates/spacejam/src/storage/parity.rs @@ -6,7 +6,7 @@ use parity_db::{ Options, }; use runtime::storage::{ - Column, Commit, KVStorage, MultiTree, NewNode, NodeAddress, NodeRef, Operation, + Column, Commit, KVStorage, MultiTree, NewNode, NodeAddress, NodeRef, }; use score::{OpaqueHash, TrieKey}; use std::path::PathBuf; @@ -17,11 +17,14 @@ const TRIE_COL: u8 = Column::TrieNodes as u8; pub struct Parity(Db); impl KVStorage for Parity { - fn commit(&self, column: Column, commit: Commit>) -> Result<()> { - self.0.commit_changes(commit.ops().map(|op| match op { - Operation::Set(k, v) => (column as u8, Op::Set(k.to_vec(), v)), - Operation::Remove(k) => (column as u8, Op::Dereference(k.to_vec())), - }))?; + fn commit(&self, column: Column, commit: &Commit>) -> Result<()> { + let sets = commit + .iset() + .map(|(k, v)| (column as u8, Op::Set(k.to_vec(), v.clone()))); + let removes = commit + .iremoval() + .map(|k| (column as u8, Op::Dereference(k.to_vec()))); + self.0.commit_changes(sets.chain(removes))?; Ok(()) } From cccc872976ed1fb8d86d5633e89a838555b9471e Mon Sep 17 00:00:00 2001 From: clearloop Date: Sat, 23 May 2026 03:12:12 +0800 Subject: [PATCH 10/23] perf(spacevm): check artifacts from disk when loading modules --- .cargo/config.toml | 1 + crates/vm/compiler/src/module/mod.rs | 6 ++++ crates/vm/compiler/src/module/object.rs | 31 ++++++++++++++++----- crates/vm/spacevm/src/lib.rs | 37 +++++++++++++++++++------ 4 files changed, 59 insertions(+), 16 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index ed5be05d..7da4a2c4 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -4,3 +4,4 @@ linker = "aarch64-linux-gnu-gcc" [target.x86_64-unknown-linux-gnu] linker = "x86_64-linux-gnu-gcc" +rustflags = ["-C", "target-cpu=x86-64-v3"] diff --git a/crates/vm/compiler/src/module/mod.rs b/crates/vm/compiler/src/module/mod.rs index 1f7c70bc..3ffce7ef 100644 --- a/crates/vm/compiler/src/module/mod.rs +++ b/crates/vm/compiler/src/module/mod.rs @@ -28,6 +28,12 @@ pub trait ModuleLike: Sized { /// Compile a program fn compile(self, program: &Program) -> Result; + /// Try to load a previously-saved artifact for `program` without + /// running codegen. + fn try_load(self, _program: &Program) -> Result> { + Ok(None) + } + /// Get the main function fn main(&self) -> Result>; diff --git a/crates/vm/compiler/src/module/object.rs b/crates/vm/compiler/src/module/object.rs index df777ac5..3861857f 100644 --- a/crates/vm/compiler/src/module/object.rs +++ b/crates/vm/compiler/src/module/object.rs @@ -17,6 +17,19 @@ pub struct ObjectModule { exec: Executable, } +impl ObjectModule { + /// On-disk artifact filename for the AOT object cache. + fn artifact_name(program: &Program) -> String { + let info = program.meta.info(); + format!( + "{}-{}-{}.o", + info.name, + info.version, + &hex::encode(crypto::blake3(program.code.as_ref()))[..6] + ) + } +} + impl ModuleLike for ObjectModule { fn new() -> Result { let isa = Engine::compilation()?; @@ -29,13 +42,7 @@ impl ModuleLike for ObjectModule { } fn compile(mut self, program: &Program) -> Result { - let info = program.meta.info(); - let name = format!( - "{}-{}-{}.o", - info.name, - info.version, - &hex::encode(crypto::blake3(program.code.as_ref()))[..6] - ); + let name = Self::artifact_name(program); if let Some(object) = Artifact::get("lib", &name) { self.exec.load::<()>(&object)?; return Ok(self); @@ -52,6 +59,16 @@ impl ModuleLike for ObjectModule { Ok(self) } + fn try_load(mut self, program: &Program) -> Result> { + match Artifact::get("lib", &Self::artifact_name(program)) { + Some(object) => { + self.exec.load::<()>(&object)?; + Ok(Some(self)) + } + None => Ok(None), + } + } + fn main(&self) -> Result> { let main = self.exec.get("main")?; Ok(unsafe { std::mem::transmute::>(main) }) diff --git a/crates/vm/spacevm/src/lib.rs b/crates/vm/spacevm/src/lib.rs index eba27cec..d4321f9d 100644 --- a/crates/vm/spacevm/src/lib.rs +++ b/crates/vm/spacevm/src/lib.rs @@ -16,17 +16,21 @@ use std::{ thread, }; -/// Maximum number of modules to keep in memory. Evicted modules remain on disk -/// and can be reloaded when needed. -const MAX_CACHED_MODULES: usize = 8; +/// Default max modules kept in memory. +const DEFAULT_MODULE_CACHE: usize = 8; + +/// Effective cache size. +fn module_cache_size() -> NonZeroUsize { + std::env::var("SPACEJAM_MODULE_CACHE") + .ok() + .and_then(|s| s.parse::().ok()) + .and_then(NonZeroUsize::new) + .unwrap_or_else(|| NonZeroUsize::new(DEFAULT_MODULE_CACHE).expect("nonzero default")) +} /// Cached modules (LRU). Uses Mutex because LruCache::get requires &mut for LRU tracking. pub static SPACEVM_MODULES: LazyLock>>> = - LazyLock::new(|| { - Mutex::new(LruCache::new( - NonZeroUsize::new(MAX_CACHED_MODULES).expect("MAX_CACHED_MODULES must be non-zero"), - )) - }); + LazyLock::new(|| Mutex::new(LruCache::new(module_cache_size()))); /// Locks for the Jastime compilation pub static SPACEVM_LOCKS: LazyLock>> = @@ -54,11 +58,26 @@ impl Invocation for SpaceVM { gas: Gas, pc: usize, ) -> Invoked { - if let Ok(true) = SPACEVM_LOCKS.read().map(|lock| !lock.contains(&hash)) + let module = if let Ok(true) = SPACEVM_LOCKS.read().map(|lock| !lock.contains(&hash)) && let Ok(Some(module)) = SPACEVM_MODULES .lock() .map(|mut cache| cache.get(&hash).cloned()) { + Some(module) + } else if let Ok(module) = ::new::() + && let Ok(program) = parser::program::preimage(code.clone(), &args) + && let Ok(Some(module)) = ModuleLike::try_load(module, &program) + { + let arc = Arc::new(module); + if let Ok(mut cache) = SPACEVM_MODULES.lock() { + cache.put(hash, arc.clone()); + } + Some(arc) + } else { + None + }; + + if let Some(module) = module { let program = parser::program::preimage(code, &args).expect("failed to preimage"); let mut context = pvm::Context { registers: program.registers, From 85264ebb6e65b66584d56f990126654c6c45d795 Mon Sep 17 00:00:00 2001 From: clearloop Date: Sat, 23 May 2026 03:39:36 +0800 Subject: [PATCH 11/23] perf(pvmc): local pool for guest memory mmap --- crates/vm/compiler/src/memory/mmap.rs | 67 +++++++++++++++++++++------ 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/crates/vm/compiler/src/memory/mmap.rs b/crates/vm/compiler/src/memory/mmap.rs index cb2519af..eb43146a 100644 --- a/crates/vm/compiler/src/memory/mmap.rs +++ b/crates/vm/compiler/src/memory/mmap.rs @@ -4,7 +4,11 @@ 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 std::{cell::RefCell, collections::BTreeMap, io, ptr}; + +thread_local! { + static POOL: RefCell> = const { RefCell::new(None) }; +} /// memory for PVM programs #[derive(Debug, Clone)] @@ -21,6 +25,23 @@ pub struct Memory { impl Memory { /// Create a new memory instance from parser Memory pub fn new(pmemory: &pvm::Memory) -> Result { + let region = match POOL.with(|p| p.borrow_mut().take()) { + Some(r) => r, + None => Self::mmap_region()?, + }; + let base = region.0; + std::mem::forget(region); + + let memory = Memory { + base, + heap_ptr: pmemory.heap_ptr, + }; + + memory.init(pmemory)?; + Ok(memory) + } + + fn mmap_region() -> Result { let base = unsafe { libc::mmap( ptr::null_mut(), @@ -31,21 +52,13 @@ impl Memory { 0, ) }; - if base == libc::MAP_FAILED { anyhow::bail!( "Failed to mmap virtual memory: {}", std::io::Error::last_os_error() ); } - - let memory = Memory { - base: base as *mut u8, - heap_ptr: pmemory.heap_ptr, - }; - - memory.init(pmemory)?; - Ok(memory) + Ok(Region(base as *mut u8)) } /// Initialize memory regions from parser memory @@ -172,11 +185,28 @@ impl Memory { impl Drop for Memory { fn drop(&mut self) { + if self.base.is_null() { + return; + } + + let size = pvm::PVM_MEMORY_SIZE as usize; unsafe { - if !self.base.is_null() { - libc::munmap(self.base as *mut _, pvm::PVM_MEMORY_SIZE as usize); - } + libc::madvise(self.base as *mut _, size, libc::MADV_DONTNEED); + libc::mprotect(self.base as *mut _, size, PROT_NONE); } + + let base = self.base; + self.base = ptr::null_mut(); + + POOL.with(|p| { + let mut slot = p.borrow_mut(); + if slot.is_none() { + *slot = Some(Region(base)); + } else { + // Slot already full; drop this region (munmaps via Region::drop). + drop(Region(base)); + } + }); } } @@ -235,3 +265,14 @@ impl MemoryLike for Memory { self.heap_ptr = heap_ptr; } } + +// Per-thread reusable 4 GB guest mapping. +struct Region(*mut u8); + +impl Drop for Region { + fn drop(&mut self) { + if !self.0.is_null() { + unsafe { libc::munmap(self.0 as *mut _, pvm::PVM_MEMORY_SIZE as usize) }; + } + } +} From 5984149c5d9a359342d5459fadbb3aaaf2f181f6 Mon Sep 17 00:00:00 2001 From: clearloop Date: Sat, 23 May 2026 04:06:40 +0800 Subject: [PATCH 12/23] refactor(spacejam): derive versions from Cargo.toml --- Cargo.toml | 3 ++ crates/spacejam/Cargo.toml | 1 + crates/spacejam/build.rs | 33 +++++++++++++- crates/spacejam/src/cmd/mod.rs | 2 +- crates/spacejam/src/fuzz/fuzzer.rs | 4 +- crates/spacejam/src/fuzz/message.rs | 67 ++++++++++++++++++++++------- crates/spacejam/src/fuzz/target.rs | 2 +- crates/spacejam/src/lib.rs | 2 - 8 files changed, 90 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d5aaccb6..98d938c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,9 @@ repository = "https://github.com/spacejamapp/jade" license = "GPL-3.0" authors = ["clearloop "] +[workspace.metadata.graypaper] +version = "0.7.2" + [profile.prod] inherits = "release" opt-level = 3 diff --git a/crates/spacejam/Cargo.toml b/crates/spacejam/Cargo.toml index ca745129..910ecc42 100644 --- a/crates/spacejam/Cargo.toml +++ b/crates/spacejam/Cargo.toml @@ -50,6 +50,7 @@ temp-dir.workspace = true syn.workspace = true proc-macro2.workspace = true quote.workspace = true +toml.workspace = true [features] default = ["tiny"] diff --git a/crates/spacejam/build.rs b/crates/spacejam/build.rs index 70832274..24d71b60 100644 --- a/crates/spacejam/build.rs +++ b/crates/spacejam/build.rs @@ -1,6 +1,6 @@ //! build script for spacejam -use std::{fs, process::Command}; +use std::{fs, path::Path, process::Command}; const TINY_DEV_SPEC: &str = "https://gist.githubusercontent.com/clearloop/52b9d5c16d3bd2a2d900b756fc64a9d1/raw/fbf84b774254cb68071a8a37cf8faac699bebf48/spec.json"; @@ -12,7 +12,36 @@ fn main() { std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set"), ); - let dev = root.join("spec/dev"); + self::emit_graypaper_version(&root); + self::fetch_tiny_dev_spec(&root); +} + +/// Read `[workspace.metadata.graypaper] version` from the root Cargo.toml +fn emit_graypaper_version(crate_root: &Path) { + let workspace_manifest = crate_root.join("../../Cargo.toml"); + println!("cargo:rerun-if-changed={}", workspace_manifest.display()); + + let text = fs::read_to_string(&workspace_manifest) + .expect("failed to read workspace Cargo.toml for graypaper version"); + let version = parse_graypaper_version(&text) + .expect("`[workspace.metadata.graypaper] version` missing from Cargo.toml"); + println!("cargo:rustc-env=GRAYPAPER_VERSION={version}"); +} + +fn parse_graypaper_version(manifest: &str) -> Option { + manifest + .parse::() + .ok()? + .get("workspace")? + .get("metadata")? + .get("graypaper")? + .get("version")? + .as_str() + .map(str::to_string) +} + +fn fetch_tiny_dev_spec(crate_root: &Path) { + let dev = crate_root.join("spec/dev"); let target = dev.join("spec.json"); if target.exists() { return; diff --git a/crates/spacejam/src/cmd/mod.rs b/crates/spacejam/src/cmd/mod.rs index 22dcbeeb..5952fc00 100644 --- a/crates/spacejam/src/cmd/mod.rs +++ b/crates/spacejam/src/cmd/mod.rs @@ -46,7 +46,7 @@ impl App { let app = App::parse(); if app.graypaper { - println!("graypaper: {}", crate::GRAYPAPER); + println!("graypaper: {}", crate::fuzz::message::Version::protocol()); return; } diff --git a/crates/spacejam/src/fuzz/fuzzer.rs b/crates/spacejam/src/fuzz/fuzzer.rs index 1a899718..fd07f757 100644 --- a/crates/spacejam/src/fuzz/fuzzer.rs +++ b/crates/spacejam/src/fuzz/fuzzer.rs @@ -185,10 +185,10 @@ impl Fuzzer { // check the remote peer info tracing::info!("Received peer info: {received:?}"); - if received.jam_version != Version::PROTOCOL { + if received.jam_version != Version::protocol() { anyhow::bail!( "Expected protocol: {:?}, got {:?}", - Version::PROTOCOL, + Version::protocol(), received.jam_version ); } diff --git a/crates/spacejam/src/fuzz/message.rs b/crates/spacejam/src/fuzz/message.rs index 653bfa30..becf658e 100644 --- a/crates/spacejam/src/fuzz/message.rs +++ b/crates/spacejam/src/fuzz/message.rs @@ -1,8 +1,9 @@ //! Fuzz messages +use anyhow::Context; use score::{Block, OpaqueHash, TimeSlot, TrieKey, block::Header}; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt::Display}; +use std::{collections::HashMap, fmt::Display, str::FromStr}; /// Messages used in the unix socket communication #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -83,8 +84,8 @@ impl Default for PeerInfo { fuzz_version: 1, // feature-ancestry (1) | feature-fork (2) — both [M1] mandatory fuzz_features: 3, - jam_version: Version::PROTOCOL, - app_version: Version::SPACEJAM, + jam_version: Version::protocol(), + app_version: Version::spacejam(), app_name: "spacejam".to_string(), } } @@ -104,19 +105,53 @@ pub struct Version { } impl Version { - /// The binary version of spacejam - pub const SPACEJAM: Version = Version { - major: 0, - minor: 1, - patch: 1, - }; - - /// The protocol version of spacejam - pub const PROTOCOL: Version = Version { - major: 0, - minor: 7, - patch: 2, - }; + /// Binary version, derived from `CARGO_PKG_VERSION_*` at compile time. + pub fn spacejam() -> Version { + Version { + major: env!("CARGO_PKG_VERSION_MAJOR") + .parse() + .expect("CARGO_PKG_VERSION_MAJOR not a u8"), + minor: env!("CARGO_PKG_VERSION_MINOR") + .parse() + .expect("CARGO_PKG_VERSION_MINOR not a u8"), + patch: env!("CARGO_PKG_VERSION_PATCH") + .parse() + .expect("CARGO_PKG_VERSION_PATCH not a u8"), + } + } + + /// JAM protocol version, sourced from `[workspace.metadata.graypaper]` + /// in the workspace manifest via the build script. + pub fn protocol() -> Version { + env!("GRAYPAPER_VERSION") + .parse() + .expect("invalid graypaper version") + } +} + +impl Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +impl FromStr for Version { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut parts = s.split('.'); + let major = parts.next().context("missing major")?.parse()?; + let minor = parts.next().context("missing minor")?.parse()?; + let patch = parts.next().context("missing patch")?.parse()?; + if parts.next().is_some() { + anyhow::bail!("version has too many components"); + } + Ok(Self { + major, + minor, + patch, + }) + } } /// A key-value pair diff --git a/crates/spacejam/src/fuzz/target.rs b/crates/spacejam/src/fuzz/target.rs index 87a6e712..e30a69f4 100644 --- a/crates/spacejam/src/fuzz/target.rs +++ b/crates/spacejam/src/fuzz/target.rs @@ -110,7 +110,7 @@ impl Target { /// Received info request pub fn info(&mut self, info: PeerInfo) -> anyhow::Result<()> { let this = PeerInfo::default(); - if info.jam_version != Version::PROTOCOL { + if info.jam_version != Version::protocol() { anyhow::bail!( "protocol version mismatched, remote: {:?}, local: {:?}", info.jam_version, diff --git a/crates/spacejam/src/lib.rs b/crates/spacejam/src/lib.rs index 88c53783..41eaa506 100644 --- a/crates/spacejam/src/lib.rs +++ b/crates/spacejam/src/lib.rs @@ -11,8 +11,6 @@ pub mod storage; mod utils; pub mod validator; -/// The version of matched graypaper -pub const GRAYPAPER: &str = "0.7.1"; /// The config of development pub struct Development; From cccf975b7381c179f8aad09b3c8820702f394d5b Mon Sep 17 00:00:00 2001 From: clearloop Date: Sat, 23 May 2026 04:31:29 +0800 Subject: [PATCH 13/23] chore(deps): bump cranelift --- Cargo.lock | 197 ++++++++++-------- Cargo.toml | 4 +- crates/vm/compiler/src/engine.rs | 2 - ...pacejam.Dockerfile => spacejam.dockerfile} | 1 + 4 files changed, 111 insertions(+), 93 deletions(-) rename docker/{spacejam.Dockerfile => spacejam.dockerfile} (97%) diff --git a/Cargo.lock b/Cargo.lock index eeedb8e1..3fb8c1fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,9 +103,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbitrary" @@ -604,9 +604,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" dependencies = [ "allocator-api2", ] @@ -823,9 +823,9 @@ dependencies = [ [[package]] name = "cranelift" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb56989ef9361e99f5702c97f343bd64d3819f02ac4ffb6aea48af3cb21a634" +checksum = "f2948a18918ec81f4701e0f94d6b1727ec2cf485f8e32d332eddc466886a279d" dependencies = [ "cranelift-codegen", "cranelift-frontend", @@ -837,46 +837,48 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0920ef6863433fa28ece7e53925be4cd39a913adba2dc3738f4edd182f76d168" +checksum = "8c80cf55a351448317210f26c434be761bcb25e7b36116ec92f89540b73e2833" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8990a217e2529a378af1daf4f8afa889f928f07ebbde6ae2f058ae60e40e2c20" +checksum = "07937ca8617b340162fe3a4716be885b5847e9b56d6c7a89abbe4d42340fdc91" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62225596b687f69a42c038485a28369badc186cb7c74bd9436eeec9f539011b1" +checksum = "88217b08180882436d54c0133274885c590698ae854e352bede1cda041230800" dependencies = [ "cranelift-entity", + "wasmtime-internal-core", ] [[package]] name = "cranelift-bitset" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c23914fc4062558650a6f0d8c1846c97b541215a291fdeabc85f68bdc9bbcca3" +checksum = "d5c3cf7ba29fa56e56040848e34835d4e45988b2760ef212413409af95ffd8c1" dependencies = [ "serde", "serde_derive", + "wasmtime-internal-core", ] [[package]] name = "cranelift-codegen" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a238b2f7e7ec077eb170145fa15fd8b3d0f36cc83d8e354e29ca550f339ca7" +checksum = "ebe1aac2efd4cba2047845fce38a68519935a30e20c8a6294ba7e2f448fe722d" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -887,8 +889,9 @@ dependencies = [ "cranelift-control", "cranelift-entity", "cranelift-isle", - "gimli 0.32.3", - "hashbrown 0.15.5", + "gimli 0.33.0", + "hashbrown 0.17.1", + "libm", "log", "postcard", "regalloc2", @@ -898,14 +901,14 @@ dependencies = [ "sha2", "smallvec", "target-lexicon", - "wasmtime-internal-math", + "wasmtime-internal-core", ] [[package]] name = "cranelift-codegen-meta" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9315ddcc2512513a9d66455ec89bb70ae5498cb472f5ed990230536f4cd5c011" +checksum = "0909eaf9d6f18f5bf802d50608cb4368ac340fbd03cc44f2888d1cfcc3faa64e" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -915,35 +918,36 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6acea40ef860f28cb36eaad479e26556c1e538b0a66fc44598cf1b1689393d" +checksum = "c95a8da8be283f49cda7d0ef228c94f10d791e517b27b0c7e282dadd2e79ce45" [[package]] name = "cranelift-control" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b2af895da90761cfda4a4445960554fcec971e637882eda5a87337d993fe1b9" +checksum = "f5b19c81145146da1f7afda2e7f52111842fe6793512e740ad5cf3f5639e6212" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e8c542c856feb50d504e4fc0526b3db3a514f882a9f68f956164531517828ab" +checksum = "4a55309b47e6633ab05821304206cb1e92952e845b1224985562bb7ac1e92323" dependencies = [ "cranelift-bitset", "serde", "serde_derive", + "wasmtime-internal-core", ] [[package]] name = "cranelift-frontend" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9996dd9c20929c03360fe0c4edf3594c0cbb94525bdbfa04b6bb639ec14573c7" +checksum = "064d2d3533d9608f1cf44c8899cf2f7f33feb70300b0fb83e687b0d9e7b91147" dependencies = [ "cranelift-codegen", "log", @@ -953,15 +957,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928b8dccad51b9e0ffe54accbd617da900239439b13d48f0f122ab61105ca6ad" +checksum = "1ac4e0bc095b2dab2212d1e99d7a74b62afc1485db023f1c0cb34a68758f7bd1" [[package]] name = "cranelift-jit" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d332429130ebdd9ee1ed97d2db3684fadbff27556df08d9f346361c22be5972" +checksum = "5b48c2a0720c7d62aadd508c662b9bf666b614a47a888589e553e0511620635e" dependencies = [ "anyhow", "cranelift-codegen", @@ -971,17 +975,18 @@ dependencies = [ "cranelift-native", "libc", "log", + "memmap2 0.2.3", "region", "target-lexicon", "wasmtime-internal-jit-icache-coherence", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "cranelift-module" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc01757dd26534ecbf14dbd183e7430daf3db2d86558f8419fea8726261beed" +checksum = "28f05d9efce7a4e8c2ceec49c76d26e53f1ee8cb13de822b6ca5118d48f50976" dependencies = [ "anyhow", "cranelift-codegen", @@ -990,9 +995,9 @@ dependencies = [ [[package]] name = "cranelift-native" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f75ef0a6a2efed3a2a14812318e28dc82c214eab5399c13d70878e2f88947b5" +checksum = "09a40053f5cb925451dd1d57393d14ad3145c8e0786701c27b5415ebb9a3ba4f" dependencies = [ "cranelift-codegen", "libc", @@ -1001,16 +1006,16 @@ dependencies = [ [[package]] name = "cranelift-object" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede9e63983baa4c2530dd2ab7ee25b171c6acf0414c00e1f1899606aa8b26d40" +checksum = "5f7a263727954f7b310796e1b5543e6dfd6afed7e15c62f2454b51b6f38a39e1" dependencies = [ "anyhow", "cranelift-codegen", "cranelift-control", "cranelift-module", "log", - "object 0.37.3", + "object 0.39.1", "target-lexicon", ] @@ -1030,9 +1035,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673bd6d1c83cb41d60afb140a1474ef6caf1a3e02f3820fc522aefbc93ac67d6" +checksum = "a3ceab9a53f7d362c89841fbaa8e63e44d47c40e91dc96ee6f777fca5d6b323b" [[package]] name = "crc32fast" @@ -1247,7 +1252,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.0", + "windows-sys 0.60.2", ] [[package]] @@ -1356,12 +1361,6 @@ dependencies = [ "typeid", ] -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - [[package]] name = "fastbloom" version = "0.14.0" @@ -1571,11 +1570,12 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gimli" -version = "0.32.3" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" dependencies = [ - "fallible-iterator", + "fnv", + "hashbrown 0.16.1", "indexmap", "stable_deref_trait", ] @@ -1702,6 +1702,15 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] + [[package]] name = "heapless" version = "0.7.17" @@ -1950,12 +1959,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", ] [[package]] @@ -1977,7 +1986,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.0", + "windows-sys 0.60.2", ] [[package]] @@ -2230,9 +2239,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -2246,9 +2255,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" @@ -2340,6 +2349,15 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "memmap2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723e3ebdcdc5c023db1df315364573789f8857c11b631a2fdfad7c00f5c046b4" +dependencies = [ + "libc", +] + [[package]] name = "memmap2" version = "0.9.8" @@ -2494,12 +2512,12 @@ dependencies = [ [[package]] name = "object" -version = "0.37.3" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" dependencies = [ "crc32fast", - "hashbrown 0.15.5", + "hashbrown 0.17.1", "indexmap", "memchr", ] @@ -2557,7 +2575,7 @@ dependencies = [ "libc", "log", "lz4", - "memmap2", + "memmap2 0.9.8", "parking_lot", "rand 0.9.2", "siphasher", @@ -2995,13 +3013,13 @@ dependencies = [ [[package]] name = "regalloc2" -version = "0.12.2" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5216b1837de2149f8bc8e6d5f88a9326b63b8c836ed58ce4a0a29ec736a59734" +checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" dependencies = [ "allocator-api2", "bumpalo", - "hashbrown 0.15.5", + "hashbrown 0.17.1", "log", "rustc-hash 2.1.1", "serde", @@ -3239,7 +3257,7 @@ version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -3285,9 +3303,9 @@ checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" [[package]] name = "serde" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -3303,18 +3321,18 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -4492,24 +4510,25 @@ dependencies = [ ] [[package]] -name = "wasmtime-internal-jit-icache-coherence" -version = "36.0.2" +name = "wasmtime-internal-core" +version = "45.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71aeb74f9b3fd9225319c723e59832a77a674b0c899ba9795f9b2130a6d1b167" +checksum = "1bdae4b55b15a23d774b15f6e7cd90ae0d0aa17c47c12b4db098b3dd11ba9d58" dependencies = [ - "anyhow", - "cfg-if", - "libc", - "windows-sys 0.60.2", + "hashbrown 0.17.1", + "libm", ] [[package]] -name = "wasmtime-internal-math" -version = "36.0.2" +name = "wasmtime-internal-jit-icache-coherence" +version = "45.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31d5dad8a609c6cc47a5f265f13b52e347e893450a69641af082b8a276043fa7" +checksum = "8a312ba8bb77955dcd44294a223e7f124c3071ff966583d385d3f6a4639c62e3" dependencies = [ - "libm", + "cfg-if", + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", ] [[package]] @@ -4572,7 +4591,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.60.2", ] [[package]] @@ -4726,9 +4745,9 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link 0.2.1", ] diff --git a/Cargo.toml b/Cargo.toml index 98d938c0..cf86e5a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,8 +86,8 @@ blake3 = "1.8.2" cc = "1.2" clap = "4.5.47" colored = "3.0.0" -cranelift = "0.123.2" -cranelift-codegen = "0.123.2" +cranelift = "0.132.0" +cranelift-codegen = "0.132.0" dhat = "0.3" dirs = "6.0.0" ed25519-zebra = "4.1.0" diff --git a/crates/vm/compiler/src/engine.rs b/crates/vm/compiler/src/engine.rs index 30ff7f66..dbda75ec 100644 --- a/crates/vm/compiler/src/engine.rs +++ b/crates/vm/compiler/src/engine.rs @@ -19,7 +19,6 @@ impl Engine { builder.set("enable_incremental_compilation_cache_checks", "false")?; builder.set("unwind_info", "false")?; builder.set("machine_code_cfg_info", "false")?; - builder.set("enable_pcc", "false")?; // Create the ISA builder and finish it with the flags let isa_builder = native::builder().map_err(|e| anyhow::anyhow!("{}", e))?; @@ -38,7 +37,6 @@ impl Engine { builder.set("enable_incremental_compilation_cache_checks", "false")?; builder.set("unwind_info", "false")?; builder.set("machine_code_cfg_info", "false")?; - builder.set("enable_pcc", "false")?; // Create the ISA builder and finish it with the flags let isa_builder = native::builder().map_err(|e| anyhow::anyhow!("{}", e))?; diff --git a/docker/spacejam.Dockerfile b/docker/spacejam.dockerfile similarity index 97% rename from docker/spacejam.Dockerfile rename to docker/spacejam.dockerfile index 144e3f3f..207a81d5 100644 --- a/docker/spacejam.Dockerfile +++ b/docker/spacejam.dockerfile @@ -18,4 +18,5 @@ COPY target/x86_64-unknown-linux-gnu/prod/spacejam-tiny /usr/local/bin/spacejam- COPY target/x86_64-unknown-linux-gnu/prod/spacejam-full /usr/local/bin/spacejam-full COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh +ENV SPACEJAM_MODULE_CACHE=64 ENTRYPOINT ["entrypoint.sh"] From a3405b38577529a8c0a82cdd56b258dcf9664dcd Mon Sep 17 00:00:00 2001 From: clearloop Date: Sat, 23 May 2026 04:33:57 +0800 Subject: [PATCH 14/23] chore: make clippy happy --- Makefile | 2 +- crates/runtime/src/chain/fork.rs | 2 +- crates/spacejam/src/lib.rs | 1 - crates/spacejam/src/storage/parity.rs | 4 +--- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index dd2a49b4..e0eac7d9 100644 --- a/Makefile +++ b/Makefile @@ -65,7 +65,7 @@ linux-amd64-both: # build the docker image, tagging both :latest and :$(VERSION) docker: linux-amd64-both docker build --platform=linux/amd64 \ - -f docker/spacejam.Dockerfile \ + -f docker/spacejam.dockerfile \ -t $(DOCKER_IMAGE):latest \ -t $(DOCKER_IMAGE):$(VERSION) \ . diff --git a/crates/runtime/src/chain/fork.rs b/crates/runtime/src/chain/fork.rs index 95ff9c29..ca59e38c 100644 --- a/crates/runtime/src/chain/fork.rs +++ b/crates/runtime/src/chain/fork.rs @@ -101,7 +101,7 @@ impl Fork { chain.insert(this.header.head()); blocks.insert(*slot, (this.clone(), commit.clone())); - branch.commit(Column::State, &commit)?; + branch.commit(Column::State, commit)?; } // import the block diff --git a/crates/spacejam/src/lib.rs b/crates/spacejam/src/lib.rs index 41eaa506..a8cbd2c8 100644 --- a/crates/spacejam/src/lib.rs +++ b/crates/spacejam/src/lib.rs @@ -11,7 +11,6 @@ pub mod storage; mod utils; pub mod validator; - /// The config of development pub struct Development; diff --git a/crates/spacejam/src/storage/parity.rs b/crates/spacejam/src/storage/parity.rs index e8121b76..2014652b 100644 --- a/crates/spacejam/src/storage/parity.rs +++ b/crates/spacejam/src/storage/parity.rs @@ -5,9 +5,7 @@ use parity_db::{ BTreeIterator, ColumnOptions, Db, NewNode as PdNewNode, NodeRef as PdNodeRef, Operation as Op, Options, }; -use runtime::storage::{ - Column, Commit, KVStorage, MultiTree, NewNode, NodeAddress, NodeRef, -}; +use runtime::storage::{Column, Commit, KVStorage, MultiTree, NewNode, NodeAddress, NodeRef}; use score::{OpaqueHash, TrieKey}; use std::path::PathBuf; From 1a865288039492b01a62764cf25fe5b262e87161 Mon Sep 17 00:00:00 2001 From: clearloop Date: Sat, 23 May 2026 10:30:00 +0800 Subject: [PATCH 15/23] fix(runtime): gts for tickets checking in block verification --- crates/runtime/src/tx/block/header.rs | 7 +++++-- crates/runtime/src/tx/block/mod.rs | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/runtime/src/tx/block/header.rs b/crates/runtime/src/tx/block/header.rs index 4cd47f2d..afea4d3f 100644 --- a/crates/runtime/src/tx/block/header.rs +++ b/crates/runtime/src/tx/block/header.rs @@ -30,7 +30,10 @@ pub fn validate(state: State, header: &Header) -> anyhow::Result<()> { }; // check the ticket mark - if new_epoch && state.safrole.accumulator.len() == score::EPOCH_LENGTH as usize { + if new_epoch + && state.timeslot % score::EPOCH_LENGTH >= score::TICKET_SUBMISSION_PERIOD + && state.safrole.accumulator.len() == score::EPOCH_LENGTH as usize + { let mut tickets = [TicketBody::default(); score::EPOCH_LENGTH as usize]; tickets.copy_from_slice(&TicketBody::sequence(&state.safrole.accumulator)); ticket = Some(tickets[slot]); @@ -182,7 +185,7 @@ pub fn check(state: &State, header: &Header, new_epoch: bool) -> anyhow::Result< // Validate ticket attempts for ticket in tickets_mark { - if ticket.attempt > score::TICKET_ENTRIES_PER_VALIDATOR as u8 { + if ticket.attempt >= score::TICKET_ENTRIES_PER_VALIDATOR as u8 { anyhow::bail!("invalid ticket attempt {}", ticket.attempt); } } diff --git a/crates/runtime/src/tx/block/mod.rs b/crates/runtime/src/tx/block/mod.rs index 08351b93..96d1d412 100644 --- a/crates/runtime/src/tx/block/mod.rs +++ b/crates/runtime/src/tx/block/mod.rs @@ -97,7 +97,6 @@ impl TestChain { pub fn import(&mut self, block: Block) -> Result { let head = block.header.hash(); let parent = block.header.parent; - if self.forks.contains_key(&parent) { self.finalize_fork(parent)?; } From f98a1f7a3621b8715b0b8a2011017a65cd063369 Mon Sep 17 00:00:00 2001 From: clearloop Date: Sat, 23 May 2026 16:02:17 +0800 Subject: [PATCH 16/23] chore(spacejam): kickout testing from spacejam default build --- crates/spacejam/Cargo.toml | 3 ++- crates/spacejam/src/cmd/fuzz.rs | 8 +++++++- crates/spacejam/src/fuzz/mod.rs | 5 ++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/spacejam/Cargo.toml b/crates/spacejam/Cargo.toml index 910ecc42..cdebbb8e 100644 --- a/crates/spacejam/Cargo.toml +++ b/crates/spacejam/Cargo.toml @@ -29,7 +29,7 @@ serde.workspace = true serde_json.workspace = true spacejson.workspace = true sysinfo.workspace = true -testing.workspace = true +testing = { workspace = true, optional = true } time = { workspace = true, features = ["local-offset"] } toml.workspace = true tokio = { workspace = true, features = [ @@ -57,3 +57,4 @@ default = ["tiny"] dhat = ["dep:dhat"] tiny = ["score/tiny"] full = ["score/full"] +trace = ["dep:testing"] diff --git a/crates/spacejam/src/cmd/fuzz.rs b/crates/spacejam/src/cmd/fuzz.rs index 27ab0bfd..7ef4031e 100644 --- a/crates/spacejam/src/cmd/fuzz.rs +++ b/crates/spacejam/src/cmd/fuzz.rs @@ -1,6 +1,8 @@ //! Fuzz related commands -use crate::fuzz::{self, fuzzer::Fuzzer, target::Target}; +use crate::fuzz::target::Target; +#[cfg(feature = "trace")] +use crate::fuzz::{self, fuzzer::Fuzzer}; use clap::Parser; use std::path::PathBuf; @@ -19,6 +21,7 @@ pub enum Fuzz { }, /// Fuzz with a fuzzer + #[cfg(feature = "trace")] Fuzzer { /// The path to the fuzzer #[clap(default_value = "/tmp/jam_target.sock")] @@ -42,6 +45,7 @@ pub enum Fuzz { }, /// Run trace test via the given trace file + #[cfg(feature = "trace")] Tx { /// The path to the trace file test: PathBuf, @@ -53,6 +57,7 @@ impl Fuzz { pub async fn run(&self) -> anyhow::Result<()> { match self { Self::Target { socket, interp } => Target::serve(socket, *interp).await, + #[cfg(feature = "trace")] Self::Fuzzer { socket, traces, @@ -68,6 +73,7 @@ impl Fuzz { Fuzzer::run(socket, traces, report) } } + #[cfg(feature = "trace")] Self::Tx { test } => fuzz::trace::test(test).await, } } diff --git a/crates/spacejam/src/fuzz/mod.rs b/crates/spacejam/src/fuzz/mod.rs index 7b1bfa86..3793c8e9 100644 --- a/crates/spacejam/src/fuzz/mod.rs +++ b/crates/spacejam/src/fuzz/mod.rs @@ -8,10 +8,13 @@ use std::{ }; pub mod env; -pub mod fuzzer; pub mod init; pub mod message; pub mod target; + +#[cfg(feature = "trace")] +pub mod fuzzer; +#[cfg(feature = "trace")] pub mod trace; /// Extension methods for streams From 5ed43bcad1e0290085428398cc46346784434532 Mon Sep 17 00:00:00 2001 From: clearloop Date: Sat, 23 May 2026 16:23:38 +0800 Subject: [PATCH 17/23] chore(vm): clean the narrow pool for vm execution --- crates/runtime/src/tx/guarantee/exec.rs | 38 +++++++------- crates/vm/compiler/src/compiler.rs | 10 +--- crates/vm/compiler/src/exec.rs | 2 +- crates/vm/compiler/src/numa/linux.rs | 58 +++++++-------------- crates/vm/compiler/src/numa/mod.rs | 68 +++++-------------------- crates/vm/spacevm/src/lib.rs | 10 +--- crates/vm/src/lib.rs | 11 +--- 7 files changed, 52 insertions(+), 145 deletions(-) diff --git a/crates/runtime/src/tx/guarantee/exec.rs b/crates/runtime/src/tx/guarantee/exec.rs index 35e92519..8cd6e8fc 100644 --- a/crates/runtime/src/tx/guarantee/exec.rs +++ b/crates/runtime/src/tx/guarantee/exec.rs @@ -111,26 +111,24 @@ pub fn parallel( let designate = context.privileges.designate; let validators_ref = &*validators; - let mut results = V::install(|| { - services - .par_iter() - .map(|service| { - let v = if *service == designate { - validators_ref.clone() - } else { - Default::default() - }; - let transfers = transfers - .par_iter() - .filter(|t| t.recipient == *service) - .cloned() - .collect(); - let result = - self::once::(context.clone(), v, transfers, reports, table, *service); - (*service, result) - }) - .collect::>>() - }); + let mut results = services + .par_iter() + .map(|service| { + let v = if *service == designate { + validators_ref.clone() + } else { + Default::default() + }; + let transfers = transfers + .par_iter() + .filter(|t| t.recipient == *service) + .cloned() + .collect(); + let result = + self::once::(context.clone(), v, transfers, reports, table, *service); + (*service, result) + }) + .collect::>>(); // Helper function R(o, a, b) from graypaper: if manager changed it (a != o), use a; else use b let r = |old: ServiceId, mgr: ServiceId, svc: ServiceId| -> ServiceId { diff --git a/crates/vm/compiler/src/compiler.rs b/crates/vm/compiler/src/compiler.rs index db115701..3199d915 100644 --- a/crates/vm/compiler/src/compiler.rs +++ b/crates/vm/compiler/src/compiler.rs @@ -9,15 +9,7 @@ use pvm::{ /// Cranelift JIT module builder pub struct Compiler; -impl Pvm for Compiler { - fn install(f: F) -> R - where - F: FnOnce() -> R + Send, - R: Send, - { - crate::numa::pool().install(f) - } -} +impl Pvm for Compiler {} impl Invocation for Compiler { fn invoke2( diff --git a/crates/vm/compiler/src/exec.rs b/crates/vm/compiler/src/exec.rs index a81a6986..c387a036 100644 --- a/crates/vm/compiler/src/exec.rs +++ b/crates/vm/compiler/src/exec.rs @@ -90,7 +90,7 @@ impl Executable { return Err(anyhow::anyhow!("Failed to allocate memory")); } - crate::numa::hint_code_pages(self.memory, self.size); + crate::numa::bind_pages(self.memory, self.size); Ok(()) } diff --git a/crates/vm/compiler/src/numa/linux.rs b/crates/vm/compiler/src/numa/linux.rs index 9b9cd229..9e0a7dc4 100644 --- a/crates/vm/compiler/src/numa/linux.rs +++ b/crates/vm/compiler/src/numa/linux.rs @@ -1,62 +1,38 @@ //! Linux NUMA detection. -use crate::numa::{NumaPlan, fallback}; +use crate::numa::NumaPlan; use std::collections::BTreeSet; /// Detect NUMA topology and pick a node for code-local execution. pub fn detect() -> NumaPlan { - let allowed: BTreeSet = match get_allowed_cpus() { - Some(set) if !set.is_empty() => set, - _ => return fallback(), + let Some(allowed) = get_allowed_cpus() else { + return NumaPlan { node: None }; }; + if allowed.is_empty() { + return NumaPlan { node: None }; + } - let buckets: Vec<(u32, Vec)> = read_node_cpulists() + let buckets: Vec<(u32, usize)> = read_node_cpulists() .into_iter() - .map(|(id, cpus)| { - let kept: Vec = cpus.into_iter().filter(|c| allowed.contains(c)).collect(); - (id, kept) - }) - .filter(|(_, cpus)| !cpus.is_empty()) + .map(|(id, cpus)| (id, cpus.into_iter().filter(|c| allowed.contains(c)).count())) + .filter(|(_, n)| *n > 0) .collect(); if buckets.len() <= 1 { - let cpus: Vec = allowed.into_iter().collect(); - let num_threads = cpus.len().max(1); - return NumaPlan { - node: None, - cpus, - num_threads, - }; + return NumaPlan { node: None }; } - let (node, cpus) = buckets.into_iter().max_by_key(|(_, c)| c.len()).unwrap(); - let num_threads = cpus.len().max(1); - tracing::info!("numa: chose node {node} ({num_threads} cpus) for code-local execution"); - NumaPlan { - node: Some(node), - cpus, - num_threads, - } -} - -pub fn set_affinity(cpus: &[usize]) -> std::io::Result<()> { - let mut set: libc::cpu_set_t = unsafe { std::mem::zeroed() }; - for &cpu in cpus { - unsafe { libc::CPU_SET(cpu, &mut set) }; - } - let size = std::mem::size_of::(); - if unsafe { libc::sched_setaffinity(0, size, &set) } != 0 { - return Err(std::io::Error::last_os_error()); - } - Ok(()) + let (node, n) = buckets.into_iter().max_by_key(|(_, n)| *n).unwrap(); + tracing::info!("numa: chose node {node} ({n} cpus) for code mappings"); + NumaPlan { node: Some(node) } } -/// Hint that the AOT code mapping should use huge pages, and bind its -/// not-yet-faulted pages to the chosen node (if any). -pub fn hint_code_pages(addr: *mut u8, size: usize) { +/// Bind the mapping to the chosen NUMA node (via `mbind`) and hint it for +/// transparent huge pages (`MADV_HUGEPAGE`). +pub fn bind_pages(addr: *mut u8, size: usize) { if unsafe { libc::madvise(addr.cast(), size, libc::MADV_HUGEPAGE) } != 0 { tracing::warn!( - "numa: madvise(MADV_HUGEPAGE) on AOT code buffer failed: {}", + "numa: madvise(MADV_HUGEPAGE) failed: {}", std::io::Error::last_os_error() ); } diff --git a/crates/vm/compiler/src/numa/mod.rs b/crates/vm/compiler/src/numa/mod.rs index f823a8e8..216ea437 100644 --- a/crates/vm/compiler/src/numa/mod.rs +++ b/crates/vm/compiler/src/numa/mod.rs @@ -1,12 +1,9 @@ -//! NUMA-aware process placement for the AOT compiler. +//! NUMA-aware hints for AOT code mappings. //! -//! The process is left unpinned so default rayon (sig batches, merkle, etc.) -//! keeps using every cgroup-allowed CPU. [`pool`] returns a dedicated rayon -//! pool whose workers are pinned to one NUMA node — used by PVM dispatch so -//! AOT code-cache locality is preserved across nested `par_iter`s. -//! -//! Note: only the AOT ([`crate::exec::Executable`]) path is hinted; cranelift's -//! JIT manages its own code memory and bypasses [`hint_code_pages`]. +//! Picks one NUMA node at startup ([`init`]) and binds AOT code pages to it +//! via [`bind_pages`] (mbind + MADV_HUGEPAGE), so JIT'd code (MB-scale) stays +//! local to the cores that execute it. No thread pinning, no separate rayon +//! pool — rayon's global pool keeps balancing across all CPUs. use std::sync::OnceLock; @@ -14,20 +11,15 @@ use std::sync::OnceLock; mod linux; static PLAN: OnceLock = OnceLock::new(); -static POOL: OnceLock = OnceLock::new(); /// Topology decision applied at startup. #[derive(Debug, Clone)] pub struct NumaPlan { - /// Chosen NUMA node, or `None` on UMA / non-Linux / on failure to pin. + /// Chosen NUMA node, or `None` on UMA / non-Linux / on detection failure. pub node: Option, - /// CPUs the process is allowed to run on after pinning. - pub cpus: Vec, - /// Suggested worker count for thread pools (`cpus.len()`, never zero). - pub num_threads: usize, } -/// Detect topology, pin the process, cache and return the plan. +/// Detect topology, cache and return the plan. pub fn init() -> &'static NumaPlan { PLAN.get_or_init(detect) } @@ -37,56 +29,22 @@ pub fn chosen_node() -> Option { PLAN.get().and_then(|p| p.node) } -/// Rayon pool whose workers are pinned to the chosen node's CPUs. -pub fn pool() -> &'static rayon::ThreadPool { - POOL.get_or_init(|| { - let plan = init(); - let cpus = plan.cpus.clone(); - rayon::ThreadPoolBuilder::new() - .num_threads(plan.num_threads) - .thread_name(|i| format!("numa-{i}")) - .start_handler(move |_| pin_worker(&cpus)) - .build() - .expect("build numa rayon pool") - }) -} - -/// Hint that an AOT code mmap should use huge pages and bind to the chosen -/// node. Call before the first byte is written. No-op on non-Linux. -pub fn hint_code_pages(addr: *mut u8, size: usize) { +/// Bind the given mapping to the chosen NUMA node and hint it for huge +/// pages. Call before the first byte is faulted in — `mbind` only affects +/// not-yet-faulted pages. No-op on non-Linux. +pub fn bind_pages(addr: *mut u8, size: usize) { #[cfg(target_os = "linux")] - linux::hint_code_pages(addr, size); + linux::bind_pages(addr, size); #[cfg(not(target_os = "linux"))] { let _ = (addr, size); } } -/// Fallback NUMA plan for non-Linux platforms. -pub fn fallback() -> NumaPlan { - let num_threads = std::thread::available_parallelism() - .map(|n| n.get()) - .unwrap_or(1); - NumaPlan { - node: None, - cpus: (0..num_threads).collect(), - num_threads, - } -} - -fn pin_worker(cpus: &[usize]) { - #[cfg(target_os = "linux")] - if let Err(err) = linux::set_affinity(cpus) { - tracing::warn!("numa: worker pin failed: {err}"); - } - #[cfg(not(target_os = "linux"))] - let _ = cpus; -} - fn detect() -> NumaPlan { #[cfg(target_os = "linux")] return linux::detect(); #[cfg(not(target_os = "linux"))] - fallback() + NumaPlan { node: None } } diff --git a/crates/vm/spacevm/src/lib.rs b/crates/vm/spacevm/src/lib.rs index d4321f9d..a1abf6b2 100644 --- a/crates/vm/spacevm/src/lib.rs +++ b/crates/vm/spacevm/src/lib.rs @@ -39,15 +39,7 @@ pub static SPACEVM_LOCKS: LazyLock>> = /// SpaceVM - JAM virtual machine pub struct SpaceVM; -impl Pvm for SpaceVM { - fn install(f: F) -> R - where - F: FnOnce() -> R + Send, - R: Send, - { - pvmc::numa::pool().install(f) - } -} +impl Pvm for SpaceVM {} impl Invocation for SpaceVM { fn invoke2( diff --git a/crates/vm/src/lib.rs b/crates/vm/src/lib.rs index e7187f89..5b966189 100644 --- a/crates/vm/src/lib.rs +++ b/crates/vm/src/lib.rs @@ -38,13 +38,4 @@ pub const REGISTER_COUNT: usize = 13; pub const MAX_FUNCTIONS: usize = 512; /// The PVM interface -pub trait Pvm: Invocation { - /// Run `f` on the worker pool if needed. - fn install(f: F) -> R - where - F: FnOnce() -> R + Send, - R: Send, - { - f() - } -} +pub trait Pvm: Invocation {} From cdf123d8fc32f244b5cfb13e648419be4675f9d4 Mon Sep 17 00:00:00 2001 From: clearloop Date: Sat, 23 May 2026 18:40:34 +0800 Subject: [PATCH 18/23] chore(runtime): sequential block processing --- crates/core/src/extrinsic/ticket.rs | 2 +- crates/runtime/src/chain/api.rs | 2 +- crates/runtime/src/chain/fork.rs | 4 ++-- crates/runtime/src/tx/block/header.rs | 6 +++--- crates/runtime/src/tx/block/mod.rs | 21 ++++++--------------- crates/runtime/src/tx/ticket/mod.rs | 2 +- 6 files changed, 14 insertions(+), 23 deletions(-) diff --git a/crates/core/src/extrinsic/ticket.rs b/crates/core/src/extrinsic/ticket.rs index 980b9746..1b469988 100644 --- a/crates/core/src/extrinsic/ticket.rs +++ b/crates/core/src/extrinsic/ticket.rs @@ -147,7 +147,7 @@ pub enum TicketsOrKeys { impl TicketsOrKeys { /// Returns the fallback keys for the given ring and entropy. #[cfg(feature = "blake2")] - pub fn fallback(ring: Vec, entropy: OpaqueHash) -> Self { + pub fn fallback(ring: &[BandersnatchPublic], entropy: OpaqueHash) -> Self { let mut keys = EpochKeys::default(); for i in 0..crate::EPOCH_LENGTH { let input = [entropy.as_slice(), &i.to_le_bytes()].concat(); diff --git a/crates/runtime/src/chain/api.rs b/crates/runtime/src/chain/api.rs index b725624c..9e7d9cf8 100644 --- a/crates/runtime/src/chain/api.rs +++ b/crates/runtime/src/chain/api.rs @@ -156,7 +156,7 @@ impl Runtime { } else { let validators = self.grid().await.next.bandersnatch(); let entropy = self.entropy().await?; - Ok(TicketsOrKeys::fallback(validators, entropy[1])) + Ok(TicketsOrKeys::fallback(&validators, entropy[1])) } } diff --git a/crates/runtime/src/chain/fork.rs b/crates/runtime/src/chain/fork.rs index ca59e38c..cc2a2da5 100644 --- a/crates/runtime/src/chain/fork.rs +++ b/crates/runtime/src/chain/fork.rs @@ -163,7 +163,7 @@ impl Fork { if epoch > prev_epoch && !self.series.contains_key(&epoch) { let validators = self.state.safrole()?.validators.bandersnatch(); let entropy = self.state.entropy()?; - let series = TicketsOrKeys::fallback(validators, entropy[1]); + let series = TicketsOrKeys::fallback(&validators, entropy[1]); self.series.insert(epoch, series); } @@ -194,7 +194,7 @@ impl Fork { } else { let validators = self.state.safrole()?.validators.bandersnatch(); let entropy = self.state.entropy()?; - let series = TicketsOrKeys::fallback(validators, entropy[1]); + let series = TicketsOrKeys::fallback(&validators, entropy[1]); Ok(series) } } diff --git a/crates/runtime/src/tx/block/header.rs b/crates/runtime/src/tx/block/header.rs index afea4d3f..c06929ad 100644 --- a/crates/runtime/src/tx/block/header.rs +++ b/crates/runtime/src/tx/block/header.rs @@ -9,9 +9,9 @@ use score::{ }; /// Validate the header -pub fn validate(state: State, header: &Header) -> anyhow::Result<()> { +pub fn validate(state: &State, header: &Header) -> anyhow::Result<()> { let new_epoch = header.slot / score::EPOCH_LENGTH > state.timeslot / score::EPOCH_LENGTH; - self::check(&state, header, new_epoch)?; + self::check(state, header, new_epoch)?; // setup the verifier let slot = (header.slot % score::EPOCH_LENGTH) as usize; @@ -55,7 +55,7 @@ pub fn validate(state: State, header: &Header) -> anyhow::Result<()> { }; let key = if new_epoch { - let TicketsOrKeys::Keys(keys) = TicketsOrKeys::fallback(vals.clone(), state.entropy[1]) + let TicketsOrKeys::Keys(keys) = TicketsOrKeys::fallback(&vals, state.entropy[1]) else { anyhow::bail!("invalid series"); }; diff --git a/crates/runtime/src/tx/block/mod.rs b/crates/runtime/src/tx/block/mod.rs index 96d1d412..6882c1be 100644 --- a/crates/runtime/src/tx/block/mod.rs +++ b/crates/runtime/src/tx/block/mod.rs @@ -21,23 +21,14 @@ const EMPTY_ROOT: OpaqueHash = [0; 32]; type Fork = Branch; -/// DEVELOPMENT: process the block with given state storage. +/// Process the block with given state storage. pub fn process(block: Block, storage: Arc) -> Result<()> { let state = storage.state()?; - let mut block2 = block.clone(); - let state2 = state.clone(); - let (vresult, sresult) = rayon::join( - || header::validate(state, &block.header), - || tx::simulate_with_state::(&mut block2, state2, storage.clone()), - ); - - match (vresult, sresult) { - (Err(e), _) | (_, Err(e)) => Err(e), - (Ok(()), Ok(diff)) => { - storage.commit(Column::State, &diff)?; - Ok(()) - } - } + header::validate(&state, &block.header)?; + let mut block = block; + let diff = tx::simulate_with_state::(&mut block, state, storage.clone())?; + storage.commit(Column::State, &diff)?; + Ok(()) } /// DEVELOPMENT: A test chain for processing fuzz blocks. diff --git a/crates/runtime/src/tx/ticket/mod.rs b/crates/runtime/src/tx/ticket/mod.rs index 09ac521e..b8ef37c2 100644 --- a/crates/runtime/src/tx/ticket/mod.rs +++ b/crates/runtime/src/tx/ticket/mod.rs @@ -168,7 +168,7 @@ pub fn sealing_key_series( { next = TicketsOrKeys::Tickets(TicketBody::sequence(&safrole.accumulator)); } else { - next = TicketsOrKeys::fallback(curr_validators.bandersnatch(), entropy[2]); + next = TicketsOrKeys::fallback(&curr_validators.bandersnatch(), entropy[2]); } next From b9656356d17cac4933d6ffef693eb7e7ee59f76b Mon Sep 17 00:00:00 2001 From: clearloop Date: Sat, 23 May 2026 20:03:28 +0800 Subject: [PATCH 19/23] chore(runtime): clean dirty clones --- crates/core/account/src/registry.rs | 5 ++-- crates/runtime/src/account/registry.rs | 4 +-- crates/runtime/src/tx/dispute/mod.rs | 24 +++++++--------- crates/runtime/src/tx/executor.rs | 26 ++++++++++------- crates/runtime/src/tx/guarantee/exec.rs | 4 +-- crates/runtime/src/tx/guarantee/validator.rs | 2 +- crates/runtime/src/tx/preimage.rs | 4 +-- crates/runtime/src/tx/ticket/mod.rs | 30 ++++++++++---------- crates/testing/src/disputes.rs | 2 +- crates/testing/src/preimage.rs | 2 +- crates/testing/src/safrole.rs | 2 +- crates/vm/compiler/src/numa/mod.rs | 9 +----- 12 files changed, 54 insertions(+), 60 deletions(-) diff --git a/crates/core/account/src/registry.rs b/crates/core/account/src/registry.rs index 6da2500d..dd8bd281 100644 --- a/crates/core/account/src/registry.rs +++ b/crates/core/account/src/registry.rs @@ -41,8 +41,9 @@ pub trait Accounts: Clone + Send + Sync + 'static { fn accounts(&self) -> &BTreeMap; /// Get the removed accounts from the registry - fn removed(&self) -> BTreeSet { - Default::default() + fn removed(&self) -> &BTreeSet { + static EMPTY: std::sync::OnceLock> = std::sync::OnceLock::new(); + EMPTY.get_or_init(BTreeSet::new) } /// Get the diff of the accounts diff --git a/crates/runtime/src/account/registry.rs b/crates/runtime/src/account/registry.rs index 71bb5ee7..f3657978 100644 --- a/crates/runtime/src/account/registry.rs +++ b/crates/runtime/src/account/registry.rs @@ -90,8 +90,8 @@ impl account::Accounts for Accounts { &self.accounts } - fn removed(&self) -> BTreeSet { - self.removed.clone() + fn removed(&self) -> &BTreeSet { + &self.removed } fn diff(self) -> (Vec<([u8; 31], Vec)>, Vec<[u8; 31]>) { diff --git a/crates/runtime/src/tx/dispute/mod.rs b/crates/runtime/src/tx/dispute/mod.rs index 8d022987..4cfb27bd 100644 --- a/crates/runtime/src/tx/dispute/mod.rs +++ b/crates/runtime/src/tx/dispute/mod.rs @@ -20,10 +20,9 @@ pub fn disputes( timeslot: TimeSlot, kappa: &ValidatorsData, lambda: &ValidatorsData, - psi: &DisputesRecords, + psi: DisputesRecords, extrinsic: &DisputesExtrinsic, ) -> Result<(DisputesRecords, DisputesRecords, Vec)> { - let mut next_psi = psi.clone(); let (mut records, mut triples) = dispute::verdicts(timeslot, kappa, lambda, &extrinsic.verdicts)?; @@ -35,26 +34,23 @@ pub fn disputes( // handle culprits let (culprit_offenders, culprit_triples) = - dispute::culprits(&validators, psi, &records.bad, &extrinsic.culprits)?; + dispute::culprits(&validators, &psi, &records.bad, &extrinsic.culprits)?; records.offenders.extend(&culprit_offenders); triples.extend(culprit_triples); // handle faults let (fault_offenders, fault_triples) = - dispute::faults(&validators, psi, &records.good, &extrinsic.faults)?; + dispute::faults(&validators, &psi, &records.good, &extrinsic.faults)?; records.offenders.extend(&fault_offenders); triples.extend(fault_triples); - // update psi - { - next_psi.good.extend(&records.good); - next_psi.wonky.extend(&records.wonky); - next_psi.bad.extend(&records.bad); - - // TODO: make offenders unique - next_psi.offenders.extend(&records.offenders); - next_psi.offenders.sort(); - } + let mut next_psi = psi; + next_psi.good.extend(&records.good); + next_psi.wonky.extend(&records.wonky); + next_psi.bad.extend(&records.bad); + // TODO: make offenders unique + next_psi.offenders.extend(&records.offenders); + next_psi.offenders.sort(); Ok((next_psi, records, triples)) } diff --git a/crates/runtime/src/tx/executor.rs b/crates/runtime/src/tx/executor.rs index 65ca4d19..8e6cc464 100644 --- a/crates/runtime/src/tx/executor.rs +++ b/crates/runtime/src/tx/executor.rs @@ -105,7 +105,7 @@ impl<'a, Vm: Pvm, S: Storage> Executor<'a, Vm, S> { self.state.timeslot, &self.state.validators.current, &self.state.validators.previous, - &self.state.disputes, + std::mem::take(&mut self.state.disputes), &self.block.extrinsic.disputes, )?; crypto::ed25519::SigItem::batch_verify(&triples)?; @@ -226,7 +226,7 @@ impl<'a, Vm: Pvm, S: Storage> Executor<'a, Vm, S> { let accounts = self.accounts.take().expect("accounts present"); let accounts = preimage::accounts( self.block.header.slot, - &self.block.extrinsic.preimages, + std::mem::take(&mut self.block.extrinsic.preimages), accounts, ); let (updates, removals) = accounts.diff(); @@ -251,11 +251,13 @@ impl<'a, Vm: Pvm, S: Storage> Executor<'a, Vm, S> { /// Run guarantee/assurance sig collect + batch_verify in parallel with /// ticket::safrole ring-VRF. fn sigs_and_safrole_parallel(&mut self) -> Result<()> { + let new_epoch = self.new_epoch; + let needs_safrole = !self.block.extrinsic.tickets.is_empty() || new_epoch; + let safrole_in = needs_safrole.then(|| std::mem::take(&mut self.state.safrole)); let state_view: &State = &self.state; let block_view: &Block = &*self.block; let accounts_view = self.accounts.as_ref().expect("accounts present"); let dispute_records_view = &self.dispute_records; - let new_epoch = self.new_epoch; let (sigs_res, safrole_res) = rayon::join( || { Self::sigs_branch( @@ -266,7 +268,11 @@ impl<'a, Vm: Pvm, S: Storage> Executor<'a, Vm, S> { new_epoch, ) }, - || Self::safrole_branch(state_view, block_view, new_epoch), + || { + safrole_in + .map(|s| Self::safrole_branch(state_view, block_view, new_epoch, s)) + .transpose() + }, ); let out = sigs_res?; @@ -337,17 +343,15 @@ impl<'a, Vm: Pvm, S: Storage> Executor<'a, Vm, S> { state: &State, block: &Block, new_epoch: bool, - ) -> Result> { - if block.extrinsic.tickets.is_empty() && !new_epoch { - return Ok(None); - } + safrole_in: Safrole, + ) -> Result { let _guard = timing::safrole(); let safrole = ticket::safrole( state.timeslot, block.header.slot, state.entropy, &state.disputes.offenders, - &state.safrole, + safrole_in, &state.validators, &block.extrinsic.tickets, )?; @@ -357,11 +361,11 @@ impl<'a, Vm: Pvm, S: Storage> Executor<'a, Vm, S> { None }; let tickets_mark = safrole.tickets_mark(state.timeslot, block.header.slot); - Ok(Some(SafroleOutput { + Ok(SafroleOutput { safrole, epoch_mark, tickets_mark, - })) + }) } } diff --git a/crates/runtime/src/tx/guarantee/exec.rs b/crates/runtime/src/tx/guarantee/exec.rs index 8cd6e8fc..ba295db5 100644 --- a/crates/runtime/src/tx/guarantee/exec.rs +++ b/crates/runtime/src/tx/guarantee/exec.rs @@ -189,7 +189,7 @@ pub fn parallel( let mut transfers = Vec::new(); let mut pairings = BTreeSet::new(); for (service_id, result) in results.iter_mut() { - transfers.extend(result.transfers.clone()); + transfers.extend(std::mem::take(&mut result.transfers)); if let Some(hash) = result.hash { pairings.insert((*service_id, hash)); @@ -199,7 +199,7 @@ pub fn parallel( continue; } - for service in result.context.accounts.removed() { + for &service in result.context.accounts.removed() { removed.insert(service); } diff --git a/crates/runtime/src/tx/guarantee/validator.rs b/crates/runtime/src/tx/guarantee/validator.rs index 440b8caa..963166d2 100644 --- a/crates/runtime/src/tx/guarantee/validator.rs +++ b/crates/runtime/src/tx/guarantee/validator.rs @@ -88,7 +88,7 @@ impl<'s, R: Accounts> GuaranteeValidator<'s, R> { .recent_blocks .history .iter() - .flat_map(|b| b.reported.clone()) + .flat_map(|b| b.reported.iter().cloned()) .collect::>(); self.recent = recent; diff --git a/crates/runtime/src/tx/preimage.rs b/crates/runtime/src/tx/preimage.rs index f5b6ac7d..fa2d8957 100644 --- a/crates/runtime/src/tx/preimage.rs +++ b/crates/runtime/src/tx/preimage.rs @@ -25,7 +25,7 @@ pub fn validate(accounts: &mut A, preimages: &PreimagesExtrinsic) - } /// (δ') Integrate providable preimages into the post-transfer state -pub fn accounts(slot: TimeSlot, preimages: &PreimagesExtrinsic, mut accounts: A) -> A { +pub fn accounts(slot: TimeSlot, preimages: PreimagesExtrinsic, mut accounts: A) -> A { for preimage in preimages { let hash = crypto::blake2b(&preimage.blob); let len = preimage.blob.len() as u32; @@ -33,7 +33,7 @@ pub fn accounts(slot: TimeSlot, preimages: &PreimagesExtrinsic, mut continue; } let account = accounts.get(preimage.requester).expect("just checked"); - account.insert_preimage(hash, preimage.blob.clone()); + account.insert_preimage(hash, preimage.blob); account.insert_lookup(hash, len, vec![slot]); } accounts diff --git a/crates/runtime/src/tx/ticket/mod.rs b/crates/runtime/src/tx/ticket/mod.rs index b8ef37c2..5ce4cd63 100644 --- a/crates/runtime/src/tx/ticket/mod.rs +++ b/crates/runtime/src/tx/ticket/mod.rs @@ -51,7 +51,7 @@ pub fn safrole( slot: u32, entropy: [OpaqueHash; 4], offenders: &[Ed25519Public], - safrole: &Safrole, + mut safrole: Safrole, validators: &Validators, tickets: &TicketsExtrinsic, ) -> Result { @@ -72,8 +72,9 @@ pub fn safrole( let epoch = tau / score::EPOCH_LENGTH; let next_epoch = slot / score::EPOCH_LENGTH; let new_epoch: bool = next_epoch > epoch; - let mut safrole = safrole.clone(); - safrole.series = self::sealing_key_series(tau, slot, entropy, &safrole, &validators.current); + if let Some(series) = self::sealing_key_series(tau, slot, entropy, &safrole, &validators.current) { + safrole.series = series; + } if new_epoch { let next = safrole.next(&validators.drawn, offenders); if next != safrole.validators { @@ -83,9 +84,10 @@ pub fn safrole( } // Process accumulator and ring commitment in parallel + let acc = std::mem::take(&mut safrole.accumulator); safrole.accumulator = self::accumulator( new_epoch, - &safrole.accumulator, + acc, entropy, &safrole.validators.bandersnatch(), tickets, @@ -99,7 +101,7 @@ pub fn safrole( /// NOTE: gamma_k has already been updated at this point pub fn accumulator( new_epoch: bool, - accumulator: &TicketsAccumulator, + mut accumulator: TicketsAccumulator, entropy: [OpaqueHash; 4], next: &Vec, tickets: &TicketsExtrinsic, @@ -113,7 +115,6 @@ pub fn accumulator( let submitted_ids: Vec = new_tickets.iter().map(|t| t.id).collect(); // update the accumulator - let mut accumulator = accumulator.clone(); if new_epoch { // Clear the accumulator if we're starting a new epoch: 6.34 accumulator.clear(); @@ -146,30 +147,29 @@ pub fn accumulator( } /// (γ_s') Updates the sealing key series according to graypaper formula 6.24. +/// Returns `None` when the series is unchanged (same epoch). pub fn sealing_key_series( tau: u32, slot: u32, entropy: [OpaqueHash; 4], safrole: &Safrole, curr_validators: &[ValidatorData], -) -> TicketsOrKeys { - let mut next = safrole.series.clone(); +) -> Option { let curr_epoch = slot / score::EPOCH_LENGTH; let prev_epoch = tau / score::EPOCH_LENGTH; let prev_slot_phase = tau % score::EPOCH_LENGTH; if curr_epoch == prev_epoch { - return next; + return None; } // FIXME: should be curr_epoch > prev_epoch - if curr_epoch == prev_epoch + 1 + let next = if curr_epoch == prev_epoch + 1 && prev_slot_phase >= score::TICKET_SUBMISSION_PERIOD && safrole.accumulator.len() == score::EPOCH_LENGTH as usize { - next = TicketsOrKeys::Tickets(TicketBody::sequence(&safrole.accumulator)); + TicketsOrKeys::Tickets(TicketBody::sequence(&safrole.accumulator)) } else { - next = TicketsOrKeys::fallback(&curr_validators.bandersnatch(), entropy[2]); - } - - next + TicketsOrKeys::fallback(&curr_validators.bandersnatch(), entropy[2]) + }; + Some(next) } diff --git a/crates/testing/src/disputes.rs b/crates/testing/src/disputes.rs index 14d3f400..2b630cf5 100644 --- a/crates/testing/src/disputes.rs +++ b/crates/testing/src/disputes.rs @@ -22,7 +22,7 @@ pub fn run(test: &specjam::Test) -> anyhow::Result<()> { input.pre_state.tau, &input.pre_state.kappa, &input.pre_state.lambda, - &input.pre_state.psi, + input.pre_state.psi.clone(), &input.input.disputes, ) .and_then(|(next_psi, records, triples)| { diff --git a/crates/testing/src/preimage.rs b/crates/testing/src/preimage.rs index 2d374658..bc425be9 100644 --- a/crates/testing/src/preimage.rs +++ b/crates/testing/src/preimage.rs @@ -22,7 +22,7 @@ pub fn run(test: &specjam::Test) -> anyhow::Result<()> { assert_eq!(input.pre_state, output.post_state); return Ok(()); } - let accounts = tx::preimage::accounts(input.input.slot, &input.input.preimages, accounts); + let accounts = tx::preimage::accounts(input.input.slot, input.input.preimages.clone(), accounts); assert_eq!( accounts .accounts() diff --git a/crates/testing/src/safrole.rs b/crates/testing/src/safrole.rs index 5f5f0b95..95b5a1f5 100644 --- a/crates/testing/src/safrole.rs +++ b/crates/testing/src/safrole.rs @@ -133,7 +133,7 @@ impl State { input.slot, self.eta, &self.post_offenders, - &safrole, + safrole.clone(), &validators, &input.extrinsic, ) { diff --git a/crates/vm/compiler/src/numa/mod.rs b/crates/vm/compiler/src/numa/mod.rs index 216ea437..b13a16a2 100644 --- a/crates/vm/compiler/src/numa/mod.rs +++ b/crates/vm/compiler/src/numa/mod.rs @@ -1,9 +1,4 @@ //! NUMA-aware hints for AOT code mappings. -//! -//! Picks one NUMA node at startup ([`init`]) and binds AOT code pages to it -//! via [`bind_pages`] (mbind + MADV_HUGEPAGE), so JIT'd code (MB-scale) stays -//! local to the cores that execute it. No thread pinning, no separate rayon -//! pool — rayon's global pool keeps balancing across all CPUs. use std::sync::OnceLock; @@ -29,9 +24,7 @@ pub fn chosen_node() -> Option { PLAN.get().and_then(|p| p.node) } -/// Bind the given mapping to the chosen NUMA node and hint it for huge -/// pages. Call before the first byte is faulted in — `mbind` only affects -/// not-yet-faulted pages. No-op on non-Linux. +/// Bind the mapping to the chosen NUMA node and hint it for huge pages. pub fn bind_pages(addr: *mut u8, size: usize) { #[cfg(target_os = "linux")] linux::bind_pages(addr, size); From 5ae2c856405fe678062e22798f8d04ffd4910546 Mon Sep 17 00:00:00 2001 From: clearloop Date: Sun, 24 May 2026 00:55:53 +0800 Subject: [PATCH 20/23] refactor(vm): drop NUMA pinning and use a capped rayon pool --- Cargo.lock | 1 + crates/spacejam/bin/spacejam.rs | 20 ++++- crates/vm/compiler/src/exec.rs | 7 +- crates/vm/compiler/src/lib.rs | 1 - crates/vm/compiler/src/numa/linux.rs | 125 --------------------------- crates/vm/compiler/src/numa/mod.rs | 43 --------- crates/vm/spacevm/Cargo.toml | 1 + crates/vm/spacevm/src/lib.rs | 5 +- 8 files changed, 29 insertions(+), 174 deletions(-) delete mode 100644 crates/vm/compiler/src/numa/linux.rs delete mode 100644 crates/vm/compiler/src/numa/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 3fb8c1fc..99dc192f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3762,6 +3762,7 @@ dependencies = [ "pvm", "pvmc", "pvmi", + "rayon", "tracing", "tracing-subscriber", ] diff --git a/crates/spacejam/bin/spacejam.rs b/crates/spacejam/bin/spacejam.rs index 3c42af80..2832d4bd 100644 --- a/crates/spacejam/bin/spacejam.rs +++ b/crates/spacejam/bin/spacejam.rs @@ -4,7 +4,7 @@ use spacejam::cmd::App; #[tokio::main] async fn main() { - let _ = spacevm::numa::init(); + self::init_rayon(); #[cfg(feature = "dhat")] dhat::init(); @@ -12,6 +12,24 @@ async fn main() { 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; diff --git a/crates/vm/compiler/src/exec.rs b/crates/vm/compiler/src/exec.rs index c387a036..9e9e0757 100644 --- a/crates/vm/compiler/src/exec.rs +++ b/crates/vm/compiler/src/exec.rs @@ -90,7 +90,12 @@ impl Executable { return Err(anyhow::anyhow!("Failed to allocate memory")); } - crate::numa::bind_pages(self.memory, self.size); + // Huge-page hint only; mbind pinned AOT code to one node and hurt + // cross-socket execution. + #[cfg(target_os = "linux")] + unsafe { + libc::madvise(self.memory.cast(), self.size, libc::MADV_HUGEPAGE); + } Ok(()) } diff --git a/crates/vm/compiler/src/lib.rs b/crates/vm/compiler/src/lib.rs index 2d180d79..596ad8a0 100644 --- a/crates/vm/compiler/src/lib.rs +++ b/crates/vm/compiler/src/lib.rs @@ -20,7 +20,6 @@ mod exec; pub mod host; pub mod memory; pub mod module; -pub mod numa; pub mod trap; #[cfg(target_os = "macos")] diff --git a/crates/vm/compiler/src/numa/linux.rs b/crates/vm/compiler/src/numa/linux.rs deleted file mode 100644 index 9e0a7dc4..00000000 --- a/crates/vm/compiler/src/numa/linux.rs +++ /dev/null @@ -1,125 +0,0 @@ -//! Linux NUMA detection. - -use crate::numa::NumaPlan; -use std::collections::BTreeSet; - -/// Detect NUMA topology and pick a node for code-local execution. -pub fn detect() -> NumaPlan { - let Some(allowed) = get_allowed_cpus() else { - return NumaPlan { node: None }; - }; - if allowed.is_empty() { - return NumaPlan { node: None }; - } - - let buckets: Vec<(u32, usize)> = read_node_cpulists() - .into_iter() - .map(|(id, cpus)| (id, cpus.into_iter().filter(|c| allowed.contains(c)).count())) - .filter(|(_, n)| *n > 0) - .collect(); - - if buckets.len() <= 1 { - return NumaPlan { node: None }; - } - - let (node, n) = buckets.into_iter().max_by_key(|(_, n)| *n).unwrap(); - tracing::info!("numa: chose node {node} ({n} cpus) for code mappings"); - NumaPlan { node: Some(node) } -} - -/// Bind the mapping to the chosen NUMA node (via `mbind`) and hint it for -/// transparent huge pages (`MADV_HUGEPAGE`). -pub fn bind_pages(addr: *mut u8, size: usize) { - if unsafe { libc::madvise(addr.cast(), size, libc::MADV_HUGEPAGE) } != 0 { - tracing::warn!( - "numa: madvise(MADV_HUGEPAGE) failed: {}", - std::io::Error::last_os_error() - ); - } - - if let Some(node) = super::chosen_node() { - bind_to_node(addr, size, node); - } -} - -fn get_allowed_cpus() -> Option> { - let mut set: libc::cpu_set_t = unsafe { std::mem::zeroed() }; - let size = std::mem::size_of::(); - if unsafe { libc::sched_getaffinity(0, size, &mut set) } != 0 { - return None; - } - let mut allowed = BTreeSet::new(); - for cpu in 0..(libc::CPU_SETSIZE as usize) { - if unsafe { libc::CPU_ISSET(cpu, &set) } { - allowed.insert(cpu); - } - } - Some(allowed) -} - -fn read_node_cpulists() -> Vec<(u32, Vec)> { - let Ok(entries) = std::fs::read_dir("/sys/devices/system/node") else { - return Vec::new(); - }; - let mut nodes = Vec::new(); - for entry in entries.flatten() { - let name = entry.file_name(); - let name = name.to_string_lossy(); - let Some(rest) = name.strip_prefix("node") else { - continue; - }; - let Ok(id) = rest.parse::() else { - continue; - }; - let Ok(text) = std::fs::read_to_string(entry.path().join("cpulist")) else { - continue; - }; - nodes.push((id, parse_cpulist(text.trim()))); - } - nodes -} - -fn parse_cpulist(s: &str) -> Vec { - let mut out = Vec::new(); - for part in s.split(',') { - let part = part.trim(); - if part.is_empty() { - continue; - } - if let Some((a, b)) = part.split_once('-') { - if let (Ok(a), Ok(b)) = (a.parse::(), b.parse::()) { - out.extend(a..=b); - } - } else if let Ok(n) = part.parse::() { - out.push(n); - } - } - out -} - -fn bind_to_node(addr: *mut u8, size: usize, node: u32) { - // MPOL_BIND from linux/mempolicy.h; not exposed by the libc crate. - const MPOL_BIND: libc::c_int = 2; - if node >= 64 { - tracing::warn!("numa: chosen node {node} out of mbind range, skipping"); - return; - } - let mask: u64 = 1u64 << node; - let ret = unsafe { - libc::syscall( - libc::SYS_mbind, - addr, - size as libc::c_ulong, - MPOL_BIND, - &mask as *const u64, - 64u64, - 0u32, - ) - }; - if ret != 0 { - tracing::warn!( - "numa: mbind AOT code buffer to node {node} failed: {}", - std::io::Error::last_os_error() - ); - } -} diff --git a/crates/vm/compiler/src/numa/mod.rs b/crates/vm/compiler/src/numa/mod.rs deleted file mode 100644 index b13a16a2..00000000 --- a/crates/vm/compiler/src/numa/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! NUMA-aware hints for AOT code mappings. - -use std::sync::OnceLock; - -#[cfg(target_os = "linux")] -mod linux; - -static PLAN: OnceLock = OnceLock::new(); - -/// Topology decision applied at startup. -#[derive(Debug, Clone)] -pub struct NumaPlan { - /// Chosen NUMA node, or `None` on UMA / non-Linux / on detection failure. - pub node: Option, -} - -/// Detect topology, cache and return the plan. -pub fn init() -> &'static NumaPlan { - PLAN.get_or_init(detect) -} - -/// Chosen NUMA node, if any. `None` before [`init`] runs. -pub fn chosen_node() -> Option { - PLAN.get().and_then(|p| p.node) -} - -/// Bind the mapping to the chosen NUMA node and hint it for huge pages. -pub fn bind_pages(addr: *mut u8, size: usize) { - #[cfg(target_os = "linux")] - linux::bind_pages(addr, size); - #[cfg(not(target_os = "linux"))] - { - let _ = (addr, size); - } -} - -fn detect() -> NumaPlan { - #[cfg(target_os = "linux")] - return linux::detect(); - - #[cfg(not(target_os = "linux"))] - NumaPlan { node: None } -} diff --git a/crates/vm/spacevm/Cargo.toml b/crates/vm/spacevm/Cargo.toml index 7081f7c6..119fdfb0 100644 --- a/crates/vm/spacevm/Cargo.toml +++ b/crates/vm/spacevm/Cargo.toml @@ -10,6 +10,7 @@ lru.workspace = true pvm.workspace = true pvmc.workspace = true pvmi.workspace = true +rayon.workspace = true tracing.workspace = true [dev-dependencies] diff --git a/crates/vm/spacevm/src/lib.rs b/crates/vm/spacevm/src/lib.rs index a1abf6b2..f76110eb 100644 --- a/crates/vm/spacevm/src/lib.rs +++ b/crates/vm/spacevm/src/lib.rs @@ -7,13 +7,12 @@ use pvm::{ Argument, Invocation, Invoked, Pvm, State, parser, score::{Gas, OpaqueHash}, }; -pub use pvmc::{Artifact, Compiler, Memory, ModuleLike, SPACEJAM_CACHE_DIR, numa}; +pub use pvmc::{Artifact, Compiler, Memory, ModuleLike, SPACEJAM_CACHE_DIR}; pub use pvmi::Interpreter; use std::{ collections::BTreeSet, num::NonZeroUsize, sync::{Arc, LazyLock, Mutex, RwLock}, - thread, }; /// Default max modules kept in memory. @@ -114,7 +113,7 @@ impl Invocation for SpaceVM { { let code = code.clone(); let args = args.clone(); - thread::spawn(move || { + rayon::spawn(move || { if let Err(e) = self::compile::(code, args, hash, true) { tracing::debug!("failed to compile program: {e:?}"); } From 5bc0daa82251399cd184f4c30cf98f66a63e21fd Mon Sep 17 00:00:00 2001 From: clearloop Date: Sun, 24 May 2026 01:16:34 +0800 Subject: [PATCH 21/23] refactor(pvmc): drop guest-memory mmap pool --- crates/vm/compiler/src/memory/mmap.rs | 67 ++++++--------------------- 1 file changed, 13 insertions(+), 54 deletions(-) diff --git a/crates/vm/compiler/src/memory/mmap.rs b/crates/vm/compiler/src/memory/mmap.rs index eb43146a..cb2519af 100644 --- a/crates/vm/compiler/src/memory/mmap.rs +++ b/crates/vm/compiler/src/memory/mmap.rs @@ -4,11 +4,7 @@ use anyhow::Result; use libc::{MAP_ANONYMOUS, MAP_NORESERVE, MAP_PRIVATE, PROT_NONE, PROT_READ, PROT_WRITE}; use pvm::MemoryLike; -use std::{cell::RefCell, collections::BTreeMap, io, ptr}; - -thread_local! { - static POOL: RefCell> = const { RefCell::new(None) }; -} +use std::{collections::BTreeMap, io, ptr}; /// memory for PVM programs #[derive(Debug, Clone)] @@ -25,23 +21,6 @@ pub struct Memory { impl Memory { /// Create a new memory instance from parser Memory pub fn new(pmemory: &pvm::Memory) -> Result { - let region = match POOL.with(|p| p.borrow_mut().take()) { - Some(r) => r, - None => Self::mmap_region()?, - }; - let base = region.0; - std::mem::forget(region); - - let memory = Memory { - base, - heap_ptr: pmemory.heap_ptr, - }; - - memory.init(pmemory)?; - Ok(memory) - } - - fn mmap_region() -> Result { let base = unsafe { libc::mmap( ptr::null_mut(), @@ -52,13 +31,21 @@ impl Memory { 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)) + + let memory = Memory { + base: base as *mut u8, + heap_ptr: pmemory.heap_ptr, + }; + + memory.init(pmemory)?; + Ok(memory) } /// Initialize memory regions from parser memory @@ -185,28 +172,11 @@ impl Memory { impl Drop for Memory { fn drop(&mut self) { - if self.base.is_null() { - return; - } - - let size = pvm::PVM_MEMORY_SIZE as usize; unsafe { - libc::madvise(self.base as *mut _, size, libc::MADV_DONTNEED); - libc::mprotect(self.base as *mut _, size, PROT_NONE); - } - - let base = self.base; - self.base = ptr::null_mut(); - - POOL.with(|p| { - let mut slot = p.borrow_mut(); - if slot.is_none() { - *slot = Some(Region(base)); - } else { - // Slot already full; drop this region (munmaps via Region::drop). - drop(Region(base)); + if !self.base.is_null() { + libc::munmap(self.base as *mut _, pvm::PVM_MEMORY_SIZE as usize); } - }); + } } } @@ -265,14 +235,3 @@ impl MemoryLike for Memory { self.heap_ptr = heap_ptr; } } - -// Per-thread reusable 4 GB guest mapping. -struct Region(*mut u8); - -impl Drop for Region { - fn drop(&mut self) { - if !self.0.is_null() { - unsafe { libc::munmap(self.0 as *mut _, pvm::PVM_MEMORY_SIZE as usize) }; - } - } -} From dba29a86ee68529d39b36487bdf1a75bf6ae53f7 Mon Sep 17 00:00:00 2001 From: clearloop Date: Sun, 24 May 2026 01:40:47 +0800 Subject: [PATCH 22/23] feat(vm): introduce shared cache abstraction for pvmc and pvmi --- Cargo.lock | 17 +++++-- Cargo.toml | 1 + crates/vm/Cargo.toml | 1 + crates/vm/interpreter/src/pvmi.rs | 29 +++--------- crates/vm/spacevm/Cargo.toml | 1 - crates/vm/spacevm/src/lib.rs | 76 ++++++++----------------------- crates/vm/src/cache.rs | 34 ++++++++++++++ crates/vm/src/lib.rs | 2 + 8 files changed, 75 insertions(+), 86 deletions(-) create mode 100644 crates/vm/src/cache.rs diff --git a/Cargo.lock b/Cargo.lock index 99dc192f..467c59eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,6 +113,15 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + [[package]] name = "ark-bls12-377" version = "0.4.0" @@ -1252,7 +1261,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1986,7 +1995,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2749,6 +2758,7 @@ name = "pvm" version = "0.0.0" dependencies = [ "anyhow", + "arc-swap", "hex", "log", "pvm-parser", @@ -3758,7 +3768,6 @@ version = "0.1.2-pre.8" dependencies = [ "anyhow", "hex", - "lru", "pvm", "pvmc", "pvmi", @@ -4592,7 +4601,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index cf86e5a8..6a7249e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ worker = { path = "crates/offchain/worker", package = "spacejam-worker" } # crates.io anyhow = { version = "1.0.99", default-features = false } +arc-swap = "1.7" ark-bls12-381 = "0.5.0" ark-ec = { version = "0.5", default-features = false } ark-ed-on-bls12-381-bandersnatch = { version = "0.5", default-features = false } diff --git a/crates/vm/Cargo.toml b/crates/vm/Cargo.toml index 6dec2612..d80153ce 100644 --- a/crates/vm/Cargo.toml +++ b/crates/vm/Cargo.toml @@ -7,6 +7,7 @@ description = "Polkadot Virtual Machine" [dependencies] account = { workspace = true, features = ["blake2"] } anyhow.workspace = true +arc-swap.workspace = true codec.workspace = true crypto = { workspace = true, features = ["blake2"] } hex.workspace = true diff --git a/crates/vm/interpreter/src/pvmi.rs b/crates/vm/interpreter/src/pvmi.rs index 675037e7..72e22194 100644 --- a/crates/vm/interpreter/src/pvmi.rs +++ b/crates/vm/interpreter/src/pvmi.rs @@ -1,41 +1,24 @@ //! PVM interface implementation use crate::Interpreter; -use lru::LruCache; use parser::{program, reader::Offset, Instruction}; use pvm::{ score::{Gas, OpaqueHash}, - Argument, Invocation, Invoked, Pvm, + Argument, Cache, Invocation, Invoked, Pvm, }; -use std::{ - num::NonZeroUsize, - sync::{Arc, LazyLock, Mutex}, -}; - -/// The maximum number of cached parsed programs. -const MAX_CACHED_PROGRAMS: usize = 16; +use std::sync::{Arc, LazyLock}; -/// Cached parsed programs (LRU). -pub static CACHED_PROGRAMS: LazyLock>>> = - LazyLock::new(|| { - Mutex::new(LruCache::new( - NonZeroUsize::new(MAX_CACHED_PROGRAMS).expect("MAX_CACHED_PROGRAMS must be non-zero"), - )) - }); +/// Cached parsed programs. +pub static CACHED_PROGRAMS: LazyLock> = LazyLock::new(Default::default); /// Set the parsed program. pub fn set(hash: OpaqueHash, program: ParsedProgram) { - if let Ok(mut cache) = CACHED_PROGRAMS.try_lock() { - cache.put(hash, Arc::new(program)); - } + CACHED_PROGRAMS.put(hash, Arc::new(program)); } /// Get the parsed program. pub fn get(hash: OpaqueHash) -> Option> { - if let Ok(mut cache) = CACHED_PROGRAMS.try_lock() { - return cache.get(&hash).cloned(); - } - None + CACHED_PROGRAMS.get(&hash) } /// The parsed program. diff --git a/crates/vm/spacevm/Cargo.toml b/crates/vm/spacevm/Cargo.toml index 119fdfb0..d6e1fd48 100644 --- a/crates/vm/spacevm/Cargo.toml +++ b/crates/vm/spacevm/Cargo.toml @@ -6,7 +6,6 @@ edition.workspace = true [dependencies] anyhow.workspace = true hex.workspace = true -lru.workspace = true pvm.workspace = true pvmc.workspace = true pvmi.workspace = true diff --git a/crates/vm/spacevm/src/lib.rs b/crates/vm/spacevm/src/lib.rs index f76110eb..0f93d15c 100644 --- a/crates/vm/spacevm/src/lib.rs +++ b/crates/vm/spacevm/src/lib.rs @@ -1,45 +1,21 @@ //! Jastime - JAM virtual machine use anyhow::Result; -use lru::LruCache; pub use pvm; use pvm::{ - Argument, Invocation, Invoked, Pvm, State, parser, + Argument, Cache, Invocation, Invoked, Pvm, State, parser, score::{Gas, OpaqueHash}, }; pub use pvmc::{Artifact, Compiler, Memory, ModuleLike, SPACEJAM_CACHE_DIR}; pub use pvmi::Interpreter; -use std::{ - collections::BTreeSet, - num::NonZeroUsize, - sync::{Arc, LazyLock, Mutex, RwLock}, -}; - -/// Default max modules kept in memory. -const DEFAULT_MODULE_CACHE: usize = 8; - -/// Effective cache size. -fn module_cache_size() -> NonZeroUsize { - std::env::var("SPACEJAM_MODULE_CACHE") - .ok() - .and_then(|s| s.parse::().ok()) - .and_then(NonZeroUsize::new) - .unwrap_or_else(|| NonZeroUsize::new(DEFAULT_MODULE_CACHE).expect("nonzero default")) -} - -/// Cached modules (LRU). Uses Mutex because LruCache::get requires &mut for LRU tracking. -pub static SPACEVM_MODULES: LazyLock>>> = - LazyLock::new(|| Mutex::new(LruCache::new(module_cache_size()))); +use std::sync::{Arc, LazyLock}; -/// Locks for the Jastime compilation -pub static SPACEVM_LOCKS: LazyLock>> = - LazyLock::new(|| RwLock::new(BTreeSet::new())); +/// Cached AOT modules. +pub static SPACEVM_MODULES: LazyLock> = LazyLock::new(Default::default); /// SpaceVM - JAM virtual machine pub struct SpaceVM; -impl Pvm for SpaceVM {} - impl Invocation for SpaceVM { fn invoke2( mut ctx: X, @@ -49,20 +25,14 @@ impl Invocation for SpaceVM { gas: Gas, pc: usize, ) -> Invoked { - let module = if let Ok(true) = SPACEVM_LOCKS.read().map(|lock| !lock.contains(&hash)) - && let Ok(Some(module)) = SPACEVM_MODULES - .lock() - .map(|mut cache| cache.get(&hash).cloned()) - { + let module = if let Some(module) = SPACEVM_MODULES.get(&hash) { Some(module) } else if let Ok(module) = ::new::() && let Ok(program) = parser::program::preimage(code.clone(), &args) && let Ok(Some(module)) = ModuleLike::try_load(module, &program) { let arc = Arc::new(module); - if let Ok(mut cache) = SPACEVM_MODULES.lock() { - cache.put(hash, arc.clone()); - } + SPACEVM_MODULES.put(hash, arc.clone()); Some(arc) } else { None @@ -106,19 +76,15 @@ impl Invocation for SpaceVM { }; } - // lock the compilation + // Kick off background AOT compile. { - if let Ok(locks) = SPACEVM_LOCKS.read() - && !locks.contains(&hash) - { - let code = code.clone(); - let args = args.clone(); - rayon::spawn(move || { - if let Err(e) = self::compile::(code, args, hash, true) { - tracing::debug!("failed to compile program: {e:?}"); - } - }); - } + let code = code.clone(); + let args = args.clone(); + rayon::spawn(move || { + if let Err(e) = self::compile::(code, args, hash, true) { + tracing::debug!("failed to compile program: {e:?}"); + } + }); } // fallback to the interpreter @@ -126,6 +92,8 @@ impl Invocation for SpaceVM { } } +impl Pvm for SpaceVM {} + /// Compile a program pub fn compile( code: Vec, @@ -133,25 +101,17 @@ pub fn compile( hash: OpaqueHash, memcache: bool, ) -> Result<()> { - if let Ok(mut locks) = SPACEVM_LOCKS.write() { - locks.insert(hash); - } - match ::new::()? .compile(&parser::program::preimage(code, &args).expect("failed to preimage")) { Ok(module) => { - if memcache && let Ok(mut cache) = SPACEVM_MODULES.lock() { - cache.put(hash, Arc::new(module)); + if memcache { + SPACEVM_MODULES.put(hash, Arc::new(module)); } } Err(err) => { tracing::debug!("failed to compile program: {:?}", err); } } - - if let Ok(mut locks) = SPACEVM_LOCKS.write() { - locks.remove(&hash); - } Ok(()) } diff --git a/crates/vm/src/cache.rs b/crates/vm/src/cache.rs new file mode 100644 index 00000000..7b296fc7 --- /dev/null +++ b/crates/vm/src/cache.rs @@ -0,0 +1,34 @@ +//! Lock-free read-mostly cache keyed by program hash. + +use arc_swap::ArcSwap; +use score::OpaqueHash; +use std::{collections::HashMap, sync::Arc}; + +/// Lock-free hash-keyed cache holding `Arc`. +pub struct Cache { + inner: ArcSwap>>, +} + +impl Cache { + /// Look up by hash; returns a cheap `Arc` clone on hit. + pub fn get(&self, hash: &OpaqueHash) -> Option> { + self.inner.load().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()); + next + }); + } +} + +impl Default for Cache { + fn default() -> Self { + Self { + inner: ArcSwap::from_pointee(HashMap::new()), + } + } +} diff --git a/crates/vm/src/lib.rs b/crates/vm/src/lib.rs index 5b966189..0fb1a616 100644 --- a/crates/vm/src/lib.rs +++ b/crates/vm/src/lib.rs @@ -5,6 +5,7 @@ pub use parser::{ }; pub use { account::Account, + cache::Cache, codec, context::{check_range, Argument, Context, Executed, Invoked, MemoryLike, State}, invocation::{AccumulateContext, AccumulateState, Accumulated, Invocation}, @@ -22,6 +23,7 @@ macro_rules! bail { }; } +mod cache; mod context; pub mod host; pub mod invocation; From 22c51b1ae69f11d50b8c6946d558d804d3b9fc6d Mon Sep 17 00:00:00 2001 From: clearloop Date: Sun, 24 May 2026 02:01:01 +0800 Subject: [PATCH 23/23] chore(docker): bake interpreter only image for fuzz --- Cargo.lock | 38 +++++++++++++-------------- Cargo.toml | 2 +- Makefile | 15 +++++++++++ crates/runtime/src/tx/block/header.rs | 3 +-- crates/runtime/src/tx/ticket/mod.rs | 4 ++- crates/spacejam/src/cmd/fuzz.rs | 2 +- crates/spacejam/src/fuzz/env.rs | 17 +++++++++--- crates/testing/src/preimage.rs | 3 ++- docker/spacejam.dockerfile | 9 +++++-- 9 files changed, 63 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 467c59eb..c49f15e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1030,7 +1030,7 @@ dependencies = [ [[package]] name = "cranelift-pvm" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "cranelift", @@ -2787,7 +2787,7 @@ dependencies = [ [[package]] name = "pvmc" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "bindgen", @@ -3323,7 +3323,7 @@ dependencies = [ [[package]] name = "serde-jam" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "serde", @@ -3491,7 +3491,7 @@ dependencies = [ [[package]] name = "spacejam" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "async-trait", @@ -3528,7 +3528,7 @@ dependencies = [ [[package]] name = "spacejam-account" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "serde-jam", @@ -3539,7 +3539,7 @@ dependencies = [ [[package]] name = "spacejam-core" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "erased-serde", @@ -3594,7 +3594,7 @@ dependencies = [ [[package]] name = "spacejam-network" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "clap", @@ -3617,7 +3617,7 @@ dependencies = [ [[package]] name = "spacejam-offchain" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "async-trait", @@ -3633,7 +3633,7 @@ dependencies = [ [[package]] name = "spacejam-rpc" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "jsonrpsee", @@ -3645,7 +3645,7 @@ dependencies = [ [[package]] name = "spacejam-runtime" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "hex", @@ -3666,7 +3666,7 @@ dependencies = [ [[package]] name = "spacejam-service" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "blake2b_simd", @@ -3680,14 +3680,14 @@ dependencies = [ [[package]] name = "spacejam-spec" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "serde", ] [[package]] name = "spacejam-testing" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "hex", @@ -3717,7 +3717,7 @@ dependencies = [ [[package]] name = "spacejam-testnet" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "clap", @@ -3728,7 +3728,7 @@ dependencies = [ [[package]] name = "spacejam-worker" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "pvm", @@ -3744,7 +3744,7 @@ dependencies = [ [[package]] name = "spacejson" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "hex", @@ -3755,7 +3755,7 @@ dependencies = [ [[package]] name = "spacejson-derive" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "proc-macro2", "quote", @@ -3764,7 +3764,7 @@ dependencies = [ [[package]] name = "spacevm" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "hex", @@ -3778,7 +3778,7 @@ dependencies = [ [[package]] name = "spacevm-export" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "pvm", "pvmc", diff --git a/Cargo.toml b/Cargo.toml index 6a7249e0..8a562513 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ resolver = "1" [workspace.package] edition = "2024" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" homepage = "https://spacejam.app" repository = "https://github.com/spacejamapp/jade" license = "GPL-3.0" diff --git a/Makefile b/Makefile index e0eac7d9..41d1054a 100644 --- a/Makefile +++ b/Makefile @@ -70,7 +70,22 @@ docker: linux-amd64-both -t $(DOCKER_IMAGE):$(VERSION) \ . +# build the fuzz-paired docker images (regular + interpreter) for AOT-vs-int +# A/B comparison on NUMA hosts. +fuzz: docker + docker build --platform=linux/amd64 \ + --build-arg SPACEJAM_INTERP=1 \ + -f docker/spacejam.dockerfile \ + -t $(DOCKER_IMAGE):int \ + -t $(DOCKER_IMAGE):$(VERSION)-int \ + . + # push images to ghcr dpush: docker push $(DOCKER_IMAGE):latest docker push $(DOCKER_IMAGE):$(VERSION) + +# push fuzz-paired images (regular + interpreter) to ghcr +fpush: dpush + docker push $(DOCKER_IMAGE):int + docker push $(DOCKER_IMAGE):$(VERSION)-int diff --git a/crates/runtime/src/tx/block/header.rs b/crates/runtime/src/tx/block/header.rs index c06929ad..e7c6fe2d 100644 --- a/crates/runtime/src/tx/block/header.rs +++ b/crates/runtime/src/tx/block/header.rs @@ -55,8 +55,7 @@ pub fn validate(state: &State, header: &Header) -> anyhow::Result<()> { }; let key = if new_epoch { - let TicketsOrKeys::Keys(keys) = TicketsOrKeys::fallback(&vals, state.entropy[1]) - else { + let TicketsOrKeys::Keys(keys) = TicketsOrKeys::fallback(&vals, state.entropy[1]) else { anyhow::bail!("invalid series"); }; keys[slot] diff --git a/crates/runtime/src/tx/ticket/mod.rs b/crates/runtime/src/tx/ticket/mod.rs index 5ce4cd63..4ce2f7fe 100644 --- a/crates/runtime/src/tx/ticket/mod.rs +++ b/crates/runtime/src/tx/ticket/mod.rs @@ -72,7 +72,9 @@ pub fn safrole( let epoch = tau / score::EPOCH_LENGTH; let next_epoch = slot / score::EPOCH_LENGTH; let new_epoch: bool = next_epoch > epoch; - if let Some(series) = self::sealing_key_series(tau, slot, entropy, &safrole, &validators.current) { + if let Some(series) = + self::sealing_key_series(tau, slot, entropy, &safrole, &validators.current) + { safrole.series = series; } if new_epoch { diff --git a/crates/spacejam/src/cmd/fuzz.rs b/crates/spacejam/src/cmd/fuzz.rs index 7ef4031e..e05b4ce3 100644 --- a/crates/spacejam/src/cmd/fuzz.rs +++ b/crates/spacejam/src/cmd/fuzz.rs @@ -16,7 +16,7 @@ pub enum Fuzz { socket: PathBuf, /// If use interpreter instead - #[clap(short, long)] + #[clap(short, long, env = "SPACEJAM_INTERP")] interp: bool, }, diff --git a/crates/spacejam/src/fuzz/env.rs b/crates/spacejam/src/fuzz/env.rs index c4dde299..91484b7d 100644 --- a/crates/spacejam/src/fuzz/env.rs +++ b/crates/spacejam/src/fuzz/env.rs @@ -15,6 +15,7 @@ const JAM_FUZZ_SPEC: &str = "JAM_FUZZ_SPEC"; const JAM_FUZZ_DATA_PATH: &str = "JAM_FUZZ_DATA_PATH"; const JAM_FUZZ_SOCK_PATH: &str = "JAM_FUZZ_SOCK_PATH"; const JAM_FUZZ_LOG_LEVEL: &str = "JAM_FUZZ_LOG_LEVEL"; +const SPACEJAM_INTERP: &str = "SPACEJAM_INTERP"; /// Whether the env-driven fuzz mode is requested. pub fn is_active() -> bool { @@ -26,8 +27,7 @@ pub async fn run() -> Result<()> { init_logger(std::env::var(JAM_FUZZ_LOG_LEVEL).ok().as_deref()); let cfg = Config::from_env()?; log_runtime_env(&cfg); - // Use the compiler on linux; Target::serve falls back to interp on other platforms. - Target::serve(&cfg.socket, /*interp=*/ false).await + Target::serve(&cfg.socket, cfg.interp).await } /// Log target config (spec / data_path / socket) and host hardware (CPU model, @@ -44,8 +44,13 @@ fn log_runtime_env(cfg: &Config) { .unwrap_or_else(|| "unknown".to_string()); let cores = sys.cpus().len(); let ram_gb = sys.total_memory() as f64 / (1024.0 * 1024.0 * 1024.0); + let vm = if cfg.interp { + "interpreter" + } else { + "compiler" + }; tracing::info!( - "fuzz target starting: spec={}, data_path={}, socket={}, vm=compiler, cpu={cpu:?} ({cores} cores), ram={ram_gb:.1} GB", + "fuzz target starting: spec={}, data_path={}, socket={}, vm={vm}, cpu={cpu:?} ({cores} cores), ram={ram_gb:.1} GB", cfg.spec.as_str(), cfg.data_path.display(), cfg.socket.display(), @@ -56,6 +61,7 @@ struct Config { spec: Spec, data_path: PathBuf, socket: PathBuf, + interp: bool, } impl Config { @@ -63,10 +69,15 @@ impl Config { let spec = Spec::from_str(&require_env(JAM_FUZZ_SPEC)?)?; let data_path = PathBuf::from(require_env(JAM_FUZZ_DATA_PATH)?); let socket = PathBuf::from(require_env(JAM_FUZZ_SOCK_PATH)?); + let interp = match std::env::var(SPACEJAM_INTERP).as_deref() { + Ok("" | "0" | "false") | Err(_) => false, + Ok(_) => true, + }; Ok(Self { spec, data_path, socket, + interp, }) } } diff --git a/crates/testing/src/preimage.rs b/crates/testing/src/preimage.rs index bc425be9..f143b46c 100644 --- a/crates/testing/src/preimage.rs +++ b/crates/testing/src/preimage.rs @@ -22,7 +22,8 @@ pub fn run(test: &specjam::Test) -> anyhow::Result<()> { assert_eq!(input.pre_state, output.post_state); return Ok(()); } - let accounts = tx::preimage::accounts(input.input.slot, input.input.preimages.clone(), accounts); + let accounts = + tx::preimage::accounts(input.input.slot, input.input.preimages.clone(), accounts); assert_eq!( accounts .accounts() diff --git a/docker/spacejam.dockerfile b/docker/spacejam.dockerfile index 207a81d5..64629444 100644 --- a/docker/spacejam.dockerfile +++ b/docker/spacejam.dockerfile @@ -11,12 +11,17 @@ # JAM_FUZZ_SOCK_PATH Unix domain socket path for fuzzer communication. # JAM_FUZZ_LOG_LEVEL Optional. error | warn | info | debug | trace. # -# Build via `make docker` (builds both tiny and full first). +# Build args: +# SPACEJAM_INTERP Set to 1 to bake an interpreter-only image (used by +# `make fuzz` for AOT-vs-interpreter A/B on NUMA hosts). +# +# Build via `make docker` (regular) or `make fuzz` (regular + interpreter). FROM debian:bookworm-slim +ARG SPACEJAM_INTERP="" COPY target/x86_64-unknown-linux-gnu/prod/spacejam-tiny /usr/local/bin/spacejam-tiny COPY target/x86_64-unknown-linux-gnu/prod/spacejam-full /usr/local/bin/spacejam-full COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh -ENV SPACEJAM_MODULE_CACHE=64 +ENV SPACEJAM_INTERP=${SPACEJAM_INTERP} ENTRYPOINT ["entrypoint.sh"]