From 0b940893790cc539445918d4e025f8835b6b6b59 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:51:44 +0200 Subject: [PATCH] feat(primitives): implement `map` module (#743) * feat(primitives): implement `map` module * chore: nits * ci * chore: fixes, clippy * perf: override write_usize on stable * chore: updates * fix * feat: docs, disable hashbrown by default * ci: add more flags to cargo hack * feat: remove S generic from Fb* types Easier to change the implementation later on. * fix: keep hashbrown by default for no_std Ideally this is only a depedencny if `not(feature = "std")` but this is currently not something you can have with cargo features. --- .github/workflows/ci.yml | 25 ++- Cargo.toml | 6 + crates/core/Cargo.toml | 36 ++- crates/core/src/lib.rs | 4 +- crates/core/tests/sol.rs | 2 +- crates/dyn-abi/src/arbitrary.rs | 4 +- crates/json-abi/src/param.rs | 7 +- crates/primitives/Cargo.toml | 52 ++++- crates/primitives/src/lib.rs | 8 +- crates/primitives/src/{log.rs => log/mod.rs} | 5 +- .../src/{log_serde.rs => log/serde.rs} | 1 + crates/primitives/src/map/fixed.rs | 205 ++++++++++++++++++ crates/primitives/src/map/mod.rs | 129 +++++++++++ crates/primitives/src/signature/sig.rs | 20 +- crates/primitives/src/utils/mod.rs | 1 + .../sol-type-parser/src/state_mutability.rs | 4 +- scripts/check_features.sh | 7 + tests/core-sol/Cargo.toml | 2 +- tests/core-sol/src/lib.rs | 8 + 19 files changed, 470 insertions(+), 56 deletions(-) rename crates/primitives/src/{log.rs => log/mod.rs} (98%) rename crates/primitives/src/{log_serde.rs => log/serde.rs} (99%) create mode 100644 crates/primitives/src/map/fixed.rs create mode 100644 crates/primitives/src/map/mod.rs create mode 100755 scripts/check_features.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee18251e05..57457dfb83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,6 @@ jobs: os: ["ubuntu-latest", "windows-latest"] rust: [ "stable", - "beta", "nightly", "1.79", # MSRV ] @@ -43,12 +42,9 @@ jobs: flags: "--features json" # All features - os: "ubuntu-latest" - rust: "stable" - flags: "--all-features" - - os: "ubuntu-latest" - rust: "beta" + rust: "nightly" flags: "--all-features" - - os: "ubuntu-latest" + - os: "windows-latest" rust: "nightly" flags: "--all-features" steps: @@ -98,17 +94,28 @@ jobs: run: cargo check --workspace --target wasm32-unknown-unknown feature-checks: + name: features ${{ matrix.rust }} ${{ matrix.flags }} runs-on: ubuntu-latest timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + rust: ["stable", "nightly"] + flags: ["", "--all-targets"] steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} - uses: taiki-e/install-action@cargo-hack - uses: Swatinem/rust-cache@v2 with: cache-on-failure: true - name: cargo hack - run: cargo hack check --feature-powerset --depth 2 + run: | + args=(${{ matrix.flags }}) + [ ${{ matrix.rust }} == "stable" ] && args+=(--skip nightly) + ./scripts/check_features.sh "${args[@]}" check-no-std: name: check no_std ${{ matrix.features }} @@ -130,7 +137,7 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@nightly with: components: clippy - uses: Swatinem/rust-cache@v2 diff --git a/Cargo.toml b/Cargo.toml index ec488194a6..45563d763f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,12 +67,18 @@ paste = "1.0" num_enum = "0.7" thiserror = "1.0" +# crypto digest = "0.10" k256 = { version = "0.13", default-features = false } keccak-asm = { version = "0.1.0", default-features = false } tiny-keccak = { version = "2.0", default-features = false } sha3 = { version = "0.10.8", default-features = false } +# maps +hashbrown = { version = "0.14", default-features = false } +indexmap = { version = "2.5", default-features = false } +rustc-hash = { version = "2.0", default-features = false } + # misc allocative = { version = "0.3.2", default-features = false } alloy-rlp = { version = "0.3", default-features = false } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 88535dc014..74288adf1e 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -21,42 +21,58 @@ rustdoc-args = ["--cfg", "docsrs"] workspace = true [dependencies] -alloy-primitives = { workspace = true, default-features = false } +alloy-primitives.workspace = true -alloy-dyn-abi = { workspace = true, default-features = false, optional = true } -alloy-json-abi = { workspace = true, default-features = false, optional = true } -alloy-sol-types = { workspace = true, default-features = false, optional = true } +alloy-dyn-abi = { workspace = true, optional = true } +alloy-json-abi = { workspace = true, optional = true } +alloy-sol-types = { workspace = true, optional = true } -alloy-rlp = { workspace = true, default-features = false, optional = true } +alloy-rlp = { workspace = true, optional = true } [features] +default = [ + "std", + "alloy-primitives/default", + "alloy-dyn-abi?/default", + "alloy-json-abi?/default", + "alloy-sol-types?/default", + "alloy-rlp?/default", +] std = [ "alloy-primitives/std", "alloy-json-abi?/std", "alloy-dyn-abi?/std", "alloy-sol-types?/std", + "alloy-rlp?/std", ] +nightly = ["alloy-primitives/nightly"] dyn-abi = ["sol-types", "dep:alloy-dyn-abi"] json-abi = ["json", "serde", "dep:alloy-json-abi"] json = ["alloy-sol-types?/json"] sol-types = ["dep:alloy-sol-types"] -tiny-keccak = ["alloy-primitives/tiny-keccak"] -native-keccak = ["alloy-primitives/native-keccak"] asm-keccak = ["alloy-primitives/asm-keccak"] +native-keccak = ["alloy-primitives/native-keccak"] sha3-keccak = ["alloy-primitives/sha3-keccak"] +tiny-keccak = ["alloy-primitives/tiny-keccak"] + +map = ["alloy-primitives/map"] +map-hashbrown = ["alloy-primitives/map-hashbrown"] +map-indexmap = ["alloy-primitives/map-indexmap"] +map-fxhash = ["alloy-primitives/map-fxhash"] -postgres = ["std", "alloy-primitives/postgres"] getrandom = ["alloy-primitives/getrandom"] rand = ["alloy-primitives/rand"] rlp = ["alloy-primitives/rlp", "dep:alloy-rlp"] serde = ["alloy-primitives/serde"] +k256 = ["alloy-primitives/k256"] +eip712 = ["alloy-sol-types?/eip712-serde", "alloy-dyn-abi?/eip712"] + +postgres = ["std", "alloy-primitives/postgres"] arbitrary = [ "std", "alloy-primitives/arbitrary", "alloy-sol-types?/arbitrary", "alloy-dyn-abi?/arbitrary", ] -k256 = ["alloy-primitives/k256"] -eip712 = ["alloy-sol-types?/eip712-serde", "alloy-dyn-abi?/eip712"] diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 255555b2b2..1863c75345 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -23,7 +23,7 @@ pub use alloy_json_abi as json_abi; #[cfg(feature = "sol-types")] #[doc(inline)] pub use alloy_sol_types as sol_types; -#[cfg(all(doc, feature = "sol-types"))] // Show this re-export in docs instead of the wrapper below. +#[cfg(all(feature = "sol-types", doc))] // Show this re-export in docs instead of the wrapper below. #[doc(no_inline)] pub use sol_types::sol; @@ -34,7 +34,7 @@ pub use alloy_rlp as rlp; /// [`sol!`](sol_types::sol!) `macro_rules!` wrapper to set import attributes. /// /// See [`sol!`](sol_types::sol!) for the actual macro documentation. -#[cfg(all(not(doc), feature = "sol-types"))] // Show the actual macro in docs. +#[cfg(all(feature = "sol-types", not(doc)))] // Show the actual macro in docs. #[macro_export] macro_rules! sol { ($($t:tt)*) => { diff --git a/crates/core/tests/sol.rs b/crates/core/tests/sol.rs index 991502fd49..2da1bfe290 100644 --- a/crates/core/tests/sol.rs +++ b/crates/core/tests/sol.rs @@ -1,5 +1,5 @@ -#![cfg(feature = "sol-types")] #![allow(missing_docs)] +#![cfg(feature = "sol-types")] use alloy_core::sol; diff --git a/crates/dyn-abi/src/arbitrary.rs b/crates/dyn-abi/src/arbitrary.rs index 336fabb8fc..2288fab8d4 100644 --- a/crates/dyn-abi/src/arbitrary.rs +++ b/crates/dyn-abi/src/arbitrary.rs @@ -14,7 +14,7 @@ use alloy_primitives::{Address, Function, B256, I256, U256}; use arbitrary::{size_hint, Unstructured}; use core::ops::RangeInclusive; use proptest::{ - collection::{hash_set as hash_set_strategy, vec as vec_strategy, VecStrategy}, + collection::{vec as vec_strategy, VecStrategy}, prelude::*, strategy::{Flatten, Map, Recursive, TupleUnion, WA}, }; @@ -240,7 +240,7 @@ macro_rules! custom_struct_strategy { .prop_flat_map(move |sz| { ( IDENT_STRATEGY, - hash_set_strategy(IDENT_STRATEGY, sz..=sz) + proptest::collection::hash_set(IDENT_STRATEGY, sz..=sz) .prop_map(|prop_names| prop_names.into_iter().collect()), vec_strategy(elem.clone(), sz..=sz), ) diff --git a/crates/json-abi/src/param.rs b/crates/json-abi/src/param.rs index 2766b0eb2b..8e03341776 100644 --- a/crates/json-abi/src/param.rs +++ b/crates/json-abi/src/param.rs @@ -675,8 +675,11 @@ mod tests { let param_value = serde_json::from_str::(param).unwrap(); assert_eq!(serde_json::from_value::(param_value).unwrap(), expected); - let reader = std::io::Cursor::new(param); - assert_eq!(serde_json::from_reader::<_, Param>(reader).unwrap(), expected); + #[cfg(feature = "std")] + { + let reader = std::io::Cursor::new(param); + assert_eq!(serde_json::from_reader::<_, Param>(reader).unwrap(), expected); + } } #[test] diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 6cdd31f5d0..ee8786acba 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -28,6 +28,7 @@ itoa.workspace = true ruint.workspace = true # macros +cfg-if.workspace = true derive_more = { workspace = true, features = [ "as_ref", "add", @@ -43,7 +44,7 @@ derive_more = { workspace = true, features = [ "into_iterator", "display", ] } -cfg-if.workspace = true +paste.workspace = true # keccak256 keccak-asm = { workspace = true, optional = true } @@ -65,6 +66,14 @@ rand = { workspace = true, optional = true, features = ["getrandom"] } # k256 k256 = { workspace = true, optional = true, features = ["ecdsa"] } +# map +hashbrown = { workspace = true, optional = true, features = [ + "ahash", + "inline-more", +] } +indexmap = { workspace = true, optional = true } +rustc-hash = { workspace = true, optional = true } + # arbitrary arbitrary = { workspace = true, optional = true } derive_arbitrary = { workspace = true, optional = true } @@ -84,30 +93,53 @@ criterion.workspace = true serde_json.workspace = true [features] -default = ["std"] +default = ["std", "map-fxhash"] std = [ "bytes/std", "hex/std", "ruint/std", "alloy-rlp?/std", + "indexmap?/std", + "k256?/std", "keccak-asm?/std", "proptest?/std", "rand?/std", + "rustc-hash?/std", "serde?/std", - "k256?/std", "sha3?/std", ] +nightly = [ + "hex/nightly", + "ruint/nightly", + "hashbrown?/nightly", + "rustc-hash?/nightly", +] -tiny-keccak = [] -native-keccak = [] asm-keccak = ["dep:keccak-asm"] +native-keccak = [] sha3-keccak = ["dep:sha3"] +tiny-keccak = [] + +map = ["dep:hashbrown"] +map-hashbrown = ["map"] +map-indexmap = ["map", "dep:indexmap"] +map-fxhash = ["map", "dep:rustc-hash"] -postgres = ["std", "dep:postgres-types", "ruint/postgres"] getrandom = ["dep:getrandom"] -rand = ["dep:rand", "getrandom", "ruint/rand"] +k256 = ["dep:k256"] +rand = ["dep:rand", "getrandom", "ruint/rand", "rustc-hash?/rand"] rlp = ["dep:alloy-rlp", "ruint/alloy-rlp"] -serde = ["dep:serde", "bytes/serde", "hex/serde", "ruint/serde"] +serde = [ + "dep:serde", + "bytes/serde", + "hex/serde", + "ruint/serde", + "hashbrown?/serde", + "indexmap?/serde", + "rand?/serde", +] + +allocative = ["dep:allocative"] arbitrary = [ "std", "dep:arbitrary", @@ -116,9 +148,9 @@ arbitrary = [ "dep:proptest-derive", "ruint/arbitrary", "ruint/proptest", + "indexmap?/arbitrary", ] -k256 = ["dep:k256"] -allocative = ["dep:allocative"] +postgres = ["std", "dep:postgres-types", "ruint/postgres"] # `const-hex` compatibility feature for `hex`. # Should not be needed most of the time. diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 2d05d20727..b46e1eaee1 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -5,14 +5,15 @@ )] #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(feature = "nightly", feature(hasher_prefixfree_extras))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] #[macro_use] extern crate alloc; +use paste as _; #[cfg(feature = "sha3-keccak")] use sha3 as _; - use tiny_keccak as _; #[cfg(feature = "postgres")] @@ -42,8 +43,9 @@ pub use common::TxKind; mod log; pub use log::{IntoLogData, Log, LogData}; -#[cfg(feature = "serde")] -mod log_serde; + +#[cfg(feature = "map")] +pub mod map; mod sealed; pub use sealed::{Sealable, Sealed}; diff --git a/crates/primitives/src/log.rs b/crates/primitives/src/log/mod.rs similarity index 98% rename from crates/primitives/src/log.rs rename to crates/primitives/src/log/mod.rs index f2a900a4a2..a9eacaeeb8 100644 --- a/crates/primitives/src/log.rs +++ b/crates/primitives/src/log/mod.rs @@ -1,9 +1,12 @@ use crate::{Address, Bytes, B256}; use alloc::vec::Vec; +#[cfg(feature = "serde")] +mod serde; + /// An Ethereum event log object. #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr(feature = "arbitrary", derive(derive_arbitrary::Arbitrary, proptest_derive::Arbitrary))] pub struct LogData { /// The indexed topic list. diff --git a/crates/primitives/src/log_serde.rs b/crates/primitives/src/log/serde.rs similarity index 99% rename from crates/primitives/src/log_serde.rs rename to crates/primitives/src/log/serde.rs index e11413f81a..70e7c6973f 100644 --- a/crates/primitives/src/log_serde.rs +++ b/crates/primitives/src/log/serde.rs @@ -80,6 +80,7 @@ mod tests { log::{Log, LogData}, Bytes, }; + use alloc::vec::Vec; #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)] struct TestStruct { diff --git a/crates/primitives/src/map/fixed.rs b/crates/primitives/src/map/fixed.rs new file mode 100644 index 0000000000..f59a86cf3c --- /dev/null +++ b/crates/primitives/src/map/fixed.rs @@ -0,0 +1,205 @@ +use super::*; +use crate::{Address, FixedBytes, Selector, B256}; +use cfg_if::cfg_if; +use core::{ + fmt, + hash::{BuildHasher, Hasher}, +}; + +/// [`HashMap`] optimized for hashing [fixed-size byte arrays](FixedBytes). +pub type FbHashMap = HashMap, V, FbBuildHasher>; +/// [`HashSet`] optimized for hashing [fixed-size byte arrays](FixedBytes). +pub type FbHashSet = HashSet, FbBuildHasher>; + +cfg_if! { + if #[cfg(feature = "map-indexmap")] { + /// [`IndexMap`] optimized for hashing [fixed-size byte arrays](FixedBytes). + pub type FbIndexMap = + indexmap::IndexMap, V, FbBuildHasher>; + /// [`IndexSet`] optimized for hashing [fixed-size byte arrays](FixedBytes). + pub type FbIndexSet = + indexmap::IndexSet, FbBuildHasher>; + } +} + +macro_rules! fb_alias_maps { + ($($ty:ident < $n:literal >),* $(,)?) => { paste::paste! { + $( + #[doc = concat!("[`HashMap`] optimized for hashing [`", stringify!($ty), "`].")] + pub type [<$ty HashMap>] = HashMap<$ty, V, FbBuildHasher<$n>>; + #[doc = concat!("[`HashSet`] optimized for hashing [`", stringify!($ty), "`].")] + pub type [<$ty HashSet>] = HashSet<$ty, FbBuildHasher<$n>>; + + cfg_if! { + if #[cfg(feature = "map-indexmap")] { + #[doc = concat!("[`IndexMap`] optimized for hashing [`", stringify!($ty), "`].")] + pub type [<$ty IndexMap>] = IndexMap<$ty, V, FbBuildHasher<$n>>; + #[doc = concat!("[`IndexSet`] optimized for hashing [`", stringify!($ty), "`].")] + pub type [<$ty IndexSet>] = IndexSet<$ty, FbBuildHasher<$n>>; + } + } + )* + } }; +} + +fb_alias_maps!(Selector<4>, Address<20>, B256<32>); + +#[allow(unused_macros)] +macro_rules! assert_unchecked { + ($e:expr) => { assert_unchecked!($e,); }; + ($e:expr, $($t:tt)*) => { + if cfg!(debug_assertions) { + assert!($e, $($t)*); + } else if !$e { + unsafe { core::hint::unreachable_unchecked() } + } + }; +} + +macro_rules! assert_eq_unchecked { + ($a:expr, $b:expr) => { assert_eq_unchecked!($a, $b,); }; + ($a:expr, $b:expr, $($t:tt)*) => { + if cfg!(debug_assertions) { + assert_eq!($a, $b, $($t)*); + } else if $a != $b { + unsafe { core::hint::unreachable_unchecked() } + } + }; +} + +/// [`BuildHasher`] optimized for hashing [fixed-size byte arrays](FixedBytes). +/// +/// Works best with `fxhash`, enabled by default with the "map-fxhash" feature. +/// +/// **NOTE:** this hasher accepts only `N`-length byte arrays! It is invalid to hash anything else. +#[derive(Default)] +pub struct FbBuildHasher { + inner: DefaultHashBuilder, + _marker: core::marker::PhantomData<[(); N]>, +} + +impl fmt::Debug for FbBuildHasher { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FbBuildHasher").finish_non_exhaustive() + } +} + +impl BuildHasher for FbBuildHasher { + type Hasher = FbHasher; + + #[inline] + fn build_hasher(&self) -> Self::Hasher { + FbHasher { inner: self.inner.build_hasher(), _marker: core::marker::PhantomData } + } +} + +/// [`Hasher`] optimized for hashing [fixed-size byte arrays](FixedBytes). +/// +/// Works best with `fxhash`, enabled by default with the "map-fxhash" feature. +/// +/// **NOTE:** this hasher accepts only `N`-length byte arrays! It is invalid to hash anything else. +#[derive(Default)] +pub struct FbHasher { + inner: DefaultHasher, + _marker: core::marker::PhantomData<[(); N]>, +} + +impl fmt::Debug for FbHasher { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FbHasher").finish_non_exhaustive() + } +} + +impl Hasher for FbHasher { + #[inline] + fn finish(&self) -> u64 { + self.inner.finish() + } + + #[inline] + fn write(&mut self, bytes: &[u8]) { + assert_eq_unchecked!(bytes.len(), N); + // Threshold decided by some basic micro-benchmarks with fxhash. + if N > 32 { + self.inner.write(bytes); + } else { + write_bytes_unrolled(&mut self.inner, bytes); + } + } + + // We can just skip hashing the length prefix entirely since we know it's always `N`. + + // `write_length_prefix` calls `write_usize` by default. + #[cfg(not(feature = "nightly"))] + #[inline] + fn write_usize(&mut self, i: usize) { + debug_assert_eq!(i, N); + } + + #[cfg(feature = "nightly")] + #[inline] + fn write_length_prefix(&mut self, len: usize) { + debug_assert_eq!(len, N); + } +} + +#[inline(always)] +fn write_bytes_unrolled(hasher: &mut impl Hasher, mut bytes: &[u8]) { + while let Some((chunk, rest)) = bytes.split_first_chunk() { + hasher.write_usize(usize::from_ne_bytes(*chunk)); + bytes = rest; + } + if usize::BITS > 64 { + if let Some((chunk, rest)) = bytes.split_first_chunk() { + hasher.write_u64(u64::from_ne_bytes(*chunk)); + bytes = rest; + } + } + if usize::BITS > 32 { + if let Some((chunk, rest)) = bytes.split_first_chunk() { + hasher.write_u32(u32::from_ne_bytes(*chunk)); + bytes = rest; + } + } + if usize::BITS > 16 { + if let Some((chunk, rest)) = bytes.split_first_chunk() { + hasher.write_u16(u16::from_ne_bytes(*chunk)); + bytes = rest; + } + } + if usize::BITS > 8 { + if let Some((chunk, rest)) = bytes.split_first_chunk() { + hasher.write_u8(u8::from_ne_bytes(*chunk)); + bytes = rest; + } + } + + debug_assert!(bytes.is_empty()); +} + +#[cfg(all(test, any(feature = "std", feature = "map-fxhash")))] +mod tests { + use super::*; + + fn hash_zero() -> u64 { + FbBuildHasher::::default().hash_one(&FixedBytes::::ZERO) + } + + #[test] + fn fb_hasher() { + // Just by running it once we test that it compiles and that debug assertions are correct. + ruint::const_for!(N in [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 47, 48, 49, 63, 64, 127, 128, 256, 512, 1024, 2048, 4096] { + let _ = hash_zero::(); + }); + } + + #[test] + fn map() { + let mut map = AddressHashMap::::default(); + map.insert(Address::ZERO, true); + assert_eq!(map.get(&Address::ZERO), Some(&true)); + assert_eq!(map.get(&Address::with_last_byte(1)), None); + } +} diff --git a/crates/primitives/src/map/mod.rs b/crates/primitives/src/map/mod.rs new file mode 100644 index 0000000000..7cdb5a7869 --- /dev/null +++ b/crates/primitives/src/map/mod.rs @@ -0,0 +1,129 @@ +//! Re-exports of map types and utilities. +//! +//! This module exports the following types: +//! - [`HashMap`] and [`HashSet`] from the standard library or `hashbrown` crate. The +//! "map-hashbrown" feature can be used to force the use of `hashbrown`, and is required in +//! `no_std` environments. +//! - [`IndexMap`] and [`IndexSet`] from the `indexmap` crate, if the "map-indexmap" feature is +//! enabled. +//! - The previously-listed hash map types prefixed with `Fx` if the "map-fxhash" feature is +//! enabled. These are type aliases with [`FxBuildHasher`] as the hasher builder. +//! - The previously-listed hash map types prefixed with `Fb`. These are type aliases with +//! [`FixedBytes`][fb] as the key, and [`FbBuildHasher`] as the hasher builder. This hasher is +//! optimized for hashing fixed-size byte arrays, and wraps around the default hasher builder. It +//! performs best when the hasher is `fxhash`, which is enabled by default with the "map-fxhash" +//! feature. +//! - The previously-listed hash map types prefixed with [`Selector`], [`Address`], and [`B256`]. +//! These use [`FbBuildHasher`] with the respective fixed-size byte array as the key. See the +//! previous point for more information. +//! +//! Unless specified otherwise, the default hasher builder used by these types is +//! [`DefaultHashBuilder`]. This hasher prioritizes speed over security. Users who require HashDoS +//! resistance should enable the "rand" feature so that the hasher is initialized using a random +//! seed. +//! +//! [fb]: crate::FixedBytes +//! [`Selector`]: crate::Selector +//! [`Address`]: crate::Address +//! [`B256`]: crate::B256 + +use cfg_if::cfg_if; + +mod fixed; +pub use fixed::*; + +// The `HashMap` implementation. +// Use `hashbrown` if requested with "map-hashbrown" or required by `no_std`. +cfg_if! { + if #[cfg(any(feature = "map-hashbrown", not(feature = "std")))] { + use hashbrown as imp; + } else { + use hashbrown as _; + use std::collections as imp; + } +} + +#[doc(no_inline)] +pub use imp::{hash_map, hash_map::Entry, hash_set}; + +/// A [`HashMap`](imp::HashMap) using the [default hasher](DefaultHasher). +/// +/// See [`HashMap`](imp::HashMap) for more information. +pub type HashMap = imp::HashMap; +/// A [`HashSet`](imp::HashSet) using the [default hasher](DefaultHasher). +/// +/// See [`HashSet`](imp::HashSet) for more information. +pub type HashSet = imp::HashSet; + +// Faster hasher. +cfg_if! { + if #[cfg(feature = "map-fxhash")] { + #[doc(no_inline)] + pub use rustc_hash::{self, FxHasher}; + + cfg_if! { + if #[cfg(all(feature = "std", feature = "rand"))] { + use rustc_hash::FxRandomState as FxBuildHasherInner; + } else { + use rustc_hash::FxBuildHasher as FxBuildHasherInner; + } + } + + /// The [`FxHasher`] hasher builder. + /// + /// This is [`rustc_hash::FxBuildHasher`], unless both the "std" and "rand" features are + /// enabled, in which case it will be [`rustc_hash::FxRandomState`] for better security at + /// very little cost. If this is not preferred, consider using the `Fx*` aliases directly. + pub type FxBuildHasher = FxBuildHasherInner; + + /// A [`HashMap`] using [`FxHasher`] as its hasher. + pub type FxHashMap = HashMap; + /// A [`HashSet`] using [`FxHasher`] as its hasher. + pub type FxHashSet = HashSet; + } +} + +// Default hasher. +cfg_if! { + if #[cfg(feature = "map-fxhash")] { + type DefaultHashBuilderInner = FxBuildHasher; + } else if #[cfg(any(feature = "map-hashbrown", not(feature = "std")))] { + type DefaultHashBuilderInner = hashbrown::hash_map::DefaultHashBuilder; + } else { + type DefaultHashBuilderInner = std::collections::hash_map::RandomState; + } +} +/// The default [`BuildHasher`](core::hash::BuildHasher) used by [`HashMap`] and [`HashSet`]. +/// +/// See [the module documentation](self) for more information on the default hasher. +pub type DefaultHashBuilder = DefaultHashBuilderInner; +/// The default [`Hasher`](core::hash::Hasher) used by [`HashMap`] and [`HashSet`]. +/// +/// See [the module documentation](self) for more information on the default hasher. +pub type DefaultHasher = ::Hasher; + +// `indexmap` re-exports. +cfg_if! { + if #[cfg(feature = "map-indexmap")] { + #[doc(no_inline)] + pub use indexmap::{self, map::Entry as IndexEntry}; + + /// [`IndexMap`](indexmap::IndexMap) using the [default hasher](DefaultHasher). + /// + /// See [`IndexMap`](indexmap::IndexMap) for more information. + pub type IndexMap = indexmap::IndexMap; + /// [`IndexSet`](indexmap::IndexSet) using the [default hasher](DefaultHasher). + /// + /// See [`IndexSet`](indexmap::IndexSet) for more information. + pub type IndexSet = indexmap::IndexSet; + + cfg_if! { + if #[cfg(feature = "map-fxhash")] { + /// An [`IndexMap`] using [`FxHasher`] as its hasher. + pub type FxIndexMap = IndexMap; + /// An [`IndexSet`] using [`FxHasher`] as its hasher. + pub type FxIndexSet = IndexSet; + } + } + } +} diff --git a/crates/primitives/src/signature/sig.rs b/crates/primitives/src/signature/sig.rs index 3835287e30..658a7fc868 100644 --- a/crates/primitives/src/signature/sig.rs +++ b/crates/primitives/src/signature/sig.rs @@ -583,14 +583,13 @@ impl proptest::arbitrary::Arbitrary for Signature { #[cfg(test)] #[allow(unused_imports)] mod tests { - use crate::Bytes; - use super::*; - use std::str::FromStr; + use crate::Bytes; + use core::str::FromStr; + use hex::FromHex; #[cfg(feature = "rlp")] use alloy_rlp::{Decodable, Encodable}; - use hex::FromHex; #[test] #[cfg(feature = "k256")] @@ -652,16 +651,13 @@ mod tests { #[cfg(feature = "serde")] #[test] fn deserialize_with_parity() { - let raw_signature_with_y_parity = serde_json::json!( - { - "r":"0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0", - "s":"0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05", - "v":"0x1", + let raw_signature_with_y_parity = serde_json::json!({ + "r": "0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0", + "s": "0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05", + "v": "0x1", "yParity": "0x1" - } - ); + }); - println!("{raw_signature_with_y_parity}"); let signature: Signature = serde_json::from_value(raw_signature_with_y_parity).unwrap(); let expected = Signature::from_rs_and_parity( diff --git a/crates/primitives/src/utils/mod.rs b/crates/primitives/src/utils/mod.rs index bf915c37b2..fc10fe380f 100644 --- a/crates/primitives/src/utils/mod.rs +++ b/crates/primitives/src/utils/mod.rs @@ -302,6 +302,7 @@ impl Keccak256 { #[cfg(test)] mod tests { use super::*; + use alloc::string::ToString; // test vector taken from: // https://web3js.readthedocs.io/en/v1.10.0/web3-eth-accounts.html#hashmessage diff --git a/crates/sol-type-parser/src/state_mutability.rs b/crates/sol-type-parser/src/state_mutability.rs index 3dc34f4956..d4086a4f95 100644 --- a/crates/sol-type-parser/src/state_mutability.rs +++ b/crates/sol-type-parser/src/state_mutability.rs @@ -157,11 +157,9 @@ pub mod serde_state_mutability_compat { } } -#[cfg(test)] +#[cfg(all(test, feature = "serde"))] mod tests { use super::*; - - #[cfg(not(feature = "std"))] use alloc::string::ToString; #[derive(Debug, Serialize, Deserialize)] diff --git a/scripts/check_features.sh b/scripts/check_features.sh new file mode 100755 index 0000000000..257dcd5e07 --- /dev/null +++ b/scripts/check_features.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -eo pipefail + +cargo hack check --feature-powerset --depth 1 \ + --group-features std,map --group-features std,map-fxhash --group-features std,map-indexmap \ + --ignore-unknown-features \ + "${@}" diff --git a/tests/core-sol/Cargo.toml b/tests/core-sol/Cargo.toml index 7f9b7d03ee..b7c83830d3 100644 --- a/tests/core-sol/Cargo.toml +++ b/tests/core-sol/Cargo.toml @@ -11,4 +11,4 @@ repository.workspace = true exclude.workspace = true [dependencies] -alloy-core = { workspace = true, features = ["sol-types", "json"] } +alloy-core = { workspace = true, features = ["sol-types", "json", "map"] } diff --git a/tests/core-sol/src/lib.rs b/tests/core-sol/src/lib.rs index 511e6381e5..a5029c1d1d 100644 --- a/tests/core-sol/src/lib.rs +++ b/tests/core-sol/src/lib.rs @@ -16,6 +16,7 @@ sol! { A, B } + #[derive(Default, PartialEq, Eq, Hash)] struct MyStruct { uint32 a; uint64 b; @@ -62,3 +63,10 @@ sol! { type MyOtherType is uint32; } } + +#[test] +fn do_stuff() { + let mut set = alloy_core::primitives::map::HashSet::::default(); + set.insert(Default::default()); + assert_eq!(set.len(), 1); +}