diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b275038d5..debfd0918 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -46,7 +46,7 @@ jobs: uses: ./.github/actions/cleanup-runner # Added: LLVM/Clang for RocksDB/bindgen - name: Install LLVM/Clang - uses: KyleMayes/install-llvm-action@v2 + uses: ./.github/actions/install-llvm with: version: "17" - name: Rustup @@ -124,3 +124,26 @@ jobs: steps: - uses: actions/checkout@v4 - run: make check-fuzz + + check-features: + name: check all feature combinations + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Cleanup large tools for build space + uses: ./.github/actions/cleanup-runner + # Added: LLVM/Clang for RocksDB/bindgen (needed for rocksdb feature) + - name: Install LLVM/Clang + uses: ./.github/actions/install-llvm + with: + version: "17" + - name: Rustup + run: rustup update --no-self-update + - uses: taiki-e/install-action@v2 + with: + tool: cargo-hack + - uses: Swatinem/rust-cache@v2 + with: + save-if: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', inputs.target_branch) }} + - name: Check all feature combinations + run: ./scripts/check-features.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 15adf420c..0657bb953 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - Added validation to `PartialMerkleTree::with_leaves()` to reject internal nodes ([#684](https://github.com/0xMiden/crypto/pull/684)). - [BREAKING] Moved `LargeSmt` root ownership from storage to in-memory layer ([#694](https://github.com/0xMiden/crypto/pull/694)). - Remove use of `transmute()` in blake3 implementation ([#704](https://github.com/0xMiden/crypto/pull/704)). +- [BREAKING] Removed the direct `hashbrown` dependency and now use `std::collections::{HashMap, HashSet}` whenever `std` is enabled (falling back to `BTreeMap`/`BTreeSet` in `no_std`) ([#696](https://github.com/0xMiden/crypto/issues/696)). +- [BREAKING] Imported miden-serde-utils crate for serialization ([#715](https://github.com/0xMiden/crypto/pull/715)). ## 0.19.2 (2025-12-04) diff --git a/Cargo.lock b/Cargo.lock index 99caf5e7a..47015f226 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,12 +21,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "anes" version = "0.1.6" @@ -610,12 +604,6 @@ dependencies = [ "spin", ] -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "futures-core" version = "0.3.31" @@ -734,14 +722,6 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", - "rayon", - "serde", - "serde_core", -] [[package]] name = "heck" @@ -957,12 +937,12 @@ dependencies = [ "ed25519-dalek", "flume", "glob", - "hashbrown", "hex", "hkdf", "itertools 0.14.0", "k256", "miden-crypto-derive", + "miden-serde-utils", "num", "num-complex", "proptest", @@ -995,6 +975,14 @@ dependencies = [ "syn", ] +[[package]] +name = "miden-serde-utils" +version = "0.20.0" +dependencies = [ + "winter-math", + "winter-utils", +] + [[package]] name = "minimal-lexical" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 48246f1f2..268d58296 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] exclude = ["miden-crypto-fuzz"] -members = ["miden-crypto", "miden-crypto-derive"] +members = ["miden-crypto", "miden-crypto-derive", "miden-serde-utils"] resolver = "3" [workspace.package] @@ -15,7 +15,14 @@ version = "0.20.0" [workspace.dependencies] miden-crypto-derive = { path = "miden-crypto-derive", version = "0.20" } +miden-serde-utils = { features = ["winter-compat"], path = "miden-serde-utils", version = "0.20" } [workspace.lints.rust] # Suppress warnings about `cfg(fuzzing)`, which is automatically set when using `cargo-fuzz`. unexpected_cfgs = { check-cfg = ['cfg(fuzzing)'], level = "warn" } + +[profile.test-release] +debug = 2 +debug-assertions = true +inherits = "release" +overflow-checks = true diff --git a/Makefile b/Makefile index e4c2940b7..08fac80c7 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ help: # -- variables -------------------------------------------------------------------------------------- -ALL_FEATURES_EXCEPT_ROCKSDB="concurrent executable hashmaps internal serde std" +ALL_FEATURES_EXCEPT_ROCKSDB="concurrent executable internal serde std" DEBUG_OVERFLOW_INFO=RUSTFLAGS="-C debug-assertions -C overflow-checks -C debuginfo=2" WARNINGS=RUSTDOCFLAGS="-D warnings" @@ -68,30 +68,26 @@ doc: ## Generate and check documentation .PHONY: test-default test-default: ## Run tests with default features - $(DEBUG_OVERFLOW_INFO) cargo nextest run --profile default --release --features ${ALL_FEATURES_EXCEPT_ROCKSDB} - -.PHONY: test-hashmaps -test-hashmaps: ## Run tests with `hashmaps` feature enabled - $(DEBUG_OVERFLOW_INFO) cargo nextest run --profile default --release --features hashmaps + cargo nextest run --profile default --cargo-profile test-release --features ${ALL_FEATURES_EXCEPT_ROCKSDB} .PHONY: test-no-std test-no-std: ## Run tests with `no-default-features` (std) - $(DEBUG_OVERFLOW_INFO) cargo nextest run --profile default --release --no-default-features + cargo nextest run --profile default --cargo-profile test-release --no-default-features .PHONY: test-smt-concurrent test-smt-concurrent: ## Run only concurrent SMT tests - $(DEBUG_OVERFLOW_INFO) cargo nextest run --profile smt-concurrent --release + cargo nextest run --profile smt-concurrent --cargo-profile test-release .PHONY: test-docs test-docs: - $(DEBUG_OVERFLOW_INFO) cargo test --doc --all-features + cargo test --doc --all-features --profile test-release .PHONY: test-large-smt test-large-smt: ## Run only large SMT tests - $(DEBUG_OVERFLOW_INFO) cargo nextest run --success-output immediate --profile large-smt --release --features hashmaps,rocksdb + cargo nextest run --success-output immediate --profile large-smt --cargo-profile test-release --features rocksdb .PHONY: test -test: test-default test-hashmaps test-no-std test-docs test-large-smt ## Run all tests except concurrent SMT tests +test: test-default test-no-std test-docs test-large-smt ## Run all tests except concurrent SMT tests # --- checking ------------------------------------------------------------------------------------ @@ -137,15 +133,15 @@ bench-smt-concurrent: ## Run SMT benchmarks with concurrent feature .PHONY: bench-large-smt-memory bench-large-smt-memory: ## Run large SMT benchmarks with memory storage - cargo run --release --features concurrent,hashmaps,executable -- --size 1000000 + cargo run --release --features concurrent,executable -- --size 1000000 .PHONY: bench-large-smt-rocksdb bench-large-smt-rocksdb: ## Run large SMT benchmarks with rocksdb storage - cargo run --release --features concurrent,hashmaps,rocksdb,executable -- --storage rocksdb --size 1000000 + cargo run --release --features concurrent,rocksdb,executable -- --storage rocksdb --size 1000000 .PHONY: bench-large-smt-rocksdb-open bench-large-smt-rocksdb-open: ## Run large SMT benchmarks with rocksdb storage and open existing database - cargo run --release --features concurrent,hashmaps,rocksdb,executable -- --storage rocksdb --open + cargo run --release --features concurrent,rocksdb,executable -- --storage rocksdb --open # --- fuzzing -------------------------------------------------------------------------------- diff --git a/README.md b/README.md index cfeac61de..6590ee379 100644 --- a/README.md +++ b/README.md @@ -136,9 +136,8 @@ make This crate can be compiled with the following features: - `concurrent`- enabled by default; enables multi-threaded implementation of `Smt::with_entries()` which significantly improves performance on multi-core CPUs. -- `std` - enabled by default and relies on the Rust standard library. +- `std` - enabled by default and relies on the Rust standard library. When enabled, the crate uses `HashMap`/`HashSet`; when disabled (`no_std`), it uses `BTreeMap`/`BTreeSet` with ordered iteration. - `no_std` does not rely on the Rust standard library and enables compilation to WebAssembly. -- `hashmaps` - uses hashbrown hashmaps in SMT and Merkle Store implementation which significantly improves performance of updates. Keys ordering in iterators is not guaranteed when this feature is enabled. - `rocksdb` - enables the RocksDB-backed storage for `LargeSmt` and related utilities. Implies `concurrent`. All of these features imply the use of [alloc](https://doc.rust-lang.org/alloc/) to support heap-allocated collections. diff --git a/THIRD_PARTY_NOTICES.txt b/THIRD_PARTY_NOTICES.txt new file mode 100644 index 000000000..b6e94ff43 --- /dev/null +++ b/THIRD_PARTY_NOTICES.txt @@ -0,0 +1,38 @@ +THIRD PARTY NOTICES +=================== + +This project includes code from the following third-party projects: + +-------------------------------------------------------------------------------- + +1. Winterfell (facebook/winterfell) + https://github.com/facebook/winterfell + + The following files contain code derived from Winterfell's winter-utils crate: + - miden-serde-utils/src/lib.rs + - miden-serde-utils/src/byte_reader.rs + - miden-serde-utils/src/byte_writer.rs + + Original Copyright Notice and License: + + Copyright (c) Facebook, Inc. and its affiliates. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +-------------------------------------------------------------------------------- diff --git a/miden-crypto-fuzz/Cargo.lock b/miden-crypto-fuzz/Cargo.lock index ae429a5f1..397ecd283 100644 --- a/miden-crypto-fuzz/Cargo.lock +++ b/miden-crypto-fuzz/Cargo.lock @@ -552,7 +552,6 @@ dependencies = [ "ed25519-dalek", "flume", "glob", - "hashbrown", "hkdf", "k256", "miden-crypto-derive", diff --git a/miden-crypto/Cargo.toml b/miden-crypto/Cargo.toml index bf6046015..ac829bf88 100644 --- a/miden-crypto/Cargo.toml +++ b/miden-crypto/Cargo.toml @@ -50,8 +50,9 @@ harness = false name = "store" [[bench]] -harness = false -name = "dsa" +harness = false +name = "dsa" +required-features = ["std"] [[bench]] harness = false @@ -62,17 +63,18 @@ harness = false name = "rand" [features] -concurrent = ["dep:rayon", "hashbrown?/rayon"] +concurrent = ["dep:rayon", "std"] default = ["concurrent", "std"] -executable = ["dep:clap", "dep:rand-utils", "std"] +executable = ["concurrent", "dep:clap", "dep:rand-utils"] fuzzing = [] -hashmaps = ["dep:hashbrown"] -internal = [] +internal = ["concurrent"] rocksdb = ["concurrent", "dep:rocksdb"] serde = ["dep:serde", "serde?/alloc", "winter-math/serde"] std = [ "blake3/std", "dep:cc", + "miden-serde-utils/std", + "miden-serde-utils/winter-compat", "rand/std", "rand/thread_rng", "winter-crypto/std", @@ -87,10 +89,10 @@ clap = { features = ["derive"], optional = true, versio curve25519-dalek = { default-features = false, version = "4" } ed25519-dalek = { features = ["zeroize"], version = "2" } flume = { version = "0.11" } -hashbrown = { features = ["serde"], optional = true, version = "0.16" } hkdf = { default-features = false, version = "0.12" } k256 = { features = ["ecdh", "ecdsa"], version = "0.13" } miden-crypto-derive.workspace = true +miden-serde-utils.workspace = true num = { default-features = false, features = ["alloc", "libm"], version = "0.4" } num-complex = { default-features = false, version = "0.4" } rand = { default-features = false, version = "0.9" } diff --git a/miden-crypto/benches/README.md b/miden-crypto/benches/README.md index 423e9610d..dce469b05 100644 --- a/miden-crypto/benches/README.md +++ b/miden-crypto/benches/README.md @@ -80,7 +80,7 @@ For each algorithm, we benchmark three core operations: ### Sparse Merkle Tree -We build cryptographic data structures incorporating these hash functions. What follows are benchmarks of operations on sparse Merkle trees (SMTs) which use the above `RPO_256` hash function. We perform a batched modification of 1,000 values in a tree with 1,000,000 leaves (with the `hashmaps` feature to use the `hashbrown` crate). +We build cryptographic data structures incorporating these hash functions. What follows are benchmarks of operations on sparse Merkle trees (SMTs) which use the above `RPO_256` hash function. We perform a batched modification of 1,000 values in a tree with 1,000,000 leaves using the default `HashMap` implementation (enabled with the `std` feature). ### Scenario 1: SMT Construction (1M pairs) @@ -151,7 +151,7 @@ cargo run --features=executable The `concurrent` feature enables the concurrent benchmark, and is enabled by default. To run a sequential benchmark, disable the crate's default features: ``` -cargo run --no-default-features --features=executable,hashmaps +cargo run --no-default-features --features=executable ``` The benchmark parameters may also be customized with the `-s`/`--size`, `-i`/`--insertions`, and `-u`/`--updates` options. diff --git a/miden-crypto/src/dsa/ecdsa_k256_keccak/tests.rs b/miden-crypto/src/dsa/ecdsa_k256_keccak/tests.rs index 117da6e84..2d6da8114 100644 --- a/miden-crypto/src/dsa/ecdsa_k256_keccak/tests.rs +++ b/miden-crypto/src/dsa/ecdsa_k256_keccak/tests.rs @@ -115,7 +115,7 @@ fn test_secret_key_debug_redaction() { #[cfg(feature = "std")] #[test] fn test_signature_serde() { - use winter_utils::SliceReader; + use crate::utils::SliceReader; let sig0 = SecretKey::new().sign(Word::from([5, 0, 0, 0u32])); let sig_bytes = sig0.to_bytes(); let mut slice_reader = SliceReader::new(&sig_bytes); diff --git a/miden-crypto/src/dsa/falcon512_rpo/hash_to_point.rs b/miden-crypto/src/dsa/falcon512_rpo/hash_to_point.rs index 045af7d21..7c5e49e1a 100644 --- a/miden-crypto/src/dsa/falcon512_rpo/hash_to_point.rs +++ b/miden-crypto/src/dsa/falcon512_rpo/hash_to_point.rs @@ -46,7 +46,7 @@ pub fn hash_to_point_rpo256(message: Word, nonce: &Nonce) -> Polynomial Polynomial { use sha3::{ Shake256, @@ -93,7 +93,7 @@ fn felt_to_falcon_felt(value: Felt) -> FalconFelt { /// Note that since `FalconFelt::new` accepts `i16`, we first reduce the `u32` value modulo /// the Falcon prime and then cast the resulting value to an `i16`. /// Note that this final cast is safe as the Falcon prime is less than `i16::MAX`. -#[cfg(test)] +#[cfg(all(test, feature = "std"))] fn u32_to_falcon_felt(value: u32) -> FalconFelt { FalconFelt::new((value % MODULUS as u32) as i16) } diff --git a/miden-crypto/src/dsa/falcon512_rpo/keys/mod.rs b/miden-crypto/src/dsa/falcon512_rpo/keys/mod.rs index 16798f699..889b0ab24 100644 --- a/miden-crypto/src/dsa/falcon512_rpo/keys/mod.rs +++ b/miden-crypto/src/dsa/falcon512_rpo/keys/mod.rs @@ -18,9 +18,12 @@ mod tests { use rand::SeedableRng; use rand_chacha::ChaCha20Rng; use winter_math::FieldElement; - use winter_utils::{Deserializable, Serializable}; - use crate::{ONE, Word, dsa::falcon512_rpo::SecretKey}; + use crate::{ + ONE, Word, + dsa::falcon512_rpo::SecretKey, + utils::{Deserializable, Serializable}, + }; #[test] fn test_falcon_verification() { diff --git a/miden-crypto/src/dsa/falcon512_rpo/keys/secret_key.rs b/miden-crypto/src/dsa/falcon512_rpo/keys/secret_key.rs index 600b08155..5ea787628 100644 --- a/miden-crypto/src/dsa/falcon512_rpo/keys/secret_key.rs +++ b/miden-crypto/src/dsa/falcon512_rpo/keys/secret_key.rs @@ -2,8 +2,6 @@ use alloc::{string::ToString, vec::Vec}; use miden_crypto_derive::{SilentDebug, SilentDisplay}; use num::Complex; -#[cfg(not(feature = "std"))] -use num::Float; use num_complex::Complex64; use rand::Rng; diff --git a/miden-crypto/src/dsa/falcon512_rpo/math/ffsampling.rs b/miden-crypto/src/dsa/falcon512_rpo/math/ffsampling.rs index 8e3c265c3..9964c5135 100644 --- a/miden-crypto/src/dsa/falcon512_rpo/math/ffsampling.rs +++ b/miden-crypto/src/dsa/falcon512_rpo/math/ffsampling.rs @@ -1,7 +1,5 @@ use alloc::boxed::Box; -#[cfg(not(feature = "std"))] -use num::Float; use num::Zero; use num_complex::{Complex, Complex64}; use rand::Rng; diff --git a/miden-crypto/src/dsa/falcon512_rpo/math/fft.rs b/miden-crypto/src/dsa/falcon512_rpo/math/fft.rs index 462f692a2..d0936fae7 100644 --- a/miden-crypto/src/dsa/falcon512_rpo/math/fft.rs +++ b/miden-crypto/src/dsa/falcon512_rpo/math/fft.rs @@ -4,8 +4,6 @@ use core::{ ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}, }; -#[cfg(not(feature = "std"))] -use num::Float; use num::{One, Zero}; use num_complex::Complex64; diff --git a/miden-crypto/src/dsa/falcon512_rpo/math/mod.rs b/miden-crypto/src/dsa/falcon512_rpo/math/mod.rs index 48bc20f26..f7d862918 100644 --- a/miden-crypto/src/dsa/falcon512_rpo/math/mod.rs +++ b/miden-crypto/src/dsa/falcon512_rpo/math/mod.rs @@ -8,8 +8,6 @@ use alloc::vec::Vec; use core::ops::MulAssign; -#[cfg(not(feature = "std"))] -use num::Float; use num::{BigInt, FromPrimitive, One, Zero}; use num_complex::Complex64; use rand::Rng; diff --git a/miden-crypto/src/dsa/falcon512_rpo/math/samplerz.rs b/miden-crypto/src/dsa/falcon512_rpo/math/samplerz.rs index e046701e7..0ad34aa1c 100644 --- a/miden-crypto/src/dsa/falcon512_rpo/math/samplerz.rs +++ b/miden-crypto/src/dsa/falcon512_rpo/math/samplerz.rs @@ -1,5 +1,3 @@ -#[cfg(not(feature = "std"))] -use num::Float; use rand::Rng; /// Samples an integer from {0, ..., 18} according to the distribution χ, which is close to diff --git a/miden-crypto/src/ecdh/k256.rs b/miden-crypto/src/ecdh/k256.rs index e54cc390c..c08a39c43 100644 --- a/miden-crypto/src/ecdh/k256.rs +++ b/miden-crypto/src/ecdh/k256.rs @@ -232,10 +232,12 @@ impl KeyAgreementScheme for K256 { mod test { use rand::rng; - use winter_utils::{Deserializable, Serializable}; use super::{EphemeralPublicKey, EphemeralSecretKey}; - use crate::dsa::ecdsa_k256_keccak::SecretKey; + use crate::{ + dsa::ecdsa_k256_keccak::SecretKey, + utils::{Deserializable, Serializable}, + }; #[test] fn key_agreement() { diff --git a/miden-crypto/src/ecdh/mod.rs b/miden-crypto/src/ecdh/mod.rs index 63a0ffccd..b6b416536 100644 --- a/miden-crypto/src/ecdh/mod.rs +++ b/miden-crypto/src/ecdh/mod.rs @@ -4,9 +4,11 @@ use alloc::vec::Vec; use rand::{CryptoRng, RngCore}; use thiserror::Error; -use winter_utils::{Deserializable, Serializable}; -use crate::zeroize::{Zeroize, ZeroizeOnDrop}; +use crate::{ + utils::{Deserializable, Serializable}, + zeroize::{Zeroize, ZeroizeOnDrop}, +}; pub mod k256; pub mod x25519; diff --git a/miden-crypto/src/hash/blake/mod.rs b/miden-crypto/src/hash/blake/mod.rs index 3746c42df..9f6f2762b 100644 --- a/miden-crypto/src/hash/blake/mod.rs +++ b/miden-crypto/src/hash/blake/mod.rs @@ -94,6 +94,21 @@ impl Deserializable for Blake3Digest { } } +// winter_utils compatibility - required for winter_crypto::Digest trait +impl winter_utils::Serializable for Blake3Digest { + fn write_into(&self, target: &mut W) { + target.write_bytes(&self.0); + } +} + +impl winter_utils::Deserializable for Blake3Digest { + fn read_from( + source: &mut R, + ) -> Result { + source.read_array().map(Self) + } +} + impl Digest for Blake3Digest { fn as_bytes(&self) -> [u8; 32] { // compile-time assertion diff --git a/miden-crypto/src/hash/keccak/mod.rs b/miden-crypto/src/hash/keccak/mod.rs index c67be4a49..890760803 100644 --- a/miden-crypto/src/hash/keccak/mod.rs +++ b/miden-crypto/src/hash/keccak/mod.rs @@ -89,6 +89,21 @@ impl Deserializable for Keccak256Digest { } } +// winter_utils compatibility - required for winter_crypto::Digest trait +impl winter_utils::Serializable for Keccak256Digest { + fn write_into(&self, target: &mut W) { + target.write_bytes(&self.0); + } +} + +impl winter_utils::Deserializable for Keccak256Digest { + fn read_from( + source: &mut R, + ) -> Result { + source.read_array().map(Self) + } +} + impl Digest for Keccak256Digest { fn as_bytes(&self) -> [u8; 32] { self.0 diff --git a/miden-crypto/src/hash/sha2/mod.rs b/miden-crypto/src/hash/sha2/mod.rs index a4c7609cd..411ca366c 100644 --- a/miden-crypto/src/hash/sha2/mod.rs +++ b/miden-crypto/src/hash/sha2/mod.rs @@ -104,6 +104,21 @@ impl Deserializable for Sha256Digest { } } +// winter_utils compatibility - required for winter_crypto::Digest trait +impl winter_utils::Serializable for Sha256Digest { + fn write_into(&self, target: &mut W) { + target.write_bytes(&self.0); + } +} + +impl winter_utils::Deserializable for Sha256Digest { + fn read_from( + source: &mut R, + ) -> Result { + source.read_array().map(Self) + } +} + impl Digest for Sha256Digest { fn as_bytes(&self) -> [u8; 32] { self.0 diff --git a/miden-crypto/src/ies/tests.rs b/miden-crypto/src/ies/tests.rs index 0f6b7faf0..bd623a263 100644 --- a/miden-crypto/src/ies/tests.rs +++ b/miden-crypto/src/ies/tests.rs @@ -919,9 +919,8 @@ mod integration_tests { // ================================================================================================ mod keys_serialization_tests { - use winter_utils::ByteReader; - use super::*; + use crate::utils::ByteReader; fn assert_roundtrip(sealing_key: SealingKey) { let expected_scheme = sealing_key.scheme(); diff --git a/miden-crypto/src/lib.rs b/miden-crypto/src/lib.rs index 80339721e..9696b54b9 100644 --- a/miden-crypto/src/lib.rs +++ b/miden-crypto/src/lib.rs @@ -30,36 +30,41 @@ pub use word::{Word, WordError}; /// An alias for a key-value map. /// -/// By default, this is an alias for the [`alloc::collections::BTreeMap`], however, when the -/// `hashmaps` feature is enabled, this is an alias for the `hashbrown`'s `HashMap`. -#[cfg(feature = "hashmaps")] -pub type Map = hashbrown::HashMap; +/// Note: When the `std` feature is enabled (which is the default), this uses +/// `std::collections::HashMap`, which does not guarantee deterministic iteration order. This may +/// change the behavior of functions returning `impl IntoIterator<..>` compared to `no_std` +/// environments (which use `BTreeMap` for deterministic ordering). Be cautious if your code relies +/// on stable iteration order. +/// +/// By default (when the `std` feature is enabled), this is an alias for +/// `std::collections::HashMap`. +#[cfg(feature = "std")] +pub type Map = std::collections::HashMap; -#[cfg(feature = "hashmaps")] -pub use hashbrown::hash_map::Entry as MapEntry; +#[cfg(feature = "std")] +pub use std::collections::hash_map::Entry as MapEntry; /// An alias for a key-value map. /// -/// By default, this is an alias for the [`alloc::collections::BTreeMap`], however, when the -/// `hashmaps` feature is enabled, this is an alias for the `hashbrown`'s `HashMap`. -#[cfg(not(feature = "hashmaps"))] +/// When the `std` feature is not enabled, this is an alias for [`alloc::collections::BTreeMap`]. +#[cfg(not(feature = "std"))] pub type Map = alloc::collections::BTreeMap; -#[cfg(not(feature = "hashmaps"))] +#[cfg(not(feature = "std"))] pub use alloc::collections::btree_map::Entry as MapEntry; /// An alias for a simple set. /// /// By default, this is an alias for the [`alloc::collections::BTreeSet`]. However, when the -/// `hashmaps` feature is enabled, this becomes an alias for hashbrown's HashSet. -#[cfg(feature = "hashmaps")] -pub type Set = hashbrown::HashSet; +/// `std` feature is enabled, this becomes an alias for `std::collections::HashSet`. +#[cfg(feature = "std")] +pub type Set = std::collections::HashSet; /// An alias for a simple set. /// /// By default, this is an alias for the [`alloc::collections::BTreeSet`]. However, when the -/// `hashmaps` feature is enabled, this becomes an alias for hashbrown's HashSet. -#[cfg(not(feature = "hashmaps"))] +/// `std` feature is enabled, this becomes an alias for `std::collections::HashSet`. +#[cfg(not(feature = "std"))] pub type Set = alloc::collections::BTreeSet; // CONSTANTS diff --git a/miden-crypto/src/merkle/error.rs b/miden-crypto/src/merkle/error.rs index 87af2d511..f3b23df60 100644 --- a/miden-crypto/src/merkle/error.rs +++ b/miden-crypto/src/merkle/error.rs @@ -1,4 +1,6 @@ -use alloc::string::{String, ToString}; +use alloc::string::String; +#[cfg(feature = "concurrent")] +use alloc::string::ToString; use thiserror::Error; diff --git a/miden-crypto/src/merkle/mmr/forest.rs b/miden-crypto/src/merkle/mmr/forest.rs index c8d480873..c28c940be 100644 --- a/miden-crypto/src/merkle/mmr/forest.rs +++ b/miden-crypto/src/merkle/mmr/forest.rs @@ -3,10 +3,11 @@ use core::{ ops::{BitAnd, BitOr, BitXor, BitXorAssign}, }; -use winter_utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; - use super::InOrderIndex; -use crate::Felt; +use crate::{ + Felt, + utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, +}; /// A compact representation of trees in a forest. Used in the Merkle forest (MMR). /// diff --git a/miden-crypto/src/merkle/mmr/full.rs b/miden-crypto/src/merkle/mmr/full.rs index af4722cbe..6243a1a20 100644 --- a/miden-crypto/src/merkle/mmr/full.rs +++ b/miden-crypto/src/merkle/mmr/full.rs @@ -12,14 +12,16 @@ //! reestablished. use alloc::vec::Vec; -use winter_utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; - use super::{ super::{InnerNodeInfo, MerklePath}, MmrDelta, MmrError, MmrPath, MmrPeaks, MmrProof, forest::{Forest, TreeSizeIterator}, }; -use crate::{Word, merkle::Rpo256}; +use crate::{ + Word, + merkle::Rpo256, + utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, +}; // MMR // =============================================================================================== @@ -451,9 +453,11 @@ impl Iterator for MmrNodes<'_> { mod tests { use alloc::vec::Vec; - use winter_utils::{Deserializable, Serializable}; - - use crate::{Felt, Word, ZERO, merkle::mmr::Mmr}; + use crate::{ + Felt, Word, ZERO, + merkle::mmr::Mmr, + utils::{Deserializable, Serializable}, + }; #[test] fn test_serialization() { diff --git a/miden-crypto/src/merkle/mmr/inorder.rs b/miden-crypto/src/merkle/mmr/inorder.rs index f6579ab72..2efc4d4cc 100644 --- a/miden-crypto/src/merkle/mmr/inorder.rs +++ b/miden-crypto/src/merkle/mmr/inorder.rs @@ -6,7 +6,7 @@ //! leaves count. use core::num::NonZeroUsize; -use winter_utils::{Deserializable, Serializable}; +use crate::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; // IN-ORDER INDEX // ================================================================================================ @@ -115,15 +115,15 @@ impl InOrderIndex { } impl Serializable for InOrderIndex { - fn write_into(&self, target: &mut W) { + fn write_into(&self, target: &mut W) { target.write_usize(self.idx); } } impl Deserializable for InOrderIndex { - fn read_from( + fn read_from( source: &mut R, - ) -> Result { + ) -> Result { let idx = source.read_usize()?; Ok(InOrderIndex { idx }) } @@ -144,9 +144,9 @@ impl From for usize { #[cfg(test)] mod test { use proptest::prelude::*; - use winter_utils::{Deserializable, Serializable}; use super::InOrderIndex; + use crate::utils::{Deserializable, Serializable}; proptest! { #[test] diff --git a/miden-crypto/src/merkle/mmr/partial.rs b/miden-crypto/src/merkle/mmr/partial.rs index 6eae29be7..d693ca726 100644 --- a/miden-crypto/src/merkle/mmr/partial.rs +++ b/miden-crypto/src/merkle/mmr/partial.rs @@ -3,8 +3,6 @@ use alloc::{ vec::Vec, }; -use winter_utils::{Deserializable, Serializable}; - use super::{MmrDelta, MmrPath}; use crate::{ Word, @@ -12,6 +10,7 @@ use crate::{ InnerNodeInfo, MerklePath, Rpo256, mmr::{InOrderIndex, MmrError, MmrPeaks, forest::Forest}, }, + utils::{ByteReader, ByteWriter, Deserializable, Serializable}, }; // TYPE ALIASES @@ -591,7 +590,7 @@ impl> Iterator for InnerNodeIterator<'_, I> { } impl Serializable for PartialMmr { - fn write_into(&self, target: &mut W) { + fn write_into(&self, target: &mut W) { self.forest.num_leaves().write_into(target); self.peaks.write_into(target); self.nodes.write_into(target); @@ -600,9 +599,9 @@ impl Serializable for PartialMmr { } impl Deserializable for PartialMmr { - fn read_from( + fn read_from( source: &mut R, - ) -> Result { + ) -> Result { let forest = Forest::new(usize::read_from(source)?); let peaks = Vec::::read_from(source)?; let nodes = NodeMap::read_from(source)?; @@ -619,8 +618,6 @@ impl Deserializable for PartialMmr { mod tests { use alloc::{collections::BTreeSet, vec::Vec}; - use winter_utils::{Deserializable, Serializable}; - use super::{MmrPeaks, PartialMmr}; use crate::{ Word, @@ -629,6 +626,7 @@ mod tests { mmr::{Mmr, forest::Forest}, store::MerkleStore, }, + utils::{Deserializable, Serializable}, }; const LEAVES: [Word; 7] = [ diff --git a/miden-crypto/src/merkle/path.rs b/miden-crypto/src/merkle/path.rs index 0c4f3e47d..59fccfc61 100644 --- a/miden-crypto/src/merkle/path.rs +++ b/miden-crypto/src/merkle/path.rs @@ -5,7 +5,7 @@ use core::{ }; use super::{InnerNodeInfo, MerkleError, NodeIndex, Rpo256, Word}; -use crate::utils::{ByteReader, Deserializable, DeserializationError, Serializable}; +use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; // MERKLE PATH // ================================================================================================ @@ -239,7 +239,7 @@ pub struct RootPath { // ================================================================================================ impl Serializable for MerklePath { - fn write_into(&self, target: &mut W) { + fn write_into(&self, target: &mut W) { assert!(self.nodes.len() <= u8::MAX.into(), "Length enforced in the constructor"); target.write_u8(self.nodes.len() as u8); target.write_many(&self.nodes); @@ -255,7 +255,7 @@ impl Deserializable for MerklePath { } impl Serializable for MerkleProof { - fn write_into(&self, target: &mut W) { + fn write_into(&self, target: &mut W) { self.value.write_into(target); self.path.write_into(target); } @@ -270,7 +270,7 @@ impl Deserializable for MerkleProof { } impl Serializable for RootPath { - fn write_into(&self, target: &mut W) { + fn write_into(&self, target: &mut W) { self.root.write_into(target); self.path.write_into(target); } diff --git a/miden-crypto/src/merkle/smt/forest/store.rs b/miden-crypto/src/merkle/smt/forest/store.rs index aedb0513b..93f45fecb 100644 --- a/miden-crypto/src/merkle/smt/forest/store.rs +++ b/miden-crypto/src/merkle/smt/forest/store.rs @@ -169,10 +169,10 @@ impl SmtStore { #[allow(unused_mut)] let mut sorted_leaf_indices = leaves_by_index.keys().cloned().collect::>(); - #[cfg(feature = "hashmaps")] + #[cfg(feature = "std")] // Sort leaves by NodeIndex to easily detect when leaves share a parent (only neighboring - // leaves can share a parent). Hashbrown::HashMap doesn't maintain key ordering, so - // we need to sort the indices. + // leaves can share a parent). `std::collections::HashMap` doesn't maintain key ordering, + // so we need to sort the indices. sorted_leaf_indices.sort(); // Ensure new leaf values override current opening values. diff --git a/miden-crypto/src/merkle/smt/full/mod.rs b/miden-crypto/src/merkle/smt/full/mod.rs index 2f2662842..9fe9ffecb 100644 --- a/miden-crypto/src/merkle/smt/full/mod.rs +++ b/miden-crypto/src/merkle/smt/full/mod.rs @@ -13,7 +13,8 @@ pub use leaf::SmtLeaf; mod proof; pub use proof::SmtProof; -use winter_utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; + +use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; // Concurrent implementation #[cfg(feature = "concurrent")] diff --git a/miden-crypto/src/merkle/smt/large/batch_ops.rs b/miden-crypto/src/merkle/smt/large/batch_ops.rs index db48c3fe0..7a917accd 100644 --- a/miden-crypto/src/merkle/smt/large/batch_ops.rs +++ b/miden-crypto/src/merkle/smt/large/batch_ops.rs @@ -305,7 +305,7 @@ impl LargeSmt { /// - Storage operations fail /// /// # Example - /// ```no_run + /// ```ignore /// use miden_crypto::{ /// EMPTY_WORD, Felt, Word, /// merkle::smt::{LargeSmt, RocksDbConfig, RocksDbStorage}, diff --git a/miden-crypto/src/merkle/smt/large/construction.rs b/miden-crypto/src/merkle/smt/large/construction.rs index fcbdc5fd9..b40f2eb6e 100644 --- a/miden-crypto/src/merkle/smt/large/construction.rs +++ b/miden-crypto/src/merkle/smt/large/construction.rs @@ -59,7 +59,7 @@ impl LargeSmt { /// Returns an error if fetching data from storage fails. /// /// # Example - /// ```no_run + /// ```ignore /// # use miden_crypto::merkle::smt::{LargeSmt, RocksDbConfig, RocksDbStorage}; /// let storage = RocksDbStorage::open(RocksDbConfig::new("/path/to/db")).unwrap(); /// let smt = LargeSmt::load(storage).expect("Failed to load SMT"); @@ -82,7 +82,7 @@ impl LargeSmt { /// - Returns a storage error if fetching data from storage fails. /// /// # Example - /// ```no_run + /// ```ignore /// # use miden_crypto::{Word, merkle::smt::{LargeSmt, RocksDbConfig, RocksDbStorage}}; /// // Load the expected root from your own persistence /// let expected_root: Word = todo!(); diff --git a/miden-crypto/src/merkle/smt/large/mod.rs b/miden-crypto/src/merkle/smt/large/mod.rs index 0d9b796db..eb522dc7e 100644 --- a/miden-crypto/src/merkle/smt/large/mod.rs +++ b/miden-crypto/src/merkle/smt/large/mod.rs @@ -9,7 +9,7 @@ //! Examples below require the `rocksdb` feature. //! //! Load an existing RocksDB-backed tree with root validation: -//! ```no_run +//! ```ignore //! use miden_crypto::{ //! Word, //! merkle::smt::{LargeSmt, RocksDbConfig, RocksDbStorage}, @@ -27,7 +27,7 @@ //! ``` //! //! Load an existing tree without root validation (use with caution): -//! ```no_run +//! ```ignore //! use miden_crypto::merkle::smt::{LargeSmt, RocksDbConfig, RocksDbStorage}; //! //! # fn main() -> Result<(), Box> { @@ -39,7 +39,7 @@ //! ``` //! //! Initialize an empty RocksDB-backed tree and bulk-load entries: -//! ```no_run +//! ```ignore //! use miden_crypto::{ //! Felt, Word, //! merkle::smt::{LargeSmt, RocksDbConfig, RocksDbStorage}, @@ -74,7 +74,7 @@ //! ``` //! //! Apply batch updates (insertions and deletions): -//! ```no_run +//! ```ignore //! use miden_crypto::{ //! EMPTY_WORD, Felt, Word, //! merkle::smt::{LargeSmt, RocksDbConfig, RocksDbStorage}, @@ -98,7 +98,7 @@ //! ``` //! //! Quick initialization with `with_entries` (best for modest datasets/tests): -//! ```no_run +//! ```ignore //! use miden_crypto::{ //! Felt, Word, //! merkle::smt::{LargeSmt, RocksDbConfig, RocksDbStorage}, diff --git a/miden-crypto/src/merkle/smt/large/storage/error.rs b/miden-crypto/src/merkle/smt/large/storage/error.rs index 4aff1ded9..efbd4d1ce 100644 --- a/miden-crypto/src/merkle/smt/large/storage/error.rs +++ b/miden-crypto/src/merkle/smt/large/storage/error.rs @@ -38,5 +38,5 @@ pub enum StorageError { Unsupported(String), /// Higher-level type (e.g., `Word`) failed to decode from bytes. #[error("failed to decode value bytes")] - Value(#[from] winter_utils::DeserializationError), + Value(#[from] crate::utils::DeserializationError), } diff --git a/miden-crypto/src/merkle/smt/large/storage/rocksdb.rs b/miden-crypto/src/merkle/smt/large/storage/rocksdb.rs index f7e43d04a..d2f843fa1 100644 --- a/miden-crypto/src/merkle/smt/large/storage/rocksdb.rs +++ b/miden-crypto/src/merkle/smt/large/storage/rocksdb.rs @@ -6,7 +6,6 @@ use rocksdb::{ DBIteratorWithThreadMode, FlushOptions, IteratorMode, Options, ReadOptions, WriteBatch, WriteOptions, }; -use winter_utils::{Deserializable, Serializable}; use super::{SmtStorage, StorageError, StorageUpdateParts, StorageUpdates, SubtreeUpdate}; use crate::{ @@ -18,6 +17,7 @@ use crate::{ large::{IN_MEMORY_DEPTH, LargeSmt, subtree::Subtree}, }, }, + utils::{Deserializable, Serializable}, }; /// The name of the RocksDB column family used for storing SMT leaves. diff --git a/miden-crypto/src/merkle/smt/mod.rs b/miden-crypto/src/merkle/smt/mod.rs index f7b8b87b3..7803b584d 100644 --- a/miden-crypto/src/merkle/smt/mod.rs +++ b/miden-crypto/src/merkle/smt/mod.rs @@ -6,10 +6,12 @@ use core::{ hash::Hash, }; -use winter_utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; - use super::{EmptySubtreeRoots, InnerNodeInfo, MerkleError, NodeIndex, SparseMerklePath}; -use crate::{EMPTY_WORD, Felt, Map, Word, hash::rpo::Rpo256}; +use crate::{ + EMPTY_WORD, Felt, Map, Word, + hash::rpo::Rpo256, + utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, +}; mod full; pub use full::{MAX_LEAF_ENTRIES, SMT_DEPTH, Smt, SmtLeaf, SmtLeafError, SmtProof, SmtProofError}; diff --git a/miden-crypto/src/merkle/smt/partial.rs b/miden-crypto/src/merkle/smt/partial.rs index 58348271c..b60c1e156 100644 --- a/miden-crypto/src/merkle/smt/partial.rs +++ b/miden-crypto/src/merkle/smt/partial.rs @@ -1,5 +1,3 @@ -use winter_utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; - use super::{LeafIndex, SMT_DEPTH}; use crate::{ EMPTY_WORD, Word, @@ -7,6 +5,7 @@ use crate::{ InnerNodeInfo, MerkleError, NodeIndex, SparseMerklePath, smt::{InnerNode, InnerNodes, Leaves, Smt, SmtLeaf, SmtProof, SparseMerkleTree}, }, + utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, }; /// A partial version of an [`Smt`]. diff --git a/miden-crypto/src/merkle/sparse_path.rs b/miden-crypto/src/merkle/sparse_path.rs index c3cffb1a2..22f23d14e 100644 --- a/miden-crypto/src/merkle/sparse_path.rs +++ b/miden-crypto/src/merkle/sparse_path.rs @@ -4,12 +4,13 @@ use core::{ num::NonZero, }; -use winter_utils::{Deserializable, DeserializationError, Serializable}; - use super::{ EmptySubtreeRoots, InnerNodeInfo, MerkleError, MerklePath, NodeIndex, Word, smt::SMT_MAX_DEPTH, }; -use crate::hash::rpo::Rpo256; +use crate::{ + hash::rpo::Rpo256, + utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, +}; /// A different representation of [`MerklePath`] designed for memory efficiency for Merkle paths /// with empty nodes. @@ -248,7 +249,7 @@ impl SparseMerklePath { // ================================================================================================ impl Serializable for SparseMerklePath { - fn write_into(&self, target: &mut W) { + fn write_into(&self, target: &mut W) { target.write_u8(self.depth()); target.write_u64(self.empty_nodes_mask); target.write_many(&self.nodes); @@ -256,9 +257,7 @@ impl Serializable for SparseMerklePath { } impl Deserializable for SparseMerklePath { - fn read_from( - source: &mut R, - ) -> Result { + fn read_from(source: &mut R) -> Result { let depth = source.read_u8()?; if depth > SMT_MAX_DEPTH { return Err(DeserializationError::InvalidValue(format!( diff --git a/miden-crypto/src/merkle/store/mod.rs b/miden-crypto/src/merkle/store/mod.rs index 9d0fe575a..5d0a379df 100644 --- a/miden-crypto/src/merkle/store/mod.rs +++ b/miden-crypto/src/merkle/store/mod.rs @@ -192,7 +192,7 @@ impl MerkleStore { /// existence verification is needed. pub fn has_path(&self, root: Word, index: NodeIndex) -> bool { // check if the root exists - if self.nodes.get(&root).is_none() { + if !self.nodes.contains_key(&root) { return false; } diff --git a/miden-crypto/src/utils/mod.rs b/miden-crypto/src/utils/mod.rs index c2a1caf89..3a6407bc1 100644 --- a/miden-crypto/src/utils/mod.rs +++ b/miden-crypto/src/utils/mod.rs @@ -3,13 +3,13 @@ use alloc::{string::String, vec::Vec}; use core::fmt::{self, Write}; -use thiserror::Error; #[cfg(feature = "std")] -pub use winter_utils::ReadAdapter; -pub use winter_utils::{ +pub use miden_serde_utils::ReadAdapter; +pub use miden_serde_utils::{ ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, SliceReader, - uninit_vector, }; +use thiserror::Error; +pub use winter_utils::uninit_vector; use crate::{Felt, FieldElement, StarkField, Word}; diff --git a/miden-crypto/src/word/mod.rs b/miden-crypto/src/word/mod.rs index e41f24eec..6c38fbd63 100644 --- a/miden-crypto/src/word/mod.rs +++ b/miden-crypto/src/word/mod.rs @@ -642,6 +642,36 @@ impl Deserializable for Word { } } +// winter_utils compatibility - required for winter_crypto::Digest trait +impl winter_utils::Serializable for Word { + fn write_into(&self, target: &mut W) { + target.write_bytes(&self.as_bytes()); + } + + fn get_size_hint(&self) -> usize { + Self::SERIALIZED_SIZE + } +} + +impl winter_utils::Deserializable for Word { + fn read_from( + source: &mut R, + ) -> Result { + let mut inner: [Felt; WORD_SIZE_FELT] = [ZERO; WORD_SIZE_FELT]; + for inner in inner.iter_mut() { + let e = source.read_u64()?; + if e >= Felt::MODULUS { + return Err(winter_utils::DeserializationError::InvalidValue(String::from( + "value not in the appropriate range", + ))); + } + *inner = Felt::new(e); + } + + Ok(Self(inner)) + } +} + // ITERATORS // ================================================================================================ impl IntoIterator for Word { diff --git a/miden-serde-utils/Cargo.toml b/miden-serde-utils/Cargo.toml new file mode 100644 index 000000000..6b1f27339 --- /dev/null +++ b/miden-serde-utils/Cargo.toml @@ -0,0 +1,28 @@ +[package] +authors.workspace = true +categories.workspace = true +description = "Serialization/deserialization utilities for Miden" +documentation = "https://docs.rs/miden-serde-utils" +edition.workspace = true +keywords.workspace = true +license.workspace = true +name = "miden-serde-utils" +readme = "README.md" +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[features] +default = ["std"] +std = ["winter-math?/std", "winter-utils?/std"] +winter-compat = ["dep:winter-math", "dep:winter-utils"] + +[dependencies] +winter-math = { default-features = false, optional = true, version = "0.13" } +winter-utils = { default-features = false, optional = true, version = "0.13" } + +[lints] +workspace = true + +[package.metadata.cargo-machete] +ignored = ["winter-math", "winter-utils"] diff --git a/miden-serde-utils/README.md b/miden-serde-utils/README.md new file mode 100644 index 000000000..3e0a72c85 --- /dev/null +++ b/miden-serde-utils/README.md @@ -0,0 +1,19 @@ +# Miden Serialization Utilities + +This crate provides serialization and deserialization utilities for Miden projects. + +## Features + +- `ByteReader` trait for reading primitive values from byte sources +- `ByteWriter` trait for writing primitive values to byte sinks +- `Serializable` and `Deserializable` traits for custom types +- Support for both `std` and `no_std` environments + +## Crate Features + +- `std` - enabled by default; enables standard library support +- `winter-compat` - provides `Serializable` and `Deserializable` implementations for types from the `winter-math` and `winter-utils` crates (specifically for `Felt` field elements). This feature exists to work around Rust's orphan rule, which prevents implementing external traits on external types. By implementing these traits in this intermediate crate, both Miden and Winter ecosystem crates can use a common serialization interface + +## License + +Any contribution intentionally submitted for inclusion in this repository, as defined in the Apache-2.0 license, shall be dual licensed under the [MIT](../LICENSE-MIT) and [Apache 2.0](../LICENSE-APACHE) licenses, without any additional terms or conditions. diff --git a/miden-serde-utils/src/byte_reader.rs b/miden-serde-utils/src/byte_reader.rs new file mode 100644 index 000000000..f494271ca --- /dev/null +++ b/miden-serde-utils/src/byte_reader.rs @@ -0,0 +1,834 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +#[cfg(feature = "std")] +use alloc::string::ToString; +use alloc::{format, string::String, vec::Vec}; +#[cfg(feature = "std")] +use core::cell::{Ref, RefCell}; +#[cfg(feature = "std")] +use std::io::BufRead; + +use crate::{Deserializable, DeserializationError}; + +// BYTE READER TRAIT +// ================================================================================================ + +/// Defines how primitive values are to be read from `Self`. +/// +/// Whenever data is read from the reader using any of the `read_*` functions, the reader advances +/// to the next unread byte. If the error occurs, the reader is not rolled back to the state prior +/// to calling any of the function. +pub trait ByteReader { + // REQUIRED METHODS + // -------------------------------------------------------------------------------------------- + + /// Returns a single byte read from `self`. + /// + /// # Errors + /// Returns a [DeserializationError] error the reader is at EOF. + fn read_u8(&mut self) -> Result; + + /// Returns the next byte to be read from `self` without advancing the reader to the next byte. + /// + /// # Errors + /// Returns a [DeserializationError] error the reader is at EOF. + fn peek_u8(&self) -> Result; + + /// Returns a slice of bytes of the specified length read from `self`. + /// + /// # Errors + /// Returns a [DeserializationError] if a slice of the specified length could not be read + /// from `self`. + fn read_slice(&mut self, len: usize) -> Result<&[u8], DeserializationError>; + + /// Returns a byte array of length `N` read from `self`. + /// + /// # Errors + /// Returns a [DeserializationError] if an array of the specified length could not be read + /// from `self`. + fn read_array(&mut self) -> Result<[u8; N], DeserializationError>; + + /// Checks if it is possible to read at least `num_bytes` bytes from this ByteReader + /// + /// # Errors + /// Returns an error if, when reading the requested number of bytes, we go beyond the + /// the data available in the reader. + fn check_eor(&self, num_bytes: usize) -> Result<(), DeserializationError>; + + /// Returns true if there are more bytes left to be read from `self`. + fn has_more_bytes(&self) -> bool; + + // PROVIDED METHODS + // -------------------------------------------------------------------------------------------- + + /// Returns a boolean value read from `self` consuming 1 byte from the reader. + /// + /// # Errors + /// Returns a [DeserializationError] if a u16 value could not be read from `self`. + fn read_bool(&mut self) -> Result { + let byte = self.read_u8()?; + match byte { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(DeserializationError::InvalidValue(format!("{byte} is not a boolean value"))), + } + } + + /// Returns a u16 value read from `self` in little-endian byte order. + /// + /// # Errors + /// Returns a [DeserializationError] if a u16 value could not be read from `self`. + fn read_u16(&mut self) -> Result { + let bytes = self.read_array::<2>()?; + Ok(u16::from_le_bytes(bytes)) + } + + /// Returns a u32 value read from `self` in little-endian byte order. + /// + /// # Errors + /// Returns a [DeserializationError] if a u32 value could not be read from `self`. + fn read_u32(&mut self) -> Result { + let bytes = self.read_array::<4>()?; + Ok(u32::from_le_bytes(bytes)) + } + + /// Returns a u64 value read from `self` in little-endian byte order. + /// + /// # Errors + /// Returns a [DeserializationError] if a u64 value could not be read from `self`. + fn read_u64(&mut self) -> Result { + let bytes = self.read_array::<8>()?; + Ok(u64::from_le_bytes(bytes)) + } + + /// Returns a u128 value read from `self` in little-endian byte order. + /// + /// # Errors + /// Returns a [DeserializationError] if a u128 value could not be read from `self`. + fn read_u128(&mut self) -> Result { + let bytes = self.read_array::<16>()?; + Ok(u128::from_le_bytes(bytes)) + } + + /// Returns a usize value read from `self` in [vint64](https://docs.rs/vint64/latest/vint64/) + /// format. + /// + /// # Errors + /// Returns a [DeserializationError] if: + /// * usize value could not be read from `self`. + /// * encoded value is greater than `usize` maximum value on a given platform. + fn read_usize(&mut self) -> Result { + let first_byte = self.peek_u8()?; + let length = first_byte.trailing_zeros() as usize + 1; + + let result = if length == 9 { + // 9-byte special case + self.read_u8()?; + let value = self.read_array::<8>()?; + u64::from_le_bytes(value) + } else { + let mut encoded = [0u8; 8]; + let value = self.read_slice(length)?; + encoded[..length].copy_from_slice(value); + u64::from_le_bytes(encoded) >> length + }; + + // check if the result value is within acceptable bounds for `usize` on a given platform + if result > usize::MAX as u64 { + return Err(DeserializationError::InvalidValue(format!( + "Encoded value must be less than {}, but {} was provided", + usize::MAX, + result + ))); + } + + Ok(result as usize) + } + + /// Returns a byte vector of the specified length read from `self`. + /// + /// # Errors + /// Returns a [DeserializationError] if a vector of the specified length could not be read + /// from `self`. + fn read_vec(&mut self, len: usize) -> Result, DeserializationError> { + let data = self.read_slice(len)?; + Ok(data.to_vec()) + } + + /// Returns a String of the specified length read from `self`. + /// + /// # Errors + /// Returns a [DeserializationError] if a String of the specified length could not be read + /// from `self`. + fn read_string(&mut self, num_bytes: usize) -> Result { + let data = self.read_vec(num_bytes)?; + String::from_utf8(data).map_err(|err| DeserializationError::InvalidValue(format!("{err}"))) + } + + /// Reads a deserializable value from `self`. + /// + /// # Errors + /// Returns a [DeserializationError] if the specified value could not be read from `self`. + fn read(&mut self) -> Result + where + Self: Sized, + D: Deserializable, + { + D::read_from(self) + } + + /// Reads a sequence of bytes from `self`, attempts to deserialize these bytes into a vector + /// with the specified number of `D` elements, and returns the result. + /// + /// # Errors + /// Returns a [DeserializationError] if the specified number elements could not be read from + /// `self`. + fn read_many(&mut self, num_elements: usize) -> Result, DeserializationError> + where + Self: Sized, + D: Deserializable, + { + let mut result = Vec::with_capacity(num_elements); + for _ in 0..num_elements { + let element = D::read_from(self)?; + result.push(element) + } + Ok(result) + } +} + +// STANDARD LIBRARY ADAPTER +// ================================================================================================ + +/// An adapter of [ByteReader] to any type that implements [std::io::Read] +/// +/// In particular, this covers things like [std::fs::File], standard input, etc. +#[cfg(feature = "std")] +pub struct ReadAdapter<'a> { + // NOTE: The [ByteReader] trait does not currently support reader implementations that require + // mutation during `peek_u8`, `has_more_bytes`, and `check_eor`. These (or equivalent) + // operations on the standard library [std::io::BufRead] trait require a mutable reference, as + // it may be necessary to read from the underlying input to implement them. + // + // To handle this, we wrap the underlying reader in an [RefCell], this allows us to mutate the + // reader if necessary during a call to one of the above-mentioned trait methods, without + // sacrificing safety - at the cost of enforcing Rust's borrowing semantics dynamically. + // + // This should not be a problem in practice, except in the case where `read_slice` is called, + // and the reference returned is from `reader` directly, rather than `buf`. If a call to one + // of the above-mentioned methods is made while that reference is live, and we attempt to read + // from `reader`, a panic will occur. + // + // Ultimately, this should be addressed by making the [ByteReader] trait align with the + // standard library I/O traits, so this is a temporary solution. + reader: RefCell>, + // A temporary buffer to store chunks read from `reader` that are larger than what is required + // for the higher-level [ByteReader] APIs. + // + // By default we attempt to satisfy reads from `reader` directly, but that is not always + // possible. + buf: alloc::vec::Vec, + // The position in `buf` at which we should start reading the next byte, when `buf` is + // non-empty. + pos: usize, + // This is set when we attempt to read from `reader` and get an empty buffer. This indicates + // that once we exhaust `buf`, we have truly reached end-of-file. + // + // We will use this to more accurately handle functions like `has_more_bytes` when this is set. + guaranteed_eof: bool, +} + +#[cfg(feature = "std")] +impl<'a> ReadAdapter<'a> { + /// Create a new [ByteReader] adapter for the given implementation of [std::io::Read] + pub fn new(reader: &'a mut dyn std::io::Read) -> Self { + Self { + reader: RefCell::new(std::io::BufReader::with_capacity(256, reader)), + buf: Default::default(), + pos: 0, + guaranteed_eof: false, + } + } + + /// Get the internal adapter buffer as a (possibly empty) slice of bytes + #[inline(always)] + fn buffer(&self) -> &[u8] { + self.buf.get(self.pos..).unwrap_or(&[]) + } + + /// Get the internal adapter buffer as a slice of bytes, or `None` if the buffer is empty + #[inline(always)] + fn non_empty_buffer(&self) -> Option<&[u8]> { + self.buf.get(self.pos..).filter(|b| !b.is_empty()) + } + + /// Return the current reader buffer as a (possibly empty) slice of bytes. + /// + /// This buffer being empty _does not_ mean we're at EOF, you must call + /// [non_empty_reader_buffer_mut] first. + #[inline(always)] + fn reader_buffer(&self) -> Ref<'_, [u8]> { + Ref::map(self.reader.borrow(), |r| r.buffer()) + } + + /// Return the current reader buffer, reading from the underlying reader + /// if the buffer is empty. + /// + /// Returns `Ok` only if the buffer is non-empty, and no errors occurred + /// while filling it (if filling was needed). + fn non_empty_reader_buffer_mut(&mut self) -> Result<&[u8], DeserializationError> { + use std::io::ErrorKind; + let buf = self.reader.get_mut().fill_buf().map_err(|e| match e.kind() { + ErrorKind::UnexpectedEof => DeserializationError::UnexpectedEOF, + e => DeserializationError::UnknownError(e.to_string()), + })?; + if buf.is_empty() { + self.guaranteed_eof = true; + Err(DeserializationError::UnexpectedEOF) + } else { + Ok(buf) + } + } + + /// Same as [non_empty_reader_buffer_mut], but with dynamically-enforced + /// borrow check rules so that it can be called in functions like `peek_u8`. + /// + /// This comes with overhead for the dynamic checks, so you should prefer + /// to call [non_empty_reader_buffer_mut] if you already have a mutable + /// reference to `self` + fn non_empty_reader_buffer(&self) -> Result, DeserializationError> { + use std::io::ErrorKind; + let mut reader = self.reader.borrow_mut(); + let buf = reader.fill_buf().map_err(|e| match e.kind() { + ErrorKind::UnexpectedEof => DeserializationError::UnexpectedEOF, + e => DeserializationError::UnknownError(e.to_string()), + })?; + if buf.is_empty() { + Err(DeserializationError::UnexpectedEOF) + } else { + // Re-borrow immutably + drop(reader); + Ok(self.reader_buffer()) + } + } + + /// Returns true if there is sufficient capacity remaining in `buf` to hold `n` bytes + #[inline] + fn has_remaining_capacity(&self, n: usize) -> bool { + let remaining = self.buf.capacity() - self.buffer().len(); + remaining >= n + } + + /// Takes the next byte from the input, returning an error if the operation fails + fn pop(&mut self) -> Result { + if let Some(byte) = self.non_empty_buffer().map(|b| b[0]) { + self.pos += 1; + return Ok(byte); + } + let result = self.non_empty_reader_buffer_mut().map(|b| b[0]); + if result.is_ok() { + self.reader.get_mut().consume(1); + } else { + self.guaranteed_eof = true; + } + result + } + + /// Takes the next `N` bytes from the input as an array, returning an error if the operation + /// fails + fn read_exact(&mut self) -> Result<[u8; N], DeserializationError> { + let buf = self.buffer(); + let mut output = [0; N]; + match buf.len() { + 0 => { + let buf = self.non_empty_reader_buffer_mut()?; + if buf.len() < N { + return Err(DeserializationError::UnexpectedEOF); + } + // SAFETY: This copy is guaranteed to be safe, as we have validated above + // that `buf` has at least N bytes, and `output` is defined to be exactly + // N bytes. + unsafe { + core::ptr::copy_nonoverlapping(buf.as_ptr(), output.as_mut_ptr(), N); + } + self.reader.get_mut().consume(N); + }, + n if n >= N => { + // SAFETY: This copy is guaranteed to be safe, as we have validated above + // that `buf` has at least N bytes, and `output` is defined to be exactly + // N bytes. + unsafe { + core::ptr::copy_nonoverlapping(buf.as_ptr(), output.as_mut_ptr(), N); + } + self.pos += N; + }, + n => { + // We have to fill from both the local and reader buffers + self.non_empty_reader_buffer_mut()?; + let reader_buf = self.reader_buffer(); + match reader_buf.len() { + #[cfg(debug_assertions)] + 0 => unreachable!("expected reader buffer to be non-empty to reach here"), + #[cfg(not(debug_assertions))] + // SAFETY: The call to `non_empty_reader_buffer_mut` will return an error + // if `reader_buffer` is non-empty, as a result is is impossible to reach + // here with a length of 0. + 0 => unsafe { core::hint::unreachable_unchecked() }, + // We got enough in one request + m if m + n >= N => { + let needed = N - n; + let dst = output.as_mut_ptr(); + // SAFETY: Both copies are guaranteed to be in-bounds: + // + // * `output` is defined to be exactly N bytes + // * `buf` is guaranteed to be < N bytes + // * `reader_buf` is guaranteed to have the remaining bytes needed, + // and we only copy exactly that many bytes + unsafe { + core::ptr::copy_nonoverlapping(self.buffer().as_ptr(), dst, n); + core::ptr::copy_nonoverlapping(reader_buf.as_ptr(), dst.add(n), needed); + drop(reader_buf); + } + self.pos += n; + self.reader.get_mut().consume(needed); + }, + // We didn't get enough, but haven't necessarily reached eof yet, so fall back + // to filling `self.buf` + m => { + let needed = N - (m + n); + drop(reader_buf); + self.buffer_at_least(needed)?; + debug_assert!( + self.buffer().len() >= N, + "expected buffer to be at least {N} bytes after call to buffer_at_least" + ); + // SAFETY: This is guaranteed to be an in-bounds copy + unsafe { + core::ptr::copy_nonoverlapping( + self.buffer().as_ptr(), + output.as_mut_ptr(), + N, + ); + } + self.pos += N; + return Ok(output); + }, + } + }, + } + + // Check if we should reset our internal buffer + if self.buffer().is_empty() && self.pos > 0 { + unsafe { + self.buf.set_len(0); + } + } + + Ok(output) + } + + /// Fill `self.buf` with `count` bytes + /// + /// This should only be called when we can't read from the reader directly + fn buffer_at_least(&mut self, mut count: usize) -> Result<(), DeserializationError> { + // Read until we have at least `count` bytes, or until we reach end-of-file, + // which ever comes first. + loop { + // If we have successfully read `count` bytes, we're done + if count == 0 || self.buffer().len() >= count { + break Ok(()); + } + + // This operation will return an error if the underlying reader hits EOF + self.non_empty_reader_buffer_mut()?; + + // Extend `self.buf` with the bytes read from the underlying reader. + // + // NOTE: We have to re-borrow the reader buffer here, since we can't get a mutable + // reference to `self.buf` while holding an immutable reference to the reader buffer. + let reader = self.reader.get_mut(); + let buf = reader.buffer(); + let consumed = buf.len(); + self.buf.extend_from_slice(buf); + reader.consume(consumed); + count = count.saturating_sub(consumed); + } + } +} + +#[cfg(feature = "std")] +impl ByteReader for ReadAdapter<'_> { + #[inline(always)] + fn read_u8(&mut self) -> Result { + self.pop() + } + + /// NOTE: If we happen to not have any bytes buffered yet when this is called, then we will be + /// forced to try and read from the underlying reader. This requires a mutable reference, which + /// is obtained dynamically via [RefCell]. + /// + ///
+ /// Callers must ensure that they do not hold any immutable references to the buffer of this + /// reader when calling this function so as to avoid a situation in which the dynamic borrow + /// check fails. Specifically, you must not be holding a reference to the result of + /// [Self::read_slice] when this function is called. + ///
+ fn peek_u8(&self) -> Result { + if let Some(byte) = self.buffer().first() { + return Ok(*byte); + } + self.non_empty_reader_buffer().map(|b| b[0]) + } + + fn read_slice(&mut self, len: usize) -> Result<&[u8], DeserializationError> { + // Edge case + if len == 0 { + return Ok(&[]); + } + + // If we have unused buffer, and the consumed portion is + // large enough, we will move the unused portion of the buffer + // to the start, freeing up bytes at the end for more reads + // before forcing a reallocation + let should_optimize_storage = self.pos >= 16 && !self.has_remaining_capacity(len); + if should_optimize_storage { + // We're going to optimize storage first + let buf = self.buffer(); + let src = buf.as_ptr(); + let count = buf.len(); + let dst = self.buf.as_mut_ptr(); + unsafe { + core::ptr::copy(src, dst, count); + self.buf.set_len(count); + self.pos = 0; + } + } + + // Fill the buffer so we have at least `len` bytes available, + // this will return an error if we hit EOF first + self.buffer_at_least(len)?; + + let slice = &self.buf[self.pos..(self.pos + len)]; + self.pos += len; + Ok(slice) + } + + #[inline] + fn read_array(&mut self) -> Result<[u8; N], DeserializationError> { + if N == 0 { + return Ok([0; N]); + } + self.read_exact() + } + + fn check_eor(&self, num_bytes: usize) -> Result<(), DeserializationError> { + // Do we have sufficient data in the local buffer? + let buffer_len = self.buffer().len(); + if buffer_len >= num_bytes { + return Ok(()); + } + + // What about if we include what is in the local buffer and the reader's buffer? + let reader_buffer_len = self.non_empty_reader_buffer().map(|b| b.len())?; + let buffer_len = buffer_len + reader_buffer_len; + if buffer_len >= num_bytes { + return Ok(()); + } + + // We have no more input, thus can't fulfill a request of `num_bytes` + if self.guaranteed_eof { + return Err(DeserializationError::UnexpectedEOF); + } + + // Because this function is read-only, we must optimistically assume we can read `num_bytes` + // from the input, and fail later if that does not hold. We know we're not at EOF yet, but + // that's all we can say without buffering more from the reader. We could make use of + // `buffer_at_least`, which would guarantee a correct result, but it would also impose + // additional restrictions on the use of this function, e.g. not using it while holding a + // reference returned from `read_slice`. Since it is not a memory safety violation to return + // an optimistic result here, it makes for a better tradeoff. + Ok(()) + } + + #[inline] + fn has_more_bytes(&self) -> bool { + !self.buffer().is_empty() || self.non_empty_reader_buffer().is_ok() + } +} + +// CURSOR +// ================================================================================================ + +#[cfg(feature = "std")] +macro_rules! cursor_remaining_buf { + ($cursor:ident) => {{ + let buf = $cursor.get_ref().as_ref(); + let start = $cursor.position().min(buf.len() as u64) as usize; + &buf[start..] + }}; +} + +#[cfg(feature = "std")] +impl> ByteReader for std::io::Cursor { + fn read_u8(&mut self) -> Result { + let buf = cursor_remaining_buf!(self); + if buf.is_empty() { + Err(DeserializationError::UnexpectedEOF) + } else { + let byte = buf[0]; + self.set_position(self.position() + 1); + Ok(byte) + } + } + + fn peek_u8(&self) -> Result { + cursor_remaining_buf!(self) + .first() + .copied() + .ok_or(DeserializationError::UnexpectedEOF) + } + + fn read_slice(&mut self, len: usize) -> Result<&[u8], DeserializationError> { + let pos = self.position(); + let size = self.get_ref().as_ref().len() as u64; + if size.saturating_sub(pos) < len as u64 { + Err(DeserializationError::UnexpectedEOF) + } else { + self.set_position(pos + len as u64); + let start = pos.min(size) as usize; + Ok(&self.get_ref().as_ref()[start..(start + len)]) + } + } + + fn read_array(&mut self) -> Result<[u8; N], DeserializationError> { + self.read_slice(N).map(|bytes| { + let mut result = [0u8; N]; + result.copy_from_slice(bytes); + result + }) + } + + fn check_eor(&self, num_bytes: usize) -> Result<(), DeserializationError> { + if cursor_remaining_buf!(self).len() >= num_bytes { + Ok(()) + } else { + Err(DeserializationError::UnexpectedEOF) + } + } + + #[inline] + fn has_more_bytes(&self) -> bool { + let pos = self.position(); + let size = self.get_ref().as_ref().len() as u64; + pos < size + } +} + +// SLICE READER +// ================================================================================================ + +/// Implements [ByteReader] trait for a slice of bytes. +/// +/// NOTE: If you are building with the `std` feature, you should probably prefer [std::io::Cursor] +/// instead. However, [SliceReader] is still useful in no-std environments until stabilization of +/// the `core_io_borrowed_buf` feature. +pub struct SliceReader<'a> { + source: &'a [u8], + pos: usize, +} + +impl<'a> SliceReader<'a> { + /// Creates a new slice reader from the specified slice. + pub fn new(source: &'a [u8]) -> Self { + SliceReader { source, pos: 0 } + } +} + +impl ByteReader for SliceReader<'_> { + fn read_u8(&mut self) -> Result { + self.check_eor(1)?; + let result = self.source[self.pos]; + self.pos += 1; + Ok(result) + } + + fn peek_u8(&self) -> Result { + self.check_eor(1)?; + Ok(self.source[self.pos]) + } + + fn read_slice(&mut self, len: usize) -> Result<&[u8], DeserializationError> { + self.check_eor(len)?; + let result = &self.source[self.pos..self.pos + len]; + self.pos += len; + Ok(result) + } + + fn read_array(&mut self) -> Result<[u8; N], DeserializationError> { + self.check_eor(N)?; + let mut result = [0_u8; N]; + result.copy_from_slice(&self.source[self.pos..self.pos + N]); + self.pos += N; + Ok(result) + } + + fn check_eor(&self, num_bytes: usize) -> Result<(), DeserializationError> { + if self.pos + num_bytes > self.source.len() { + return Err(DeserializationError::UnexpectedEOF); + } + Ok(()) + } + + fn has_more_bytes(&self) -> bool { + self.pos < self.source.len() + } +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use std::io::Cursor; + + use super::*; + use crate::ByteWriter; + + #[test] + fn read_adapter_empty() -> Result<(), DeserializationError> { + let mut reader = std::io::empty(); + let mut adapter = ReadAdapter::new(&mut reader); + assert!(!adapter.has_more_bytes()); + assert_eq!(adapter.check_eor(8), Err(DeserializationError::UnexpectedEOF)); + assert_eq!(adapter.peek_u8(), Err(DeserializationError::UnexpectedEOF)); + assert_eq!(adapter.read_u8(), Err(DeserializationError::UnexpectedEOF)); + assert_eq!(adapter.read_slice(0), Ok([].as_slice())); + assert_eq!(adapter.read_slice(1), Err(DeserializationError::UnexpectedEOF)); + assert_eq!(adapter.read_array(), Ok([])); + assert_eq!(adapter.read_array::<1>(), Err(DeserializationError::UnexpectedEOF)); + Ok(()) + } + + #[test] + fn read_adapter_passthrough() -> Result<(), DeserializationError> { + let mut reader = std::io::repeat(0b101); + let mut adapter = ReadAdapter::new(&mut reader); + assert!(adapter.has_more_bytes()); + assert_eq!(adapter.check_eor(8), Ok(())); + assert_eq!(adapter.peek_u8(), Ok(0b101)); + assert_eq!(adapter.read_u8(), Ok(0b101)); + assert_eq!(adapter.read_slice(0), Ok([].as_slice())); + assert_eq!(adapter.read_slice(4), Ok([0b101, 0b101, 0b101, 0b101].as_slice())); + assert_eq!(adapter.read_array(), Ok([])); + assert_eq!(adapter.read_array(), Ok([0b101, 0b101])); + Ok(()) + } + + #[test] + fn read_adapter_exact() { + const VALUE: usize = 2048; + let mut reader = Cursor::new(VALUE.to_le_bytes()); + let mut adapter = ReadAdapter::new(&mut reader); + assert_eq!(usize::from_le_bytes(adapter.read_array().unwrap()), VALUE); + assert!(!adapter.has_more_bytes()); + assert_eq!(adapter.peek_u8(), Err(DeserializationError::UnexpectedEOF)); + assert_eq!(adapter.read_u8(), Err(DeserializationError::UnexpectedEOF)); + } + + #[test] + fn read_adapter_roundtrip() { + const VALUE: usize = 2048; + + // Write VALUE to storage + let mut cursor = Cursor::new([0; core::mem::size_of::()]); + cursor.write_usize(VALUE); + + // Read VALUE from storage + cursor.set_position(0); + let mut adapter = ReadAdapter::new(&mut cursor); + + assert_eq!(adapter.read_usize(), Ok(VALUE)); + } + + #[test] + fn read_adapter_for_file() { + use std::fs::File; + + use crate::ByteWriter; + + let path = std::env::temp_dir().join("read_adapter_for_file.bin"); + + // Encode some data to a buffer, then write that buffer to a file + { + let mut buf = Vec::::with_capacity(256); + buf.write_bytes(b"MAGIC\0"); + buf.write_bool(true); + buf.write_u32(0xbeef); + buf.write_usize(0xfeed); + buf.write_u16(0x5); + + std::fs::write(&path, &buf).unwrap(); + } + + // Open the file, and try to decode the encoded items + let mut file = File::open(&path).unwrap(); + let mut reader = ReadAdapter::new(&mut file); + assert_eq!(reader.peek_u8().unwrap(), b'M'); + assert_eq!(reader.read_slice(6).unwrap(), b"MAGIC\0"); + assert!(reader.read_bool().unwrap()); + assert_eq!(reader.read_u32().unwrap(), 0xbeef); + assert_eq!(reader.read_usize().unwrap(), 0xfeed); + assert_eq!(reader.read_u16().unwrap(), 0x5); + assert!(!reader.has_more_bytes(), "expected there to be no more data in the input"); + } + + #[test] + fn read_adapter_issue_383() { + const STR_BYTES: &[u8] = b"just a string"; + + use std::fs::File; + + use crate::ByteWriter; + + let path = std::env::temp_dir().join("issue_383.bin"); + + // Encode some data to a buffer, then write that buffer to a file + { + let mut buf = vec![0u8; 1024]; + unsafe { + buf.set_len(0); + } + buf.write_u128(2 * u64::MAX as u128); + unsafe { + buf.set_len(512); + } + buf.write_bytes(STR_BYTES); + buf.write_u32(0xbeef); + + std::fs::write(&path, &buf).unwrap(); + } + + // Open the file, and try to decode the encoded items + let mut file = File::open(&path).unwrap(); + let mut reader = ReadAdapter::new(&mut file); + assert_eq!(reader.read_u128().unwrap(), 2 * u64::MAX as u128); + assert_eq!(reader.buf.len(), 0); + assert_eq!(reader.pos, 0); + // Read to offset 512 (we're 16 bytes into the underlying file, i.e. offset of 496) + reader.read_slice(496).unwrap(); + assert_eq!(reader.buf.len(), 496); + assert_eq!(reader.pos, 496); + // The byte string is 13 bytes, followed by 4 bytes containing the trailing u32 value. + // We expect that the underlying reader will buffer the remaining bytes of the file when + // reading STR_BYTES, so the total size of our adapter's buffer should be + // 496 + STR_BYTES.len() + size_of::(); + assert_eq!(reader.read_slice(STR_BYTES.len()).unwrap(), STR_BYTES); + assert_eq!(reader.buf.len(), 496 + STR_BYTES.len() + core::mem::size_of::()); + // We haven't read the u32 yet + assert_eq!(reader.pos, 509); + assert_eq!(reader.read_u32().unwrap(), 0xbeef); + // Now we have + assert_eq!(reader.pos, 513); + assert!(!reader.has_more_bytes(), "expected there to be no more data in the input"); + } +} diff --git a/miden-serde-utils/src/byte_writer.rs b/miden-serde-utils/src/byte_writer.rs new file mode 100644 index 000000000..ca6d2b07a --- /dev/null +++ b/miden-serde-utils/src/byte_writer.rs @@ -0,0 +1,171 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +use crate::Serializable; + +// BYTE WRITER TRAIT +// ================================================================================================ + +/// Defines how primitive values are to be written into `Self`. +pub trait ByteWriter: Sized { + // REQUIRED METHODS + // -------------------------------------------------------------------------------------------- + + /// Writes a single byte into `self`. + /// + /// # Panics + /// Panics if the byte could not be written into `self`. + fn write_u8(&mut self, value: u8); + + /// Writes a sequence of bytes into `self`. + /// + /// # Panics + /// Panics if the sequence of bytes could not be written into `self`. + fn write_bytes(&mut self, values: &[u8]); + + // PROVIDED METHODS + // -------------------------------------------------------------------------------------------- + + /// Writes a boolean value into `self`. + /// + /// A boolean value is written as a single byte. + /// + /// # Panics + /// Panics if the value could not be written into `self`. + fn write_bool(&mut self, val: bool) { + self.write_u8(val as u8); + } + + /// Writes a u16 value in little-endian byte order into `self`. + /// + /// # Panics + /// Panics if the value could not be written into `self`. + fn write_u16(&mut self, value: u16) { + self.write_bytes(&value.to_le_bytes()); + } + + /// Writes a u32 value in little-endian byte order into `self`. + /// + /// # Panics + /// Panics if the value could not be written into `self`. + fn write_u32(&mut self, value: u32) { + self.write_bytes(&value.to_le_bytes()); + } + + /// Writes a u64 value in little-endian byte order into `self`. + /// + /// # Panics + /// Panics if the value could not be written into `self`. + fn write_u64(&mut self, value: u64) { + self.write_bytes(&value.to_le_bytes()); + } + + /// Writes a u128 value in little-endian byte order into `self`. + /// + /// # Panics + /// Panics if the value could not be written into `self`. + fn write_u128(&mut self, value: u128) { + self.write_bytes(&value.to_le_bytes()); + } + + /// Writes a usize value in [vint64](https://docs.rs/vint64/latest/vint64/) format into `self`. + /// + /// # Panics + /// Panics if the value could not be written into `self`. + fn write_usize(&mut self, value: usize) { + // convert the value into a u64 so that we always get 8 bytes from to_le_bytes() call + let value = value as u64; + let length = usize_encoded_len(value); + + // 9-byte special case + if length == 9 { + // length byte is zero in this case + self.write_u8(0); + self.write(value.to_le_bytes()); + } else { + let encoded_bytes = (((value << 1) | 1) << (length - 1)).to_le_bytes(); + self.write_bytes(&encoded_bytes[..length]); + } + } + + /// Writes a serializable value into `self`. + /// + /// # Panics + /// Panics if the value could not be written into `self`. + fn write(&mut self, value: S) { + value.write_into(self) + } + + /// Serializes all `elements` and writes the resulting bytes into `self`. + /// + /// This method does not write any metadata (e.g. number of serialized elements) into `self`. + fn write_many(&mut self, elements: T) + where + T: IntoIterator, + S: Serializable, + { + for element in elements { + element.write_into(self); + } + } +} + +// BYTE WRITER IMPLEMENTATIONS +// ================================================================================================ + +#[cfg(feature = "std")] +impl ByteWriter for W { + #[inline(always)] + fn write_u8(&mut self, byte: u8) { + ::write_all(self, &[byte]).expect("write failed") + } + #[inline(always)] + fn write_bytes(&mut self, bytes: &[u8]) { + ::write_all(self, bytes).expect("write failed") + } +} + +#[cfg(not(feature = "std"))] +impl ByteWriter for alloc::vec::Vec { + fn write_u8(&mut self, value: u8) { + self.push(value); + } + + fn write_bytes(&mut self, values: &[u8]) { + self.extend_from_slice(values); + } +} + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Returns the length of the usize value in vint64 encoding. +pub(super) fn usize_encoded_len(value: u64) -> usize { + let zeros = value.leading_zeros() as usize; + let len = zeros.saturating_sub(1) / 7; + 9 - core::cmp::min(len, 8) +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use std::io::Cursor; + + use super::*; + + #[test] + fn write_adapter_passthrough() { + let mut writer = Cursor::new([0u8; 128]); + writer.write_bytes(b"nope"); + let buf = writer.get_ref(); + assert_eq!(&buf[..4], b"nope"); + } + + #[test] + #[should_panic] + fn write_adapter_writer_out_of_capacity() { + let mut writer = Cursor::new([0; 2]); + writer.write_bytes(b"nope"); + } +} diff --git a/miden-serde-utils/src/lib.rs b/miden-serde-utils/src/lib.rs new file mode 100644 index 000000000..901a6ac7c --- /dev/null +++ b/miden-serde-utils/src/lib.rs @@ -0,0 +1,616 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use alloc::{ + collections::{BTreeMap, BTreeSet}, + format, + string::String, + vec::Vec, +}; + +// ERROR +// ================================================================================================ + +/// Defines errors which can occur during deserialization. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum DeserializationError { + /// Indicates that the deserialization failed because of insufficient data. + UnexpectedEOF, + /// Indicates that the deserialization failed because the value was not valid. + InvalidValue(String), + /// Indicates that deserialization failed for an unknown reason. + UnknownError(String), +} + +#[cfg(feature = "std")] +impl std::error::Error for DeserializationError {} + +impl core::fmt::Display for DeserializationError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::UnexpectedEOF => write!(f, "unexpected end of file"), + Self::InvalidValue(msg) => write!(f, "invalid value: {}", msg), + Self::UnknownError(msg) => write!(f, "unknown error: {}", msg), + } + } +} + +mod byte_reader; +#[cfg(feature = "std")] +pub use byte_reader::ReadAdapter; +pub use byte_reader::{ByteReader, SliceReader}; + +mod byte_writer; +pub use byte_writer::ByteWriter; + +#[cfg(feature = "winter-compat")] +mod winter_compat; + +// SERIALIZABLE TRAIT +// ================================================================================================ + +/// Defines how to serialize `Self` into bytes. +pub trait Serializable { + // REQUIRED METHODS + // -------------------------------------------------------------------------------------------- + /// Serializes `self` into bytes and writes these bytes into the `target`. + fn write_into(&self, target: &mut W); + + // PROVIDED METHODS + // -------------------------------------------------------------------------------------------- + + /// Serializes `self` into a vector of bytes. + fn to_bytes(&self) -> Vec { + let mut result = Vec::with_capacity(self.get_size_hint()); + self.write_into(&mut result); + result + } + + /// Returns an estimate of how many bytes are needed to represent self. + /// + /// The default implementation returns zero. + fn get_size_hint(&self) -> usize { + 0 + } +} + +impl Serializable for &T { + fn write_into(&self, target: &mut W) { + (*self).write_into(target) + } + + fn get_size_hint(&self) -> usize { + (*self).get_size_hint() + } +} + +impl Serializable for () { + fn write_into(&self, _target: &mut W) {} + + fn get_size_hint(&self) -> usize { + 0 + } +} + +impl Serializable for (T1,) +where + T1: Serializable, +{ + fn write_into(&self, target: &mut W) { + self.0.write_into(target); + } + + fn get_size_hint(&self) -> usize { + self.0.get_size_hint() + } +} + +impl Serializable for (T1, T2) +where + T1: Serializable, + T2: Serializable, +{ + fn write_into(&self, target: &mut W) { + self.0.write_into(target); + self.1.write_into(target); + } + + fn get_size_hint(&self) -> usize { + self.0.get_size_hint() + self.1.get_size_hint() + } +} + +impl Serializable for (T1, T2, T3) +where + T1: Serializable, + T2: Serializable, + T3: Serializable, +{ + fn write_into(&self, target: &mut W) { + self.0.write_into(target); + self.1.write_into(target); + self.2.write_into(target); + } + + fn get_size_hint(&self) -> usize { + self.0.get_size_hint() + self.1.get_size_hint() + self.2.get_size_hint() + } +} + +impl Serializable for (T1, T2, T3, T4) +where + T1: Serializable, + T2: Serializable, + T3: Serializable, + T4: Serializable, +{ + fn write_into(&self, target: &mut W) { + self.0.write_into(target); + self.1.write_into(target); + self.2.write_into(target); + self.3.write_into(target); + } + + fn get_size_hint(&self) -> usize { + self.0.get_size_hint() + + self.1.get_size_hint() + + self.2.get_size_hint() + + self.3.get_size_hint() + } +} + +impl Serializable for (T1, T2, T3, T4, T5) +where + T1: Serializable, + T2: Serializable, + T3: Serializable, + T4: Serializable, + T5: Serializable, +{ + fn write_into(&self, target: &mut W) { + self.0.write_into(target); + self.1.write_into(target); + self.2.write_into(target); + self.3.write_into(target); + self.4.write_into(target); + } + + fn get_size_hint(&self) -> usize { + self.0.get_size_hint() + + self.1.get_size_hint() + + self.2.get_size_hint() + + self.3.get_size_hint() + + self.4.get_size_hint() + } +} + +impl Serializable for (T1, T2, T3, T4, T5, T6) +where + T1: Serializable, + T2: Serializable, + T3: Serializable, + T4: Serializable, + T5: Serializable, + T6: Serializable, +{ + fn write_into(&self, target: &mut W) { + self.0.write_into(target); + self.1.write_into(target); + self.2.write_into(target); + self.3.write_into(target); + self.4.write_into(target); + self.5.write_into(target); + } + + fn get_size_hint(&self) -> usize { + self.0.get_size_hint() + + self.1.get_size_hint() + + self.2.get_size_hint() + + self.3.get_size_hint() + + self.4.get_size_hint() + + self.5.get_size_hint() + } +} + +impl Serializable for u8 { + fn write_into(&self, target: &mut W) { + target.write_u8(*self); + } + + fn get_size_hint(&self) -> usize { + core::mem::size_of::() + } +} + +impl Serializable for u16 { + fn write_into(&self, target: &mut W) { + target.write_u16(*self); + } + + fn get_size_hint(&self) -> usize { + core::mem::size_of::() + } +} + +impl Serializable for u32 { + fn write_into(&self, target: &mut W) { + target.write_u32(*self); + } + + fn get_size_hint(&self) -> usize { + core::mem::size_of::() + } +} + +impl Serializable for u64 { + fn write_into(&self, target: &mut W) { + target.write_u64(*self); + } + + fn get_size_hint(&self) -> usize { + core::mem::size_of::() + } +} + +impl Serializable for u128 { + fn write_into(&self, target: &mut W) { + target.write_u128(*self); + } + + fn get_size_hint(&self) -> usize { + core::mem::size_of::() + } +} + +impl Serializable for usize { + fn write_into(&self, target: &mut W) { + target.write_usize(*self) + } + + fn get_size_hint(&self) -> usize { + byte_writer::usize_encoded_len(*self as u64) + } +} + +impl Serializable for Option { + fn write_into(&self, target: &mut W) { + match self { + Some(v) => { + target.write_bool(true); + v.write_into(target); + }, + None => target.write_bool(false), + } + } + + fn get_size_hint(&self) -> usize { + core::mem::size_of::() + self.as_ref().map(|value| value.get_size_hint()).unwrap_or(0) + } +} + +impl Serializable for [T; C] { + fn write_into(&self, target: &mut W) { + target.write_many(self) + } + + fn get_size_hint(&self) -> usize { + let mut size = 0; + for item in self { + size += item.get_size_hint(); + } + size + } +} + +impl Serializable for [T] { + fn write_into(&self, target: &mut W) { + target.write_usize(self.len()); + for element in self.iter() { + element.write_into(target); + } + } + + fn get_size_hint(&self) -> usize { + let mut size = self.len().get_size_hint(); + for element in self { + size += element.get_size_hint(); + } + size + } +} + +impl Serializable for Vec { + fn write_into(&self, target: &mut W) { + target.write_usize(self.len()); + target.write_many(self); + } + + fn get_size_hint(&self) -> usize { + let mut size = self.len().get_size_hint(); + for item in self { + size += item.get_size_hint(); + } + size + } +} + +impl Serializable for BTreeMap { + fn write_into(&self, target: &mut W) { + target.write_usize(self.len()); + target.write_many(self); + } + + fn get_size_hint(&self) -> usize { + let mut size = self.len().get_size_hint(); + for item in self { + size += item.get_size_hint(); + } + size + } +} + +impl Serializable for BTreeSet { + fn write_into(&self, target: &mut W) { + target.write_usize(self.len()); + target.write_many(self); + } + + fn get_size_hint(&self) -> usize { + let mut size = self.len().get_size_hint(); + for item in self { + size += item.get_size_hint(); + } + size + } +} + +impl Serializable for str { + fn write_into(&self, target: &mut W) { + target.write_usize(self.len()); + target.write_many(self.as_bytes()); + } + + fn get_size_hint(&self) -> usize { + self.len().get_size_hint() + self.len() + } +} + +impl Serializable for String { + fn write_into(&self, target: &mut W) { + target.write_usize(self.len()); + target.write_many(self.as_bytes()); + } + + fn get_size_hint(&self) -> usize { + self.len().get_size_hint() + self.len() + } +} + +// DESERIALIZABLE +// ================================================================================================ + +/// Defines how to deserialize `Self` from bytes. +pub trait Deserializable: Sized { + // REQUIRED METHODS + // -------------------------------------------------------------------------------------------- + + /// Reads a sequence of bytes from the provided `source`, attempts to deserialize these bytes + /// into `Self`, and returns the result. + /// + /// # Errors + /// Returns an error if: + /// * The `source` does not contain enough bytes to deserialize `Self`. + /// * Bytes read from the `source` do not represent a valid value for `Self`. + fn read_from(source: &mut R) -> Result; + + // PROVIDED METHODS + // -------------------------------------------------------------------------------------------- + + /// Attempts to deserialize the provided `bytes` into `Self` and returns the result. + /// + /// # Errors + /// Returns an error if: + /// * The `bytes` do not contain enough information to deserialize `Self`. + /// * The `bytes` do not represent a valid value for `Self`. + /// + /// Note: if `bytes` contains more data than needed to deserialize `self`, no error is + /// returned. + fn read_from_bytes(bytes: &[u8]) -> Result { + Self::read_from(&mut SliceReader::new(bytes)) + } +} + +impl Deserializable for () { + fn read_from(_source: &mut R) -> Result { + Ok(()) + } +} + +impl Deserializable for (T1,) +where + T1: Deserializable, +{ + fn read_from(source: &mut R) -> Result { + let v1 = T1::read_from(source)?; + Ok((v1,)) + } +} + +impl Deserializable for (T1, T2) +where + T1: Deserializable, + T2: Deserializable, +{ + fn read_from(source: &mut R) -> Result { + let v1 = T1::read_from(source)?; + let v2 = T2::read_from(source)?; + Ok((v1, v2)) + } +} + +impl Deserializable for (T1, T2, T3) +where + T1: Deserializable, + T2: Deserializable, + T3: Deserializable, +{ + fn read_from(source: &mut R) -> Result { + let v1 = T1::read_from(source)?; + let v2 = T2::read_from(source)?; + let v3 = T3::read_from(source)?; + Ok((v1, v2, v3)) + } +} + +impl Deserializable for (T1, T2, T3, T4) +where + T1: Deserializable, + T2: Deserializable, + T3: Deserializable, + T4: Deserializable, +{ + fn read_from(source: &mut R) -> Result { + let v1 = T1::read_from(source)?; + let v2 = T2::read_from(source)?; + let v3 = T3::read_from(source)?; + let v4 = T4::read_from(source)?; + Ok((v1, v2, v3, v4)) + } +} + +impl Deserializable for (T1, T2, T3, T4, T5) +where + T1: Deserializable, + T2: Deserializable, + T3: Deserializable, + T4: Deserializable, + T5: Deserializable, +{ + fn read_from(source: &mut R) -> Result { + let v1 = T1::read_from(source)?; + let v2 = T2::read_from(source)?; + let v3 = T3::read_from(source)?; + let v4 = T4::read_from(source)?; + let v5 = T5::read_from(source)?; + Ok((v1, v2, v3, v4, v5)) + } +} + +impl Deserializable for (T1, T2, T3, T4, T5, T6) +where + T1: Deserializable, + T2: Deserializable, + T3: Deserializable, + T4: Deserializable, + T5: Deserializable, + T6: Deserializable, +{ + fn read_from(source: &mut R) -> Result { + let v1 = T1::read_from(source)?; + let v2 = T2::read_from(source)?; + let v3 = T3::read_from(source)?; + let v4 = T4::read_from(source)?; + let v5 = T5::read_from(source)?; + let v6 = T6::read_from(source)?; + Ok((v1, v2, v3, v4, v5, v6)) + } +} + +impl Deserializable for u8 { + fn read_from(source: &mut R) -> Result { + source.read_u8() + } +} + +impl Deserializable for u16 { + fn read_from(source: &mut R) -> Result { + source.read_u16() + } +} + +impl Deserializable for u32 { + fn read_from(source: &mut R) -> Result { + source.read_u32() + } +} + +impl Deserializable for u64 { + fn read_from(source: &mut R) -> Result { + source.read_u64() + } +} + +impl Deserializable for u128 { + fn read_from(source: &mut R) -> Result { + source.read_u128() + } +} + +impl Deserializable for usize { + fn read_from(source: &mut R) -> Result { + source.read_usize() + } +} + +impl Deserializable for Option { + fn read_from(source: &mut R) -> Result { + let contains = source.read_bool()?; + + match contains { + true => Ok(Some(T::read_from(source)?)), + false => Ok(None), + } + } +} + +impl Deserializable for [T; C] { + fn read_from(source: &mut R) -> Result { + let data: Vec = source.read_many(C)?; + + // SAFETY: the call above only returns a Vec if there are `C` elements, this conversion + // always succeeds + let res = data.try_into().unwrap_or_else(|v: Vec| { + panic!("Expected a Vec of length {} but it was {}", C, v.len()) + }); + + Ok(res) + } +} + +impl Deserializable for Vec { + fn read_from(source: &mut R) -> Result { + let len = source.read_usize()?; + source.read_many(len) + } +} + +impl Deserializable for BTreeMap { + fn read_from(source: &mut R) -> Result { + let len = source.read_usize()?; + let data = source.read_many(len)?; + Ok(BTreeMap::from_iter(data)) + } +} + +impl Deserializable for BTreeSet { + fn read_from(source: &mut R) -> Result { + let len = source.read_usize()?; + let data = source.read_many(len)?; + Ok(BTreeSet::from_iter(data)) + } +} + +impl Deserializable for String { + fn read_from(source: &mut R) -> Result { + let len = source.read_usize()?; + let data = source.read_many(len)?; + + String::from_utf8(data).map_err(|err| DeserializationError::InvalidValue(format!("{err}"))) + } +} diff --git a/miden-serde-utils/src/winter_compat.rs b/miden-serde-utils/src/winter_compat.rs new file mode 100644 index 000000000..a66aed54e --- /dev/null +++ b/miden-serde-utils/src/winter_compat.rs @@ -0,0 +1,29 @@ +//! Compatibility layer implementing miden-serde-utils traits for winter ecosystem types. + +use crate::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; + +// WINTER_MATH FIELD ELEMENT IMPLEMENTATIONS +// ================================================================================================ + +impl Serializable for winter_math::fields::f64::BaseElement { + fn write_into(&self, target: &mut W) { + target.write_u64(self.as_int()); + } + + fn get_size_hint(&self) -> usize { + core::mem::size_of::() + } +} + +impl Deserializable for winter_math::fields::f64::BaseElement { + fn read_from(source: &mut R) -> Result { + use winter_math::StarkField; + let value = source.read_u64()?; + if value >= Self::MODULUS { + return Err(DeserializationError::InvalidValue( + "field element value exceeds modulus".into(), + )); + } + Ok(Self::new(value)) + } +} diff --git a/scripts/check-features.sh b/scripts/check-features.sh new file mode 100755 index 000000000..10c17940c --- /dev/null +++ b/scripts/check-features.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -euo pipefail + +# Script to check all feature combinations compile without warnings +# This script ensures that warnings are treated as errors for CI + +echo "Checking all feature combinations with cargo-hack..." + +# Set environment variables to treat warnings as errors +export RUSTFLAGS="-D warnings" + +# Run cargo-hack with comprehensive feature checking +# Note: We exclude 'default' to test non-default feature combinations +# and use --each-feature to test each feature individually +cargo hack check \ + --workspace \ + --each-feature \ + --exclude-features default \ + --all-targets + +echo "" +echo "All feature combinations compiled successfully!"