Skip to content

feat: support no_std in stateless-core#126

Merged
flyq merged 10 commits into
mainfrom
liquan/no_std-rebased
Apr 27, 2026
Merged

feat: support no_std in stateless-core#126
flyq merged 10 commits into
mainfrom
liquan/no_std-rebased

Conversation

@flyq
Copy link
Copy Markdown
Member

@flyq flyq commented Apr 24, 2026

Supersedes #125 — that branch had ~30 commits of pre-squash refactor4 history left over after PR #122 was squash-merged, which produced phantom merge conflicts against main. This branch contains exactly the same no_std work, rebased cleanly onto current main (git rebase --onto origin/main b84ef2e^), so only the 4 real no_std commits remain.

Summary

Make stateless-core a true no_std crate so it can be consumed in constrained environments (zkVMs, bare-metal). salt and mega-evm already support no_std; this mirrors their pattern. Every public module except pipeline compiles under --no-default-features on bare-metal targets.

Changes

stateless-core becomes no_std

  • Added #![cfg_attr(not(feature = "std"), no_std)] and the salt-style #[cfg(not(feature = "std"))] extern crate alloc as std; alias so existing std::vec::Vec / std::sync::Arc / std::collections::BTreeMap paths keep resolving via alloc.
  • New std feature (default-on) gates the std-only bits:
    • pipeline module (tokio / kanal / num_cpus / CancellationToken)
    • executor's trace_writer: Option<Box<dyn io::Write>> parameter, Instant/SystemTime timing, and ValidationResult.completed_at field
  • std feature forwards to upstream {dep}/std where applicable (mega-evm/default, revm/default, salt/default, alloy/reth */std…), so perf-critical features (salt parallel/rayon, revm secp256k1/blst/
    c-kzg/portable/tracer) stay enabled on the std side.

no_std-compatible public surface

All modules except pipeline compile in no_std mode: chain_spec, data_types, db, evm_database, executor, light_witness, withdrawals.

Supporting swaps:

  • std::collections::HashMap<B256, Arc<Bytecode>>alloy_primitives::map::HashMap<…> in ContractLookup, WitnessDatabase, and validate_block's signature. Cascaded through stateless-db,
    binaries, and test fixtures (HashMap::new()HashMap::default() where hasher inference would otherwise pick RandomState).
  • std::error::Errorcore::error::Error (StoreError::Backend, StoreResultExt bound).
  • rustc_hash::FxHashMap → local type FxHashMap<K, V> = hashbrown::HashMap<K, V, FxBuildHasher>; so LightWitness.levels matches salt::SaltWitness.proof.levels (which is also hashbrown-based).
  • reth_trie::Nibblesreth_trie_common::Nibbles in withdrawals.rs; drops the heavy reth-trie dep.
  • op_alloy_network::{TransactionResponse, eip2718::Encodable2718}alloy_network_primitives::TransactionResponse + alloy_eips::eip2718::Encodable2718. op-alloy-network removed (it transitively pulled alloy-providergetrandom, which breaks bare-metal).
  • eyre made optional = true, gated behind std. (eyre was the last dep silently enabling once_cell/defaultonce_cell/std, which fails on bare-metal.)

chain_spec unified on mega_mainnet_hardforks()

Dropped the pub static MEGA_MAINNET_HARDFORKS: LazyLock<ChainHardforks> (std-only). Replaced with pub fn mega_mainnet_hardforks() -> ChainHardforks which works in both modes. ChainSpec::from_genesis already consumed the function; no other callers.

Workspace Cargo.toml

  • default-features = false added to alloy-hardforks, alloy-op-hardforks, mega-evm, salt, op-alloy-network, rustc-hash, thiserror.
  • serde / serde_json gain features = ["alloc", "derive"/"alloc"] so they compile in no_std.
  • New alloy-network-primitives, hashbrown workspace deps.

Test plan

  • cargo check -p stateless-core (std default)
  • cargo check -p stateless-core --no-default-features (host no_std)
  • cargo check -p stateless-core --no-default-features --target riscv64imac-unknown-none-elf
  • cargo build -p stateless-core --no-default-features --target riscv64imac-unknown-none-elf (real codegen)
  • cargo build -p stateless-core --no-default-features --target wasm32-unknown-unknown (second bare-metal target)
  • cargo build --workspace
  • cargo test --workspace — all suites pass
  • cargo test -p stateless-validator --test integration — 0.70s (baseline ~0.65s, no regression)
  • cargo clippy --workspace --all-targets --all-features
  • cargo fmt --all --check
  • cargo sort --check --workspace --grouped
  • Only pipeline remains #[cfg(feature = "std")]-gated in lib.rs

flyq and others added 4 commits April 24, 2026 17:32
Tests hit three no_std gaps that only surfaced once the coverage script
(`scripts/coverage_stateless_core.sh`) started building them with the
`std` feature off:

- `vec!` macro isn't in scope under no_std — add `use std::vec;` to the
  `withdrawals` and `data_types` test modules.
- `ToString::to_string` needs the trait in scope — add
  `use std::string::ToString;` to the `chain_spec` and `db` test modules.
- `validate_block`'s `trace_writer` param is `#[cfg(feature = "std")]`;
  cfg-gate the matching `None` argument in the executor mainnet-fixture
  test so the call compiles in both modes.

No production code changed. Fixes the Coverage CI job.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented Apr 24, 2026

The enhancement label appears to be missing from this PR.

This PR adds no_std support to stateless-core (a new capability, not a bug fix), which matches the enhancement label description: "New feature or request". The conventional-commits prefix in the title (feat:) also confirms it is a feature addition.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 24, 2026

Codecov Report

❌ Patch coverage is 95.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.9%. Comparing base (840b3d9) to head (cc8cf51).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
crates/stateless-core/src/executor.rs 94.4% 1 Missing ⚠️
crates/stateless-core/src/light_witness.rs 0.0% 1 Missing ⚠️

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 24, 2026

Clean no_std port. The extern crate alloc as std aliasing approach mirrors the upstream salt pattern correctly, the feature-gating is consistent throughout, and the test plan (including bare-metal riscv64 and wasm32 codegen) is thorough.

Two things worth noting:

ValidationStats timing fields are silently zeroed in no_std. witness_verification_time, block_replay_time, and salt_update_time are set to 0.0 when compiled without std. Since the same ValidationStats type is returned in both modes, a consumer that logs or meters these values will silently see zeros — there's no way to distinguish "very fast" from "not measured". Adding a // no_std: unmeasured comment on each 0.0_f64 binding would make this explicit for future readers.

ValidationResult.completed_at shape change is a semver break. Any code that constructs or destructures ValidationResult as a struct literal without a #[cfg(feature = "std")] guard will fail to compile in no_std contexts. Since this is only consumed inside the binary (which always has std), it's not a practical problem today, but it's worth documenting in the field doc comment that the field is only present with the std feature — the updated doc comment in the diff already moves in this direction.

Both are nits — the PR improves the codebase and the no_std support is correct.

Comment thread crates/stateless-core/src/executor.rs
@claude
Copy link
Copy Markdown

claude Bot commented Apr 24, 2026

Clean no_std port. The extern crate alloc as std aliasing pattern is idiomatic (mirrors how salt and mega-evm do it), the feature surface is fully gated, and the test plan covers both host and bare-metal codegen (RISC-V, WASM).

A few observations:

Timing fields in ValidationStats (existing thread, still open): witness\_verification\_time, block\_replay\_time, and salt\_update\_time are silently 0.0 in no_std builds. Callers (metrics reporters, loggers) have no way to know the values are meaningless. Please add a doc comment to the struct explaining the no_std caveat, or change the fields to Option<f64> (requires no feature gating at the struct level, consumers just get None).

ValidationResult::completed\_at is conditionally compiled: the field is absent in no_std, so any downstream code that reads .completed\_at will fail to compile without std. This is intentional per the PR description — just worth calling out for downstream consumers.

salt bumped v1.0.1 → v1.0.2 as part of this PR. The diff shows hugepage-rs dropped, salt-macros added, and hashbrown added to ipa-multipoint. Fine to bundle with the no_std work since it's a required upstream no_std fix, but it's two changes in one PR.

Everything else is mechanical and correct. LGTM pending the timing-fields nit.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 24, 2026

Overall this is a clean, well-prepared PR. The approach mirrors the salt/mega-evm pattern, the test plan is thorough (including actual bare-metal codegen), and the dependency surgery (dropping reth-trie, op-alloy-network, eyre from non-std builds) is well-motivated.

One nit inline on stateless-test-utils/src/fixtures.rs. Everything else looks good to me.

Comment thread crates/stateless-test-utils/src/fixtures.rs
@claude
Copy link
Copy Markdown

claude Bot commented Apr 24, 2026

Overall this is clean and well-structured. The approach (aliasing alloc as std, feature-gating pipeline/timing/io::Write) mirrors the upstream salt/mega-evm pattern, the test plan is thorough (two bare-metal targets), and no correctness issues jump out.

A few observations:

mega-evm/default in the std feature list — using /default instead of /std is intentional (to carry through rayon/crypto features that live under mega-evm's own default gate), but it's worth a comment in the feature list so the next reader doesn't assume it's a mistake.

#[cfg(feature = "std")] trace_writer parameter — cfg-gating a positional argument is valid Rust but uncommon; every call site now needs the same annotation (visible in the test at executor.rs). The PR description acknowledges this, and there's no clean alternative given the no_std constraint, so this is a known tradeoff rather than a defect.

default() + reserve() in cache.rs — the old HashMap::with_capacity(n) should still compile against alloy_primitives::map::HashMap since foldhash::fast::RandomState implements Default. nit: the two-step pattern is fine but HashMap::with_capacity(hashes.len()) is more idiomatic.

Nothing blocks merge.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 24, 2026

LGTM. The no_std implementation is clean and follows established patterns from the upstream salt/mega-evm crates.

A few notes:

  • Previous thread resolved: the fixture HashMap inconsistency flagged earlier is addressed in this diff — all four fields in TestFixtures now use alloy_primitives::map::HashMap.
  • mega-evm/default / salt/default / revm/default in the std feature list looks intentional (those crates expose std behaviour through their default feature rather than a separate std one), but worth a one-line comment in Cargo.toml so future readers don't "fix" it.
  • The extern crate alloc as std aliasing lets existing std:: paths resolve through alloc without a grep-and-replace; the approach is sound given the module list (no std::sync::Mutex, no std::io outside #[cfg(feature = "std")] guards).
  • Dropping reth-trie in favour of reth-trie-common::Nibbles (already a transitive dep) is a nice dep-graph simplification.

The CI no-std job covers host no_std, riscv64, and wasm32 — sufficient signal for the compile-time guarantee this PR targets.

Comment thread .github/workflows/build-and-test.yml
@claude
Copy link
Copy Markdown

claude Bot commented Apr 24, 2026

The approach is solid — the salt-style extern crate alloc as std; alias keeps churn low, the feature propagation list is complete, and the CI job covers both a RISC-V bare-metal target and WASM. Previous review comments are addressed.

One nit in the CI job (inline). Otherwise LGTM.

Comment thread crates/stateless-core/src/executor.rs Outdated
@claude
Copy link
Copy Markdown

claude Bot commented Apr 27, 2026

Overall this is clean and well-structured. The reth-trie removal is a nice bonus that drops two transitive deps (reth-trie, reth-stages-types). The CI no_std job is a good gate.

One design concern worth discussing before merge:

extern crate alloc as std in lib.rs — this aliases alloc as std so existing use std::vec::Vec / use std::boxed::Box etc. resolve under no_std. It works today because every std import in the crate is from an alloc-available module. But it's invisible to future contributors: someone adding use std::io or use std::sync::Mutex without realising they need a #[cfg(feature = "std")] guard will get a cryptic alloc::io not-found error rather than the expected "std is not available" message. The CI no_std check catches regressions, but the failure message is confusing.

The idiomatic alternative is explicit use alloc::... in no_std paths (i.e., #[cfg(not(feature = "std"))] use alloc::vec::Vec or extern crate alloc; + unambiguous imports). That said, the alias trick is widespread in embedded-Rust crates and the CI job provides a real safety net, so this is a judgement call.

Everything else looks correct — the ValidationResult removal, mega_mainnet_hardforks() conversion, HashMap unification, and feature-gating of the pipeline/tokio layer are all well done.

@flyq flyq merged commit 686f3ba into main Apr 27, 2026
33 checks passed
@flyq flyq deleted the liquan/no_std-rebased branch April 27, 2026 13:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants