Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/pr_nostd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ jobs:
run: |
TARGET=.github/targets/riscv64im_zicclsm-unknown-none-elf.json
cargo +nightly-2026-06-29 -Z json-target-spec -Z build-std=core,alloc check \
-p ethrex-rlp -p ethrex-crypto --no-default-features --target "$TARGET"
-p ethrex-rlp -p ethrex-crypto -p ethrex-trie --no-default-features --target "$TARGET"
cargo +nightly-2026-06-29 -Z json-target-spec -Z build-std=core,alloc check \
-p ethrex-crypto --no-default-features --features kzg-rs --target "$TARGET"
# `eip-8025` currently gates no code in ethrex-trie (empty stub kept for the
# ethrex-common feature chain). This mirrors the guest's feature set and
# guards future eip-8025-gated trie code staying no_std-clean.
cargo +nightly-2026-06-29 -Z json-target-spec -Z build-std=core,alloc check \
-p ethrex-trie --no-default-features --features eip-8025 --target "$TARGET"
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions crates/blockchain/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ use ethrex_common::types::{EIP7702_DELEGATED_CODE_LEN, is_eip7702_delegation};
use ethrex_common::types::{ELASTICITY_MULTIPLIER, P2PTransaction};
use ethrex_common::types::{Fork, MempoolTransaction};
use ethrex_common::utils::keccak;
use ethrex_common::{Address, H256, TrieLogger, U256};
use ethrex_common::{Address, H256, U256};
pub use ethrex_common::{
get_total_blob_gas, validate_block_access_list_hash, validate_block_pre_execution,
validate_gas_used, validate_receipts_root_and_logs_bloom, validate_requests_hash,
Expand All @@ -89,7 +89,7 @@ use ethrex_storage::{
AccountUpdatesList, Store, UpdateBatch, error::StoreError, hash_address, hash_key,
};
use ethrex_trie::node::{BranchNode, ExtensionNode, LeafNode};
use ethrex_trie::{Nibbles, Node, NodeRef, Trie, TrieError, TrieNode};
use ethrex_trie::{Nibbles, Node, NodeRef, Trie, TrieError, TrieLogger, TrieNode};
use ethrex_vm::backends::CachingDatabase;
#[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
use ethrex_vm::backends::levm::LEVM;
Expand Down
10 changes: 9 additions & 1 deletion crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@ repository.workspace = true

[dependencies]
ethrex-rlp.workspace = true
ethrex-trie.workspace = true
# Inlined (path + version) instead of `workspace = true` so we can set
# `default-features = false`: Cargo ignores (with a warning) an override of a
# workspace dependency's default-features unless the root declaration also disables
# them, and the root declares `ethrex-trie` with defaults on. Keeping `ethrex-trie` no_std here means
# the zkVM guest (which reaches the trie only through this crate) gets the
# non-atomic `OnceLock`. Host builds re-enable `ethrex-trie/std` via the direct
# `ethrex-trie` deps in storage/p2p/rpc/blockchain/l2 (Cargo unifies the feature
# on). Keep the version in sync with the workspace root.
ethrex-trie = { path = "trie", version = "19.0.0", default-features = false }
ethrex-crypto.workspace = true

tracing.workspace = true
Expand Down
1 change: 0 additions & 1 deletion crates/common/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ pub mod types;
pub mod validation;
pub use bytes::Bytes;
pub mod base64;
pub use ethrex_trie::{TrieLogger, TrieWitness};
pub mod errors;
pub mod evm;
pub mod fd_limit;
Expand Down
47 changes: 35 additions & 12 deletions crates/common/trie/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,45 @@ description = "Ethereum Merkle Patricia Trie for the ethrex Ethereum execution c
repository.workspace = true

[dependencies]
ethrex-crypto.workspace = true
ethrex-crypto = { workspace = true, default-features = false }
ethrex-rlp.workspace = true

ethereum-types.workspace = true
anyhow = "1.0.86"
bytes.workspace = true
thiserror.workspace = true
serde.workspace = true
lazy_static.workspace = true
crossbeam.workspace = true
rayon.workspace = true
rustc-hash.workspace = true
rkyv.workspace = true
# Inlined with explicit versions instead of `workspace = true`: Cargo ignores
# (with a warning) a member setting `default-features = false` unless the workspace
# declaration also disables them, and these are declared with defaults on for the
# rest of the workspace. Disabling defaults is required for no_std; the `std` feature below
# re-enables each. Keep these versions in sync with the workspace root.
ethereum-types = { version = "0.15.1", default-features = false, features = ["serialize"] }
anyhow = { version = "1.0.86", default-features = false }
bytes = { version = "1.6.0", default-features = false, features = ["serde"] }
thiserror = { version = "2.0.9", default-features = false }
serde = { version = "1.0.203", default-features = false, features = ["derive", "rc", "alloc"] }
rustc-hash = { version = "2.1.1", default-features = false }
rkyv = { version = "0.8.10", default-features = false, features = ["alloc", "unaligned", "bytecheck"] }

# no_std HashMap/HashSet, only compiled when `std` is off (replaces the std-only rustc-hash aliases)
hashbrown = { version = "0.15", default-features = false }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 hashbrown compiled in every build even when unused

hashbrown is an unconditional non-optional dependency, but its types (FxHashMap, FxHashSet) are only referenced under #[cfg(not(feature = "std"))]. In a std build Cargo compiles hashbrown even though no code uses it, adding unnecessary compile time. Making it optional (optional = true) and declaring it only active in the not(std) code path would avoid this overhead.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/common/trie/Cargo.toml
Line: 29

Comment:
**`hashbrown` compiled in every build even when unused**

`hashbrown` is an unconditional non-optional dependency, but its types (`FxHashMap`, `FxHashSet`) are only referenced under `#[cfg(not(feature = "std"))]`. In a `std` build Cargo compiles `hashbrown` even though no code uses it, adding unnecessary compile time. Making it optional (`optional = true`) and declaring it only active in the `not(std)` code path would avoid this overhead.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think making it optional is more trouble than it's worth:

  • it's already a transitive dependency
  • cargo can't activate conditional on a feature not being enabled

# no_std-friendly `Lazy` for EMPTY_TRIE_HASH (replaces lazy_static; compiled in all builds)
spin = { version = "0.9.8", default-features = false, features = ["lazy", "mutex", "spin_mutex"] }

# std-only: parallel merkleization (threadpool / trie_sorted / validate_parallel)
crossbeam = { workspace = true, optional = true }
rayon = { workspace = true, optional = true }

[features]
default = []
default = ["std"]
std = [
"ethrex-crypto/std",
"ethereum-types/std",
"anyhow/std",
"bytes/std",
"thiserror/std",
"serde/std",
"rustc-hash/std",
"rkyv/std",
"dep:crossbeam",
"dep:rayon",
]
eip-8025 = []

[lib]
Expand Down
75 changes: 53 additions & 22 deletions crates/common/trie/db.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
use ethereum_types::H256;
use ethrex_rlp::encode::RLPEncode;

use crate::{Nibbles, Node, Trie, error::TrieError};
use std::{
collections::BTreeMap,
sync::{Arc, Mutex},
};
use crate::{Nibbles, Node, error::TrieError};
use alloc::collections::BTreeMap;
use alloc::sync::Arc;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;

// `InMemoryTrieDB` is available in both builds. On `std` its map is guarded by
// `std::sync::Mutex`; on `no_std` (the zkVM guest) by `spin::Mutex`, which never
// contends because the guest is single-threaded. Only `from_nodes` stays host-only,
// by design rather than necessity: the guest never builds a trie from a node dump.
#[cfg(feature = "std")]
use crate::Trie;
#[cfg(feature = "std")]
use ethereum_types::H256;
#[cfg(not(feature = "std"))]
use spin::Mutex;
#[cfg(feature = "std")]
use std::sync::Mutex;

// Nibbles -> encoded node
pub type NodeMap = Arc<Mutex<BTreeMap<Vec<u8>, Vec<u8>>>>;

// Guard returned by locking a `NodeMap`; the backend mutex differs per build.
#[cfg(feature = "std")]
type NodeMapGuard<'a> = std::sync::MutexGuard<'a, BTreeMap<Vec<u8>, Vec<u8>>>;
#[cfg(not(feature = "std"))]
type NodeMapGuard<'a> = spin::MutexGuard<'a, BTreeMap<Vec<u8>, Vec<u8>>>;

pub trait TrieDB: Send + Sync {
fn get(&self, key: Nibbles) -> Result<Option<Vec<u8>>, TrieError>;
fn put_batch(&self, key_values: Vec<(Nibbles, Vec<u8>)>) -> Result<(), TrieError>;
Expand Down Expand Up @@ -66,6 +84,33 @@ impl InMemoryTrieDB {
}
}

fn apply_prefix(&self, path: Nibbles) -> Nibbles {
match &self.prefix {
Some(prefix) => prefix.concat(&path),
None => path,
}
}

// Locks the inner map, bridging the std (`Result`, poisonable) and spin
// (infallible, single-threaded guest) `Mutex::lock` APIs.
fn lock_inner(&self) -> Result<NodeMapGuard<'_>, TrieError> {
#[cfg(feature = "std")]
let guard = self.inner.lock().map_err(|_| TrieError::LockError)?;
#[cfg(not(feature = "std"))]
let guard = self.inner.lock();
Ok(guard)
}

// Do not remove or make private as we use this in ethrex-replay
pub fn inner(&self) -> NodeMap {
Arc::clone(&self.inner)
}
}

// `from_nodes` is host-only by design: nothing in it requires `std`, but the guest
// never builds a trie from a node dump, so gating it keeps the no_std surface small.
#[cfg(feature = "std")]
impl InMemoryTrieDB {
// Do not remove or make private as we use this in ethrex-replay
pub fn from_nodes(
root_hash: H256,
Expand All @@ -87,32 +132,18 @@ impl InMemoryTrieDB {
let in_memory_trie = Arc::new(Mutex::new(hashed_nodes));
Ok(Self::new(in_memory_trie))
}

fn apply_prefix(&self, path: Nibbles) -> Nibbles {
match &self.prefix {
Some(prefix) => prefix.concat(&path),
None => path,
}
}

// Do not remove or make private as we use this in ethrex-replay
pub fn inner(&self) -> NodeMap {
Arc::clone(&self.inner)
}
}

impl TrieDB for InMemoryTrieDB {
fn get(&self, key: Nibbles) -> Result<Option<Vec<u8>>, TrieError> {
Ok(self
.inner
.lock()
.map_err(|_| TrieError::LockError)?
.lock_inner()?
.get(self.apply_prefix(key).as_ref())
.cloned())
}

fn put_batch(&self, key_values: Vec<(Nibbles, Vec<u8>)>) -> Result<(), TrieError> {
let mut db = self.inner.lock().map_err(|_| TrieError::LockError)?;
let mut db = self.lock_inner()?;

for (key, value) in key_values {
let prefixed_key = self.apply_prefix(key);
Expand Down
6 changes: 4 additions & 2 deletions crates/common/trie/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#[cfg(not(feature = "std"))]
use alloc::{boxed::Box, string::String};
use ethereum_types::H256;
use ethrex_rlp::error::RLPDecodeError;
use thiserror::Error;
Expand Down Expand Up @@ -43,8 +45,8 @@ pub struct ExtensionNodeErrorData {
pub node_path: Nibbles,
}

impl std::fmt::Display for ExtensionNodeErrorData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Display for ExtensionNodeErrorData {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"Node with hash {:#x}, child of the Extension Node (hash {:#x}, prefix {:?}) on path {:?}",
Expand Down
20 changes: 11 additions & 9 deletions crates/common/trie/nibbles.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use std::{cmp, mem};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use core::{cmp, mem};

use ethrex_rlp::{
decode::RLPDecode,
Expand Down Expand Up @@ -45,7 +47,7 @@ unsafe fn expand_bytes_to_nibbles(bytes: &[u8], output: *mut u8) {
#[allow(unsafe_code)]
#[inline]
unsafe fn expand_bytes_to_nibbles_x86_64(bytes: &[u8], output: *mut u8) {
use std::arch::x86_64::*;
use core::arch::x86_64::*;

let n = bytes.len();
let mut i = 0usize;
Expand Down Expand Up @@ -111,7 +113,7 @@ unsafe fn expand_bytes_to_nibbles_x86_64(bytes: &[u8], output: *mut u8) {
#[allow(unsafe_code)]
#[inline]
unsafe fn expand_bytes_to_nibbles_aarch64(bytes: &[u8], output: *mut u8) {
use std::arch::aarch64::*;
use core::arch::aarch64::*;

let n = bytes.len();
let mut i = 0usize;
Expand Down Expand Up @@ -205,7 +207,7 @@ unsafe fn pack_nibble_pairs_x86_64(nibbles: &[u8], output: *mut u8) {
#[cfg(target_feature = "ssse3")]
// SAFETY: SSSE3 enabled at compile time; pointer arithmetic stays within bounds.
unsafe {
use std::arch::x86_64::*;
use core::arch::x86_64::*;
// Multiplier: weight = [16, 1] repeated → multiply even nibble by 16, odd by 1
let weights = _mm_set1_epi16(0x0110_u16 as i16); // bytes: [16, 1, 16, 1, ...]
while i + 32 <= n {
Expand Down Expand Up @@ -239,7 +241,7 @@ unsafe fn pack_nibble_pairs_x86_64(nibbles: &[u8], output: *mut u8) {
#[allow(unsafe_code)]
#[inline]
unsafe fn pack_nibble_pairs_aarch64(nibbles: &[u8], output: *mut u8) {
use std::arch::aarch64::*;
use core::arch::aarch64::*;

let n = nibbles.len();
let mut i = 0usize;
Expand Down Expand Up @@ -309,7 +311,7 @@ fn count_common_prefix(a: &[u8], b: &[u8]) -> usize {
#[allow(unsafe_code)]
#[inline]
unsafe fn count_common_prefix_x86_64(a: &[u8], b: &[u8]) -> usize {
use std::arch::x86_64::*;
use core::arch::x86_64::*;

let n = a.len().min(b.len());
let mut i = 0usize;
Expand Down Expand Up @@ -356,7 +358,7 @@ unsafe fn count_common_prefix_x86_64(a: &[u8], b: &[u8]) -> usize {
#[allow(unsafe_code)]
#[inline]
unsafe fn count_common_prefix_aarch64(a: &[u8], b: &[u8]) -> usize {
use std::arch::aarch64::*;
use core::arch::aarch64::*;

let n = a.len().min(b.len());
let mut i = 0usize;
Expand Down Expand Up @@ -436,8 +438,8 @@ impl Ord for Nibbles {
}
}

impl std::hash::Hash for Nibbles {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
impl core::hash::Hash for Nibbles {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.data.hash(state);
}
}
Expand Down
Loading
Loading