diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1d0ae53c40..5950afdfc9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,3 +34,21 @@ jobs: rustup default ${{ matrix.toolchain }} rustup target add wasm32-unknown-unknown make build-no-std + + target-miden: + name: Build miden-field for on-chain target + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + toolchain: [stable, nightly] + steps: + - uses: actions/checkout@main + - name: Cleanup large tools for build space + uses: ./.github/actions/cleanup-runner + - name: Build miden-field for on-chain target + run: | + rustup update --no-self-update ${{ matrix.toolchain }} + rustup default ${{ matrix.toolchain }} + rustup target add wasm32-wasip2 + make build-target-miden diff --git a/CHANGELOG.md b/CHANGELOG.md index 376952ed47..5fcec1a117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.22.3 (unreleased) + +- Refactored to introduce a unified `Felt` type for on-chain and off-chain code ([#819](https://github.com/0xMiden/crypto/pull/819)). + ## 0.22.2 (2026-02-01) - Re-exported `p3_keccak::VECTOR_LEN`. diff --git a/Cargo.lock b/Cargo.lock index 950d51c268..8ec7155b89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -964,6 +964,7 @@ dependencies = [ "itertools 0.14.0", "k256", "miden-crypto-derive", + "miden-field", "miden-serde-utils", "num", "num-complex", @@ -1010,6 +1011,23 @@ dependencies = [ "syn", ] +[[package]] +name = "miden-field" +version = "0.22.2" +dependencies = [ + "miden-serde-utils", + "num-bigint", + "p3-challenger", + "p3-field", + "p3-goldilocks", + "paste", + "proptest", + "rand", + "rstest", + "serde", + "thiserror", +] + [[package]] name = "miden-serde-utils" version = "0.22.2" @@ -1584,6 +1602,7 @@ dependencies = [ "rand", "rand_chacha", "rand_xorshift", + "regex-syntax", "unarray", ] diff --git a/Cargo.toml b/Cargo.toml index e08a2245b7..e85a659d1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] exclude = ["miden-crypto-fuzz"] -members = ["miden-crypto", "miden-crypto-derive", "miden-serde-utils"] +members = ["miden-crypto", "miden-crypto-derive", "miden-field", "miden-serde-utils"] resolver = "3" [workspace.package] @@ -15,6 +15,7 @@ version = "0.22.2" [workspace.dependencies] miden-crypto-derive = { path = "miden-crypto-derive", version = "0.22" } +miden-field = { path = "miden-field", version = "0.22" } miden-serde-utils = { path = "miden-serde-utils", version = "0.22" } [workspace.lints.rust] diff --git a/Makefile b/Makefile index ff45aa8578..b9a66ee613 100644 --- a/Makefile +++ b/Makefile @@ -112,6 +112,10 @@ build: ## Build with default features enabled build-no-std: ## Build without the standard library cargo build --release --no-default-features --target wasm32-unknown-unknown +.PHONY: build-target-miden +build-target-miden: ## Build `miden-field` for wasm32-wasip2 with `--cfg miden` + RUSTFLAGS="$${RUSTFLAGS:+$$RUSTFLAGS }--cfg miden" cargo build --release -p miden-field --target wasm32-wasip2 + .PHONY: build-avx2 build-avx2: ## Build with avx2 support RUSTFLAGS="-C target-feature=+avx2" cargo build --release diff --git a/miden-crypto/Cargo.toml b/miden-crypto/Cargo.toml index 2c55d43732..3649c283c0 100644 --- a/miden-crypto/Cargo.toml +++ b/miden-crypto/Cargo.toml @@ -86,36 +86,40 @@ internal = ["concurrent"] rocksdb = ["concurrent", "dep:rocksdb"] serde = ["dep:serde", "serde?/alloc"] std = ["blake3/std", "dep:cc", "miden-serde-utils/std", "rand/std", "rand/thread_rng"] -testing = ["dep:proptest"] +testing = ["dep:proptest", "miden-field/testing"] [dependencies] -blake3 = { default-features = false, version = "1.8" } -chacha20poly1305 = { features = ["alloc", "stream"], version = "0.10" } -clap = { features = ["derive"], optional = true, version = "4.5" } -curve25519-dalek = { default-features = false, version = "4" } -ed25519-dalek = { features = ["zeroize"], version = "2" } -flume = { version = "0.11.1" } -hashbrown = { features = ["serde"], optional = true, version = "0.16" } -hkdf = { default-features = false, version = "0.12" } -k256 = { features = ["ecdh", "ecdsa"], version = "0.13" } +# Miden dependencies miden-crypto-derive.workspace = true +miden-field.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" } -proptest = { default-features = false, features = ["alloc"], optional = true, version = "1.7" } -rand = { default-features = false, version = "0.9" } -rand-utils = { optional = true, package = "winter-rand-utils", version = "0.13" } -rand_chacha = { default-features = false, version = "0.9" } -rand_core = { default-features = false, version = "0.9" } -rand_hc = { version = "0.3" } -rayon = { optional = true, version = "1.10" } -rocksdb = { default-features = false, features = ["bindgen-runtime", "lz4"], optional = true, version = "0.24" } -serde = { default-features = false, features = ["derive"], optional = true, version = "1.0" } -sha2 = { default-features = false, version = "0.10" } -sha3 = { default-features = false, version = "0.10" } -subtle = { default-features = false, version = "2.6" } -thiserror = { default-features = false, version = "2.0" } -x25519-dalek = { default-features = false, features = ["static_secrets"], version = "2.0" } + +# External dependencies +blake3 = { default-features = false, version = "1.8" } +chacha20poly1305 = { features = ["alloc", "stream"], version = "0.10" } +clap = { features = ["derive"], optional = true, version = "4.5" } +curve25519-dalek = { default-features = false, version = "4" } +ed25519-dalek = { features = ["zeroize"], version = "2" } +flume = { version = "0.11.1" } +hashbrown = { features = ["serde"], optional = true, version = "0.16" } +hkdf = { default-features = false, version = "0.12" } +k256 = { features = ["ecdh", "ecdsa"], version = "0.13" } +num = { default-features = false, features = ["alloc", "libm"], version = "0.4" } +num-complex = { default-features = false, version = "0.4" } +proptest = { default-features = false, features = ["alloc"], optional = true, version = "1.7" } +rand = { default-features = false, version = "0.9" } +rand-utils = { optional = true, package = "winter-rand-utils", version = "0.13" } +rand_chacha = { default-features = false, version = "0.9" } +rand_core = { default-features = false, version = "0.9" } +rand_hc = { version = "0.3" } +rayon = { optional = true, version = "1.10" } +rocksdb = { default-features = false, features = ["bindgen-runtime", "lz4"], optional = true, version = "0.24" } +serde = { default-features = false, features = ["derive"], optional = true, version = "1.0" } +sha2 = { default-features = false, version = "0.10" } +sha3 = { default-features = false, version = "0.10" } +subtle = { default-features = false, version = "2.6" } +thiserror = { default-features = false, version = "2.0" } +x25519-dalek = { default-features = false, features = ["static_secrets"], version = "2.0" } # Upstream Plonky3 dependencies p3-air = { default-features = false, version = "0.4.2" } @@ -142,6 +146,7 @@ assert_matches = { default-features = false, version = "1.5" } criterion = { features = ["html_reports"], version = "0.7" } hex = { default-features = false, features = ["alloc"], version = "0.4" } itertools = { version = "0.14" } +miden-field = { features = ["testing"], workspace = true } proptest = { default-features = false, features = ["alloc"], version = "1.7" } rand-utils = { package = "winter-rand-utils", version = "0.13" } rstest = { version = "0.26" } diff --git a/miden-crypto/benches/word.rs b/miden-crypto/benches/word.rs index bfd0bd347a..a3e6666b4f 100644 --- a/miden-crypto/benches/word.rs +++ b/miden-crypto/benches/word.rs @@ -22,7 +22,7 @@ use criterion::{Criterion, criterion_group, criterion_main}; // Import Word modules -use miden_crypto::{Felt, Word, word::LexicographicWord}; +use miden_crypto::{Felt, LexicographicWord, Word}; // Import common utilities mod common; diff --git a/miden-crypto/src/hash/blake/tests.rs b/miden-crypto/src/hash/blake/tests.rs index 57c810af00..2d929f1360 100644 --- a/miden-crypto/src/hash/blake/tests.rs +++ b/miden-crypto/src/hash/blake/tests.rs @@ -2,11 +2,10 @@ use alloc::vec::Vec; use p3_field::PrimeField64; -use p3_goldilocks::Goldilocks as Felt; use proptest::prelude::*; use super::*; -use crate::rand::test_utils::rand_vector; +use crate::{Felt, rand::test_utils::rand_vector}; #[test] fn blake3_hash_elements() { diff --git a/miden-crypto/src/lib.rs b/miden-crypto/src/lib.rs index 18017ba3f2..b11ac403c1 100644 --- a/miden-crypto/src/lib.rs +++ b/miden-crypto/src/lib.rs @@ -15,21 +15,20 @@ pub mod ies; pub mod merkle; pub mod rand; pub mod utils; -pub mod word; // RE-EXPORTS // ================================================================================================ -pub use p3_goldilocks::Goldilocks as Felt; -pub use word::{Word, WordError}; +pub use miden_field::{Felt, LexicographicWord, Word, WordError, word}; pub mod field { //! Traits and utilities for working with the Goldilocks finite field (i.e., //! [Felt](super::Felt)). - pub use p3_field::{ - BasedVectorSpace, ExtensionField, Field, PrimeCharacteristicRing, PrimeField64, - TwoAdicField, batch_multiplicative_inverse, extension::BinomialExtensionField, - integers::QuotientMap, + pub use miden_field::{ + BasedVectorSpace, BinomialExtensionField, BinomiallyExtendable, + BinomiallyExtendableAlgebra, ExtensionField, Field, HasTwoAdicBinomialExtension, + InjectiveMonomial, Packable, PermutationMonomial, PrimeCharacteristicRing, PrimeField, + PrimeField64, QuotientMap, RawDataSerializable, TwoAdicField, batch_multiplicative_inverse, }; pub use super::batch_inversion::batch_inversion_allow_zeros; @@ -140,7 +139,7 @@ pub type Set = alloc::collections::BTreeSet; // ================================================================================================ /// Number of field elements in a word. -pub const WORD_SIZE: usize = 4; +pub const WORD_SIZE: usize = word::WORD_SIZE_FELTS; /// Field element representing ZERO in the Miden base filed. pub const ZERO: Felt = Felt::ZERO; diff --git a/miden-crypto/src/rand/mod.rs b/miden-crypto/src/rand/mod.rs index 7220397218..3243c0e2a0 100644 --- a/miden-crypto/src/rand/mod.rs +++ b/miden-crypto/src/rand/mod.rs @@ -1,5 +1,6 @@ //! Pseudo-random element generation. +use miden_field::word::WORD_SIZE_BYTES; use p3_field::PrimeField64; use rand::RngCore; @@ -104,6 +105,19 @@ impl Randomizable for Felt { } } +impl Randomizable for Word { + const VALUE_SIZE: usize = WORD_SIZE_BYTES; + + fn from_random_bytes(bytes: &[u8]) -> Option { + let bytes_array: Option<[u8; 32]> = bytes.try_into().ok(); + if let Some(bytes_array) = bytes_array { + Self::try_from(bytes_array).ok() + } else { + None + } + } +} + impl Randomizable for [u8; N] { const VALUE_SIZE: usize = N; diff --git a/miden-crypto/src/utils/mod.rs b/miden-crypto/src/utils/mod.rs index de2a93907c..e5b772d5ba 100644 --- a/miden-crypto/src/utils/mod.rs +++ b/miden-crypto/src/utils/mod.rs @@ -12,7 +12,6 @@ pub use miden_serde_utils::{ }; use p3_field::{PrimeCharacteristicRing, RawDataSerializable, integers::QuotientMap}; use p3_maybe_rayon::prelude::*; -use thiserror::Error; use crate::{Felt, Word, field::PrimeField64}; @@ -41,59 +40,7 @@ pub fn word_to_hex(w: &Word) -> Result { Ok(s) } -/// Renders an array of bytes as hex into a String. -pub fn bytes_to_hex_string(data: [u8; N]) -> String { - let mut s = String::with_capacity(N + 2); - - s.push_str("0x"); - for byte in data.iter() { - write!(s, "{byte:02x}").expect("formatting hex failed"); - } - - s -} - -/// Defines errors which can occur during parsing of hexadecimal strings. -#[derive(Debug, Error)] -pub enum HexParseError { - #[error("expected hex data to have length {expected}, including the 0x prefix, found {actual}")] - InvalidLength { expected: usize, actual: usize }, - #[error("hex encoded data must start with 0x prefix")] - MissingPrefix, - #[error("hex encoded data must contain only characters [0-9a-fA-F]")] - InvalidChar, - #[error("hex encoded values of a Digest must be inside the field modulus")] - OutOfRange, -} - -/// Parses a hex string into an array of bytes of known size. -pub fn hex_to_bytes(value: &str) -> Result<[u8; N], HexParseError> { - let expected: usize = (N * 2) + 2; - if value.len() != expected { - return Err(HexParseError::InvalidLength { expected, actual: value.len() }); - } - - if !value.starts_with("0x") { - return Err(HexParseError::MissingPrefix); - } - - let mut data = value.bytes().skip(2).map(|v| match v { - b'0'..=b'9' => Ok(v - b'0'), - b'a'..=b'f' => Ok(v - b'a' + 10), - b'A'..=b'F' => Ok(v - b'A' + 10), - _ => Err(HexParseError::InvalidChar), - }); - - let mut decoded = [0u8; N]; - for byte in decoded.iter_mut() { - // These `unwrap` calls are okay because the length was checked above - let high: u8 = data.next().unwrap()?; - let low: u8 = data.next().unwrap()?; - *byte = (high << 4) + low; - } - - Ok(decoded) -} +pub use miden_field::utils::{HexParseError, bytes_to_hex_string, hex_to_bytes}; // CONVERSIONS BETWEEN BYTES AND ELEMENTS // ================================================================================================ diff --git a/miden-crypto/src/word/tests.rs b/miden-crypto/src/word/tests.rs deleted file mode 100644 index 0fe1198249..0000000000 --- a/miden-crypto/src/word/tests.rs +++ /dev/null @@ -1,323 +0,0 @@ -#![cfg(feature = "std")] -use alloc::string::String; - -use p3_field::PrimeCharacteristicRing; - -use super::{Deserializable, Felt, Serializable, WORD_SIZE_BYTES, WORD_SIZE_FELT, Word}; -use crate::{rand::test_utils::rand_value, utils::SliceReader, word}; - -// TESTS -// ================================================================================================ - -#[test] -fn word_serialization() { - let e1 = Felt::new(rand_value()); - let e2 = Felt::new(rand_value()); - let e3 = Felt::new(rand_value()); - let e4 = Felt::new(rand_value()); - - let d1 = Word([e1, e2, e3, e4]); - - let mut bytes = vec![]; - d1.write_into(&mut bytes); - assert_eq!(WORD_SIZE_BYTES, bytes.len()); - assert_eq!(bytes.len(), d1.get_size_hint()); - - let mut reader = SliceReader::new(&bytes); - let d2 = Word::read_from(&mut reader).unwrap(); - - assert_eq!(d1, d2); -} - -#[test] -fn word_encoding() { - let word = Word([ - Felt::new(rand_value()), - Felt::new(rand_value()), - Felt::new(rand_value()), - Felt::new(rand_value()), - ]); - - let string: String = word.into(); - let round_trip: Word = string.try_into().expect("decoding failed"); - - assert_eq!(word, round_trip); -} - -#[test] -fn test_conversions() { - let word = Word([ - Felt::new(rand_value()), - Felt::new(rand_value()), - Felt::new(rand_value()), - Felt::new(rand_value()), - ]); - - // BY VALUE - // ---------------------------------------------------------------------------------------- - let v: [bool; WORD_SIZE_FELT] = [true, false, true, true]; - let v2: Word = v.into(); - assert_eq!(v, <[bool; WORD_SIZE_FELT]>::try_from(v2).unwrap()); - - let v: [u8; WORD_SIZE_FELT] = [0_u8, 1_u8, 2_u8, 3_u8]; - let v2: Word = v.into(); - assert_eq!(v, <[u8; WORD_SIZE_FELT]>::try_from(v2).unwrap()); - - let v: [u16; WORD_SIZE_FELT] = [0_u16, 1_u16, 2_u16, 3_u16]; - let v2: Word = v.into(); - assert_eq!(v, <[u16; WORD_SIZE_FELT]>::try_from(v2).unwrap()); - - let v: [u32; WORD_SIZE_FELT] = [0_u32, 1_u32, 2_u32, 3_u32]; - let v2: Word = v.into(); - assert_eq!(v, <[u32; WORD_SIZE_FELT]>::try_from(v2).unwrap()); - - let v: [u64; WORD_SIZE_FELT] = word.into(); - let v2: Word = v.try_into().unwrap(); - assert_eq!(word, v2); - - let v: [Felt; WORD_SIZE_FELT] = word.into(); - let v2: Word = v.into(); - assert_eq!(word, v2); - - let v: [u8; WORD_SIZE_BYTES] = word.into(); - let v2: Word = v.try_into().unwrap(); - assert_eq!(word, v2); - - let v: String = word.into(); - let v2: Word = v.try_into().unwrap(); - assert_eq!(word, v2); - - // BY REF - // ---------------------------------------------------------------------------------------- - let v: [bool; WORD_SIZE_FELT] = [true, false, true, true]; - let v2: Word = (&v).into(); - assert_eq!(v, <[bool; WORD_SIZE_FELT]>::try_from(&v2).unwrap()); - - let v: [u8; WORD_SIZE_FELT] = [0_u8, 1_u8, 2_u8, 3_u8]; - let v2: Word = (&v).into(); - assert_eq!(v, <[u8; WORD_SIZE_FELT]>::try_from(&v2).unwrap()); - - let v: [u16; WORD_SIZE_FELT] = [0_u16, 1_u16, 2_u16, 3_u16]; - let v2: Word = (&v).into(); - assert_eq!(v, <[u16; WORD_SIZE_FELT]>::try_from(&v2).unwrap()); - - let v: [u32; WORD_SIZE_FELT] = [0_u32, 1_u32, 2_u32, 3_u32]; - let v2: Word = (&v).into(); - assert_eq!(v, <[u32; WORD_SIZE_FELT]>::try_from(&v2).unwrap()); - - let v: [u64; WORD_SIZE_FELT] = (&word).into(); - let v2: Word = (&v).try_into().unwrap(); - assert_eq!(word, v2); - - let v: [Felt; WORD_SIZE_FELT] = (&word).into(); - let v2: Word = (&v).into(); - assert_eq!(word, v2); - - let v: [u8; WORD_SIZE_BYTES] = (&word).into(); - let v2: Word = (&v).try_into().unwrap(); - assert_eq!(word, v2); - - let v: String = (&word).into(); - let v2: Word = (&v).try_into().unwrap(); - assert_eq!(word, v2); -} - -#[test] -fn test_index() { - let word = Word::new([ - Felt::from_u32(1_u32), - Felt::from_u32(2_u32), - Felt::from_u32(3_u32), - Felt::from_u32(4_u32), - ]); - assert_eq!(word[0], Felt::from_u32(1_u32)); - assert_eq!(word[1], Felt::from_u32(2_u32)); - assert_eq!(word[2], Felt::from_u32(3_u32)); - assert_eq!(word[3], Felt::from_u32(4_u32)); -} - -#[test] -fn test_index_mut() { - let mut word = Word::new([ - Felt::from_u32(1_u32), - Felt::from_u32(2_u32), - Felt::from_u32(3_u32), - Felt::from_u32(4_u32), - ]); - - word[0] = Felt::from_u32(5_u32); - word[1] = Felt::from_u32(6_u32); - word[2] = Felt::from_u32(7_u32); - word[3] = Felt::from_u32(8_u32); - assert_eq!(word[0], Felt::from_u32(5_u32)); - assert_eq!(word[1], Felt::from_u32(6_u32)); - assert_eq!(word[2], Felt::from_u32(7_u32)); - assert_eq!(word[3], Felt::from_u32(8_u32)); -} - -#[test] -fn test_index_mut_range() { - let mut word = Word::new([ - Felt::from_u32(1_u32), - Felt::from_u32(2_u32), - Felt::from_u32(3_u32), - Felt::from_u32(4_u32), - ]); - - word[1..3].copy_from_slice(&[Felt::from_u32(6_u32), Felt::from_u32(7_u32)]); - assert_eq!(word[1], Felt::from_u32(6_u32)); - assert_eq!(word[2], Felt::from_u32(7_u32)); -} - -#[rstest::rstest] -#[case::missing_prefix("1234")] -#[case::invalid_character("1234567890abcdefg")] -#[case::too_long("0xx00000000000000000000000000000000000000000000000000000000000000001")] -#[case::overflow_felt0("0x01000000ffffffff000000000000000000000000000000000000000000000000")] -#[case::overflow_felt1("0x000000000000000001000000ffffffff00000000000000000000000000000000")] -#[case::overflow_felt2("0x0000000000000000000000000000000001000000ffffffff0000000000000000")] -#[case::overflow_felt3("0x00000000000000000000000000000000000000000000000001000000ffffffff")] -#[should_panic] -fn word_macro_invalid(#[case] bad_input: &str) { - word!(bad_input); -} - -#[rstest::rstest] -#[case::each_digit("0x1234567890abcdef")] -#[case::empty("0x")] -#[case::zero("0x0")] -#[case::zero_full("0x0000000000000000000000000000000000000000000000000000000000000000")] -#[case::one_lsb("0x1")] -#[case::one_msb("0x0000000000000000000000000000000000000000000000000000000000000001")] -#[case::one_partial("0x0001")] -#[case::odd("0x123")] -#[case::even("0x1234")] -#[case::touch_each_felt("0x00000000000123450000000000067890000000000000abcd00000000000000ef")] -#[case::unique_felt("0x111111111111111155555555555555559999999999999999cccccccccccccccc")] -#[case::digits_on_repeat("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")] -fn word_macro(#[case] input: &str) { - let uut = word!(input); - - // Right pad to 64 hex digits (66 including prefix). This is required by the - // Word::try_from(String) implementation. - let padded_input = format!("{input:<66}").replace(" ", "0"); - let expected = crate::Word::try_from(padded_input.as_str()).unwrap(); - - assert_eq!(uut, expected); -} - -#[rstest::rstest] -#[case::first_nibble("0x1000000000000000000000000000000000000000000000000000000000000000", crate::Word::new([Felt::new(16), Felt::new(0), Felt::new(0), Felt::new(0)]))] -#[case::second_nibble("0x0100000000000000000000000000000000000000000000000000000000000000", crate::Word::new([Felt::new(1), Felt::new(0), Felt::new(0), Felt::new(0)]))] -#[case::all_first_nibbles("0x1000000000000000100000000000000010000000000000001000000000000000", crate::Word::new([Felt::new(16), Felt::new(16), Felt::new(16), Felt::new(16)]))] -#[case::all_first_nibbles_asc("0x1000000000000000200000000000000030000000000000004000000000000000", crate::Word::new([Felt::new(16), Felt::new(32), Felt::new(48), Felt::new(64)]))] -fn word_macro_endianness(#[case] input: &str, #[case] expected: crate::Word) { - let uut = word!(input); - assert_eq!(uut, expected); -} - -#[test] -fn word_ord_respects_partialeq() { - use core::cmp::Ordering; - - // Test that Word::cmp() respects the PartialEq invariant: - // if a == b, then a.cmp(b) must equal Ordering::Equal - - let test_cases = vec![ - Word::new([Felt::new(2), Felt::new(0), Felt::new(0), Felt::new(0)]), - Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(0)]), - Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]), - Word::new([Felt::new(100), Felt::new(200), Felt::new(300), Felt::new(400)]), - ]; - - for word in test_cases { - let word_copy = word; - - assert_eq!(word, word_copy, "Word should be equal to itself"); - assert_eq!( - word.cmp(&word_copy), - Ordering::Equal, - "Word::cmp() should return Ordering::Equal for equal words: {:?}", - word - ); - } -} - -#[test] -fn word_ord_btreemap_usage() { - use alloc::collections::BTreeMap; - - // Test that Word works correctly as a BTreeMap key - // This will fail if Ord and PartialEq are inconsistent - - let mut map = BTreeMap::new(); - let key1 = Word::new([Felt::new(2), Felt::new(0), Felt::new(0), Felt::new(0)]); - let key2 = Word::new([Felt::new(2), Felt::new(0), Felt::new(0), Felt::new(0)]); - - map.insert(key1, "value1"); - - // key2 should be equal to key1 - assert_eq!(key1, key2); - - // So map should contain key2 - assert!(map.contains_key(&key2), "BTreeMap should find key2 since it's equal to key1"); - - // And getting by key2 should return the same value - assert_eq!(map.get(&key2), Some(&"value1")); - - // Inserting with key2 should update the existing entry - map.insert(key2, "value2"); - assert_eq!(map.len(), 1, "Map should still have only one entry"); - assert_eq!(map.get(&key1), Some(&"value2")); -} - -#[test] -fn word_ord_consistency_with_partialeq() { - use core::cmp::Ordering; - - // Comprehensive test that Ord is consistent with PartialEq - // This is required by Rust's trait contract: if a == b, then a.cmp(b) == Ordering::Equal - - let test_pairs = vec![ - // Same values - ( - Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]), - Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]), - Ordering::Equal, - ), - // Different first element - ( - Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]), - Word::new([Felt::new(2), Felt::new(2), Felt::new(3), Felt::new(4)]), - Ordering::Less, - ), - // Different last element - ( - Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]), - Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(3)]), - Ordering::Greater, - ), - ]; - - for (w1, w2, expected_ordering) in test_pairs { - let actual_ordering = w1.cmp(&w2); - assert_eq!( - actual_ordering, expected_ordering, - "Word::cmp mismatch: {:?}.cmp({:?}) returned {:?}, expected {:?}", - w1, w2, actual_ordering, expected_ordering - ); - - // Verify consistency with PartialEq - match expected_ordering { - Ordering::Equal => { - assert_eq!(w1, w2, "Words should be equal when cmp returns Equal"); - }, - Ordering::Less => { - assert_ne!(w1, w2, "Words should not be equal when cmp returns Less"); - }, - Ordering::Greater => { - assert_ne!(w1, w2, "Words should not be equal when cmp returns Greater"); - }, - } - } -} diff --git a/miden-field/Cargo.toml b/miden-field/Cargo.toml new file mode 100644 index 0000000000..31f9698f18 --- /dev/null +++ b/miden-field/Cargo.toml @@ -0,0 +1,44 @@ +[package] +authors.workspace = true +categories.workspace = true +description = "A unified field element type for on-chain and off-chain Miden Rust code" +documentation = "https://docs.rs/miden-field" +edition.workspace = true +keywords.workspace = true +license.workspace = true +name = "miden-field" +readme = "README.md" +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[lib] +crate-type = ["rlib"] + +# dependendies for both off-chain and on-chain targets +[dependencies] +thiserror = { default-features = false, version = "2.0" } + +# dependendies for the off-chain target only +[target.'cfg(not(all(target_family = "wasm", miden)))'.dependencies] +miden-serde-utils = { workspace = true } +num-bigint = { default-features = false, version = "0.4" } +p3-challenger = { default-features = false, version = "0.4.2" } +p3-field = { default-features = false, version = "0.4.2" } +p3-goldilocks = { default-features = false, version = "0.4.2" } +paste = { version = "1.0.15" } +proptest = { default-features = false, features = ["alloc", "std"], optional = true, version = "1.7" } +rand = { default-features = false, features = ["small_rng"], version = "0.9.0" } +serde = { default-features = false, features = ["derive"], version = "1.0" } + +[features] +default = [] +testing = ["dep:proptest"] + +[dev-dependencies] +proptest = { default-features = false, features = ["alloc", "std"], version = "1.7" } +rand = { default-features = false, features = ["small_rng"], version = "0.9" } +rstest = { version = "0.26" } + +[lints] +workspace = true diff --git a/miden-field/README.md b/miden-field/README.md new file mode 100644 index 0000000000..bbdbdb976d --- /dev/null +++ b/miden-field/README.md @@ -0,0 +1,39 @@ +# `miden-field` + +A unified field element type for Miden Rust code that needs to run in two very different +environments: + +- **Off-chain** (native, or regular Wasm): `Felt` is a thin wrapper around Plonky3’s + `Goldilocks` field element. +- **On-chain** (Wasm compiled for the Miden VM): `Felt` is represented using Miden compiler + intrinsics. + +## Motivation + +In the Miden on-chain execution environment, field elements are currently represented by the +compiler using a *Wasm primitive type* (`f32`) that the compiler “reinterprets” as a felt. +That works for on-chain code generation, but it means the on-chain `Felt` is not the same type +as the off-chain `Felt` used throughout the Rust ecosystem. + +The result is that any code meant to be shared between on-chain and off-chain ends up either: + +- duplicated, or +- littered with `#[cfg(...)]` gates and wrapper types to bridge the two representations. + +`miden-field` exists to provide a single `miden_field::Felt` API surface that compiles in both +contexts without forcing downstream crates to pick a side. + +## How it works + +`miden-field` uses conditional compilation to select the backing implementation: + +- `cfg(all(target_family = "wasm", miden))` (on-chain): `Felt` is a `#[repr(transparent)]` + record with an `inner: f32` field (matching the WIT shape expected by bindings). Arithmetic + and conversions are implemented by calling Miden compiler intrinsics (e.g. + `intrinsics::felt::add`), and `f32` is never treated as a floating-point number. +- otherwise (off-chain): `Felt` is `#[repr(transparent)]` over `p3_goldilocks::Goldilocks` and + implements the usual field traits. The modulus is the Goldilocks prime `2^64 - 2^32 + 1`. + +The rest of the crate (e.g. `Word`) builds on top of `Felt` and therefore works in both +environments as well. + diff --git a/miden-field/build.rs b/miden-field/build.rs new file mode 100644 index 0000000000..51a4a0ad88 --- /dev/null +++ b/miden-field/build.rs @@ -0,0 +1,21 @@ +use std::env; + +fn main() { + println!("cargo::rerun-if-env-changed=MIDENC_TARGET_IS_MIDEN_VM"); + println!("cargo::rustc-check-cfg=cfg(miden)"); + + // `cargo-miden` compiles Rust to Wasm which will then be compiled to Miden VM code by `midenc`. + // When targeting a "real" Wasm runtime (e.g. `wasm32-unknown-unknown` for a web SDK), we want a + // regular felt representation instead. + // + // Treat this as a boolean flag to avoid enabling the `miden` cfg when the variable is set but + // empty (e.g. `MIDENC_TARGET_IS_MIDEN_VM=`). + let target_is_miden_vm = env::var("MIDENC_TARGET_IS_MIDEN_VM").is_ok_and(|value| { + let value = value.trim(); + value == "1" || value.eq_ignore_ascii_case("true") + }); + + if target_is_miden_vm { + println!("cargo::rustc-cfg=miden"); + } +} diff --git a/miden-field/src/lib.rs b/miden-field/src/lib.rs new file mode 100644 index 0000000000..1a59d81561 --- /dev/null +++ b/miden-field/src/lib.rs @@ -0,0 +1,39 @@ +//! A unified `Felt` for Miden Rust code. +//! +//! This crate provides a single `Felt` type that can be used in both: +//! - On-chain (Wasm + `miden`): `Felt` is backed by a Miden VM felt via compiler intrinsics. +//! - Off-chain (native / non-Miden Wasm): `Felt` is backed by Plonky3's Goldilocks field element. + +#![no_std] +#![deny(warnings)] + +extern crate alloc; + +#[cfg(all(target_family = "wasm", miden))] +mod wasm_miden; +#[cfg(all(target_family = "wasm", miden))] +pub use wasm_miden::Felt; + +#[cfg(not(all(target_family = "wasm", miden)))] +mod native; +#[cfg(not(all(target_family = "wasm", miden)))] +pub use native::Felt; + +pub mod utils; + +pub mod word; + +// RE-EXPORTS +// ================================================================================================ +#[cfg(not(all(target_family = "wasm", miden)))] +pub use p3_field::{ + BasedVectorSpace, ExtensionField, Field, InjectiveMonomial, Packable, PermutationMonomial, + PrimeCharacteristicRing, PrimeField, PrimeField64, RawDataSerializable, TwoAdicField, + batch_multiplicative_inverse, + extension::{ + BinomialExtensionField, BinomiallyExtendable, BinomiallyExtendableAlgebra, + HasTwoAdicBinomialExtension, + }, + integers::QuotientMap, +}; +pub use word::{LexicographicWord, Word, WordError}; diff --git a/miden-field/src/native/mod.rs b/miden-field/src/native/mod.rs new file mode 100644 index 0000000000..256aa08ccd --- /dev/null +++ b/miden-field/src/native/mod.rs @@ -0,0 +1,503 @@ +//! Off-chain implementation of [`crate::Felt`]. + +use alloc::format; +use core::{ + array, fmt, + hash::{Hash, Hasher}, + iter::{Product, Sum}, + ops::{Add, AddAssign, Deref, DerefMut, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}, +}; + +use num_bigint::BigUint; +use p3_challenger::UniformSamplingField; +use p3_field::{ + Field, InjectiveMonomial, Packable, PermutationMonomial, PrimeCharacteristicRing, PrimeField, + PrimeField64, RawDataSerializable, TwoAdicField, + extension::{BinomiallyExtendable, BinomiallyExtendableAlgebra, HasTwoAdicBinomialExtension}, + impl_raw_serializable_primefield64, + integers::QuotientMap, + quotient_map_large_iint, quotient_map_large_uint, quotient_map_small_int, +}; +use p3_goldilocks::Goldilocks; +use rand::{ + Rng, + distr::{Distribution, StandardUniform}, +}; + +/// A `Felt` backed by Plonky3's Goldilocks field element. +#[derive(Copy, Clone, Default, serde::Serialize, serde::Deserialize)] +#[repr(transparent)] +pub struct Felt(Goldilocks); + +impl Felt { + /// Creates a new field element from any `u64`. + /// + /// Any `u64` value is accepted. No reduction is performed since Goldilocks uses a + /// non-canonical internal representation. + #[inline] + pub const fn new(value: u64) -> Self { + Self(Goldilocks::new(value)) + } +} + +impl miden_serde_utils::Serializable for Felt { + fn write_into(&self, target: &mut W) { + target.write_u64(self.as_canonical_u64()); + } + + fn get_size_hint(&self) -> usize { + core::mem::size_of::() + } +} + +impl miden_serde_utils::Deserializable for Felt { + fn read_from( + source: &mut R, + ) -> Result { + let value = source.read_u64()?; + Self::from_canonical_checked(value).ok_or_else(|| { + miden_serde_utils::DeserializationError::InvalidValue(format!( + "value {value} is not a valid felt" + )) + }) + } +} + +impl PrimeCharacteristicRing for Felt { + type PrimeSubfield = Goldilocks; + + const ZERO: Self = Self(Goldilocks::ZERO); + const ONE: Self = Self(Goldilocks::ONE); + const TWO: Self = Self(Goldilocks::TWO); + const NEG_ONE: Self = Self(Goldilocks::NEG_ONE); + + #[inline] + fn from_prime_subfield(f: Self::PrimeSubfield) -> Self { + Self(f) + } + + #[inline] + fn from_bool(value: bool) -> Self { + Self::new(value.into()) + } + + #[inline] + fn halve(&self) -> Self { + Self(self.0.halve()) + } + + #[inline] + fn mul_2exp_u64(&self, exp: u64) -> Self { + Self(self.0.mul_2exp_u64(exp)) + } + + #[inline] + fn div_2exp_u64(&self, exp: u64) -> Self { + Self(self.0.div_2exp_u64(exp)) + } + + #[inline] + fn exp_u64(&self, power: u64) -> Self { + self.0.exp_u64(power).into() + } +} + +quotient_map_small_int!(Felt, u64, [u8, u16, u32]); +quotient_map_small_int!(Felt, i64, [i8, i16, i32]); + +quotient_map_large_uint!( + Felt, + u64, + Felt::ORDER_U64, + "`[0, 2^64 - 2^32]`", + "`[0, 2^64 - 1]`", + [u128] +); +quotient_map_large_iint!( + Felt, + i64, + "`[-(2^63 - 2^31), 2^63 - 2^31]`", + "`[1 + 2^32 - 2^64, 2^64 - 1]`", + [(i128, u128)] +); + +impl QuotientMap for Felt { + #[inline] + fn from_int(int: u64) -> Self { + Goldilocks::from_int(int).into() + } + + #[inline] + fn from_canonical_checked(int: u64) -> Option { + Goldilocks::from_canonical_checked(int).map(From::from) + } + + #[inline(always)] + unsafe fn from_canonical_unchecked(int: u64) -> Self { + Goldilocks::new(int).into() + } +} + +impl QuotientMap for Felt { + #[inline] + fn from_int(int: i64) -> Self { + Goldilocks::from_int(int).into() + } + + #[inline] + fn from_canonical_checked(int: i64) -> Option { + Goldilocks::from_canonical_checked(int).map(From::from) + } + + #[inline(always)] + unsafe fn from_canonical_unchecked(int: i64) -> Self { + unsafe { Goldilocks::from_canonical_unchecked(int).into() } + } +} + +impl PrimeField for Felt { + #[inline] + fn as_canonical_biguint(&self) -> BigUint { + ::as_canonical_biguint(&self.0) + } +} + +impl PrimeField64 for Felt { + const ORDER_U64: u64 = ::ORDER_U64; + + #[inline] + fn as_canonical_u64(&self) -> u64 { + self.0.as_canonical_u64() + } +} + +impl TwoAdicField for Felt { + const TWO_ADICITY: usize = ::TWO_ADICITY; + + #[inline] + fn two_adic_generator(bits: usize) -> Self { + Self(::two_adic_generator(bits)) + } +} + +// EXTENSION FIELDS +// ================================================================================================ + +impl BinomiallyExtendableAlgebra for Felt {} + +impl BinomiallyExtendable<2> for Felt { + const W: Self = Self(>::W); + + const DTH_ROOT: Self = Self(>::DTH_ROOT); + + const EXT_GENERATOR: [Self; 2] = [ + Self(>::EXT_GENERATOR[0]), + Self(>::EXT_GENERATOR[1]), + ]; +} + +impl HasTwoAdicBinomialExtension<2> for Felt { + const EXT_TWO_ADICITY: usize = >::EXT_TWO_ADICITY; + + #[inline] + fn ext_two_adic_generator(bits: usize) -> [Self; 2] { + let [a, b] = >::ext_two_adic_generator(bits); + [Self(a), Self(b)] + } +} + +impl BinomiallyExtendableAlgebra for Felt {} + +impl BinomiallyExtendable<5> for Felt { + const W: Self = Self(>::W); + + const DTH_ROOT: Self = Self(>::DTH_ROOT); + + const EXT_GENERATOR: [Self; 5] = [ + Self(>::EXT_GENERATOR[0]), + Self(>::EXT_GENERATOR[1]), + Self(>::EXT_GENERATOR[2]), + Self(>::EXT_GENERATOR[3]), + Self(>::EXT_GENERATOR[4]), + ]; +} + +impl HasTwoAdicBinomialExtension<5> for Felt { + const EXT_TWO_ADICITY: usize = >::EXT_TWO_ADICITY; + + #[inline] + fn ext_two_adic_generator(bits: usize) -> [Self; 5] { + let ext_generator = + >::ext_two_adic_generator(bits); + [ + Self(ext_generator[0]), + Self(ext_generator[1]), + Self(ext_generator[2]), + Self(ext_generator[3]), + Self(ext_generator[4]), + ] + } +} + +impl RawDataSerializable for Felt { + impl_raw_serializable_primefield64!(); +} + +impl Packable for Felt {} + +impl Field for Felt { + type Packing = Self; + + const GENERATOR: Self = Self(Goldilocks::GENERATOR); + + #[inline] + fn is_zero(&self) -> bool { + self.0.is_zero() + } + + #[inline] + fn try_inverse(&self) -> Option { + self.0.try_inverse().map(Self) + } + + #[inline] + fn order() -> BigUint { + ::order() + } +} + +impl Distribution for StandardUniform { + #[inline] + fn sample(&self, rng: &mut R) -> Felt { + let inner = >::sample(self, rng); + Felt(inner) + } +} + +impl UniformSamplingField for Felt { + const MAX_SINGLE_SAMPLE_BITS: usize = + ::MAX_SINGLE_SAMPLE_BITS; + const SAMPLING_BITS_M: [u64; 64] = ::SAMPLING_BITS_M; +} + +impl InjectiveMonomial<7> for Felt {} + +impl PermutationMonomial<7> for Felt { + #[inline] + fn injective_exp_root_n(&self) -> Self { + Self(self.0.injective_exp_root_n()) + } +} + +impl Deref for Felt { + type Target = Goldilocks; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Felt { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From for Felt { + #[inline] + fn from(value: Goldilocks) -> Self { + Self(value) + } +} + +impl From for Goldilocks { + #[inline] + fn from(value: Felt) -> Self { + value.0 + } +} + +impl Add for Felt { + type Output = Self; + + #[inline] + fn add(self, other: Self) -> Self { + Self(self.0 + other.0) + } +} + +impl AddAssign for Felt { + #[inline] + fn add_assign(&mut self, other: Self) { + *self = *self + other; + } +} + +impl Sub for Felt { + type Output = Self; + + #[inline] + fn sub(self, other: Self) -> Self { + Self(self.0 - other.0) + } +} + +impl SubAssign for Felt { + #[inline] + fn sub_assign(&mut self, other: Self) { + *self = *self - other; + } +} + +impl Mul for Felt { + type Output = Self; + + #[inline] + fn mul(self, other: Self) -> Self { + Self(self.0 * other.0) + } +} + +impl MulAssign for Felt { + #[inline] + fn mul_assign(&mut self, other: Self) { + *self = *self * other; + } +} + +impl Div for Felt { + type Output = Self; + + #[inline] + fn div(self, other: Self) -> Self { + Self(self.0 / other.0) + } +} + +impl DivAssign for Felt { + #[inline] + fn div_assign(&mut self, other: Self) { + *self = *self / other; + } +} + +impl Neg for Felt { + type Output = Self; + + #[inline] + fn neg(self) -> Self { + Self(-self.0) + } +} + +impl PartialEq for Felt { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl PartialEq for Felt { + #[inline] + fn eq(&self, other: &Goldilocks) -> bool { + self.0 == *other + } +} + +impl Eq for Felt {} + +impl PartialOrd for Felt { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Felt { + #[inline] + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.0.cmp(&other.0) + } +} + +impl fmt::Display for Felt { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl fmt::Debug for Felt { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } +} + +impl Hash for Felt { + #[inline] + fn hash(&self, state: &mut H) { + state.write_u64(self.as_canonical_u64()); + } +} + +impl Sum for Felt { + #[inline] + fn sum>(iter: I) -> Self { + Self(iter.map(|x| x.0).sum()) + } +} + +impl<'a> Sum<&'a Felt> for Felt { + #[inline] + fn sum>(iter: I) -> Self { + Self(iter.map(|x| x.0).sum()) + } +} + +impl Product for Felt { + #[inline] + fn product>(iter: I) -> Self { + Self(iter.map(|x| x.0).product()) + } +} + +impl<'a> Product<&'a Felt> for Felt { + #[inline] + fn product>(iter: I) -> Self { + Self(iter.map(|x| x.0).product()) + } +} + +// ARBITRARY (proptest) +// ================================================================================================ + +#[cfg(all(any(test, feature = "testing"), not(all(target_family = "wasm", miden))))] +mod arbitrary { + use p3_field::PrimeField64; + use proptest::prelude::*; + + use super::Felt; + + impl Arbitrary for Felt { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + let canonical = (0u64..Felt::ORDER_U64).prop_map(Felt::new).boxed(); + // Goldilocks uses representation where values above the field order are valid and + // represent wrapped field elements. Generate such values 1/5 of the time to exercise + // this behavior. + let non_canonical = (Felt::ORDER_U64..=u64::MAX).prop_map(Felt::new).boxed(); + prop_oneof![4 => canonical, 1 => non_canonical].no_shrink().boxed() + } + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests; diff --git a/miden-field/src/native/tests.rs b/miden-field/src/native/tests.rs new file mode 100644 index 0000000000..3c822bde4b --- /dev/null +++ b/miden-field/src/native/tests.rs @@ -0,0 +1,295 @@ +use alloc::{format, vec::Vec}; +use core::hash::{Hash, Hasher}; + +use p3_challenger::UniformSamplingField; +use p3_field::{ + Field, InjectiveMonomial, PermutationMonomial, PrimeCharacteristicRing, PrimeField, + PrimeField64, TwoAdicField, + extension::{BinomiallyExtendable, HasTwoAdicBinomialExtension}, + integers::QuotientMap, +}; +use proptest::prelude::*; +use rand::{SeedableRng, distr::Distribution, rngs::SmallRng}; + +use super::{Felt, Goldilocks}; + +/// A minimal hasher used to validate that `Felt` hashes identically to `Goldilocks`. +#[derive(Default)] +struct U64Hasher { + state: u64, +} + +impl Hasher for U64Hasher { + #[inline] + fn finish(&self) -> u64 { + self.state + } + + #[inline] + fn write(&mut self, bytes: &[u8]) { + // `Felt` and `Goldilocks` only call `write_u64` in their `Hash` impls. If this is + // called, something about hashing has changed and this test helper should be updated. + let _ = bytes; + panic!("unexpected Hasher::write call"); + } + + #[inline] + fn write_u64(&mut self, i: u64) { + self.state = i; + } +} + +proptest! { + /// `Felt::new` matches `Goldilocks::new` for the same input. + #[test] + fn felt_new_matches_goldilocks_new(x in any::()) { + prop_assert_eq!(Felt::new(x), Goldilocks::new(x)); + } + + /// Core arithmetic operations match `Goldilocks`. + #[test] + fn felt_arithmetic_matches_goldilocks(a in any::(), b in any::()) { + let fa = Felt::new(a); + let fb = Felt::new(b); + let ga = Goldilocks::new(a); + let gb = Goldilocks::new(b); + + prop_assert_eq!(fa + fb, ga + gb); + prop_assert_eq!(fa - fb, ga - gb); + prop_assert_eq!(fa * fb, ga * gb); + prop_assert_eq!(-fa, -ga); + + let mut fa2 = fa; + fa2 += fb; + prop_assert_eq!(fa2, ga + gb); + + let mut fa2 = fa; + fa2 -= fb; + prop_assert_eq!(fa2, ga - gb); + + let mut fa2 = fa; + fa2 *= fb; + prop_assert_eq!(fa2, ga * gb); + + if !gb.is_zero() { + prop_assert_eq!(fa / fb, ga / gb); + let mut fa2 = fa; + fa2 /= fb; + prop_assert_eq!(fa2, ga / gb); + } + } + + /// `Field` and `PrimeCharacteristicRing` operations match `Goldilocks`. + #[test] + fn felt_field_methods_match_goldilocks(a in any::(), exp in any::(), shift in any::()) { + let fa = Felt::new(a); + let ga = Goldilocks::new(a); + + prop_assert_eq!( + Felt::from_bool((a & 1) == 1), + Goldilocks::from_bool((a & 1) == 1), + ); + prop_assert_eq!(fa.is_zero(), ga.is_zero()); + prop_assert_eq!( + fa.try_inverse(), + ga.try_inverse().map(Felt::from), + ); + + prop_assert_eq!(fa.halve(), ga.halve()); + prop_assert_eq!(fa.mul_2exp_u64(shift as u64), ga.mul_2exp_u64(shift as u64)); + prop_assert_eq!(fa.div_2exp_u64(shift as u64), ga.div_2exp_u64(shift as u64)); + prop_assert_eq!(fa.exp_u64(exp), ga.exp_u64(exp)); + } + + /// Formatting, ordering, and hashing match `Goldilocks`. + #[test] + fn felt_misc_traits_match_goldilocks(a in any::(), b in any::()) { + let fa = Felt::new(a); + let fb = Felt::new(b); + let ga = Goldilocks::new(a); + let gb = Goldilocks::new(b); + + prop_assert_eq!(fa.cmp(&fb), ga.cmp(&gb)); + prop_assert_eq!(format!("{fa}"), format!("{ga}")); + prop_assert_eq!(format!("{fa:?}"), format!("{ga:?}")); + + let mut h1 = U64Hasher::default(); + fa.hash(&mut h1); + let mut h2 = U64Hasher::default(); + ga.hash(&mut h2); + prop_assert_eq!(h1.finish(), h2.finish()); + } + + /// Integer conversion and canonical checks match `Goldilocks`. + #[test] + fn felt_quotient_map_matches_goldilocks_u64(x in any::()) { + let f_checked = >::from_canonical_checked(x); + let g_checked = >::from_canonical_checked(x).map(Felt::from); + prop_assert_eq!(f_checked, g_checked); + + prop_assert_eq!(>::from_int(x), >::from_int(x)); + + if x < Felt::ORDER_U64 { + let f_unchecked = unsafe { >::from_canonical_unchecked(x) }; + let g_unchecked = unsafe { >::from_canonical_unchecked(x) }; + prop_assert_eq!(f_unchecked, g_unchecked); + } + } + + /// Signed integer conversion and canonical checks match `Goldilocks`. + #[test] + fn felt_quotient_map_matches_goldilocks_i64(x in any::()) { + let f_checked = >::from_canonical_checked(x); + let g_checked = >::from_canonical_checked(x).map(Felt::from); + prop_assert_eq!(f_checked, g_checked); + + prop_assert_eq!(>::from_int(x), >::from_int(x)); + + let min = i64::MIN + (1i64 << 31); + let max = i64::MAX - (1i64 << 31); + if (min..=max).contains(&x) { + let f_unchecked = unsafe { >::from_canonical_unchecked(x) }; + let g_unchecked = unsafe { >::from_canonical_unchecked(x) }; + prop_assert_eq!(f_unchecked, g_unchecked); + } + } + + /// Iterated operations (`Sum`/`Product`) match `Goldilocks`. + #[test] + fn felt_iterators_match_goldilocks(xs in prop::collection::vec(any::(), 0..64)) { + let felts: Vec = xs.iter().copied().map(Felt::new).collect(); + let golds: Vec = xs.iter().copied().map(Goldilocks::new).collect(); + + let fs = felts.iter().copied().sum::(); + let gs = golds.iter().copied().sum::(); + prop_assert_eq!(fs, gs); + + let fp = felts.iter().copied().product::(); + let gp = golds.iter().copied().product::(); + prop_assert_eq!(fp, gp); + + let fs_ref = felts.iter().sum::(); + let gs_ref = golds.iter().copied().sum::(); + prop_assert_eq!(fs_ref, gs_ref); + + let fp_ref = felts.iter().product::(); + let gp_ref = golds.iter().copied().product::(); + prop_assert_eq!(fp_ref, gp_ref); + } + + /// RNG sampling via `StandardUniform` matches `Goldilocks` for the same RNG seed. + #[test] + fn felt_distribution_matches_goldilocks(seed in any::()) { + let mut rng1 = SmallRng::seed_from_u64(seed); + let mut rng2 = SmallRng::seed_from_u64(seed); + + let g = >::sample(&rand::distr::StandardUniform, &mut rng1); + let f = >::sample(&rand::distr::StandardUniform, &mut rng2); + prop_assert_eq!(f, g); + } +} + +/// Validates that `Felt` exposes the same field constants as `Goldilocks`. +#[test] +fn felt_constants_match_goldilocks() { + assert_eq!(Felt::ZERO, Goldilocks::ZERO); + assert_eq!(Felt::ONE, Goldilocks::ONE); + assert_eq!(Felt::TWO, Goldilocks::TWO); + assert_eq!(Felt::NEG_ONE, Goldilocks::NEG_ONE); + assert_eq!(Felt::GENERATOR, Goldilocks::GENERATOR); + + assert_eq!(Felt::ORDER_U64, Goldilocks::ORDER_U64); + assert_eq!(Felt::TWO_ADICITY, Goldilocks::TWO_ADICITY); + + assert_eq!(Felt::MAX_SINGLE_SAMPLE_BITS, Goldilocks::MAX_SINGLE_SAMPLE_BITS); + assert_eq!(Felt::SAMPLING_BITS_M, Goldilocks::SAMPLING_BITS_M); + + assert_eq!(::order(), ::order()); + assert_eq!( + ::as_canonical_biguint(&Felt::new(u64::MAX)), + ::as_canonical_biguint(&Goldilocks::new(u64::MAX)), + ); + assert_eq!( + Felt::new(u64::MAX).as_canonical_u64(), + Goldilocks::new(u64::MAX).as_canonical_u64() + ); +} + +/// Validates extension-field and two-adic generator delegation. +#[test] +fn felt_extension_and_generators_match_goldilocks() { + for bits in 0..=Felt::TWO_ADICITY { + assert_eq!(Felt::two_adic_generator(bits), Goldilocks::two_adic_generator(bits)); + } + + assert_eq!(>::W, >::W); + assert_eq!( + >::DTH_ROOT, + >::DTH_ROOT + ); + for (f, g) in >::EXT_GENERATOR + .iter() + .copied() + .zip(>::EXT_GENERATOR.iter().copied()) + { + assert_eq!(f, g); + } + for bits in 0..=>::EXT_TWO_ADICITY { + let f = >::ext_two_adic_generator(bits); + let g = >::ext_two_adic_generator(bits); + assert_eq!(f[0], g[0]); + assert_eq!(f[1], g[1]); + } + + assert_eq!(>::W, >::W); + assert_eq!( + >::DTH_ROOT, + >::DTH_ROOT + ); + for (f, g) in >::EXT_GENERATOR + .iter() + .copied() + .zip(>::EXT_GENERATOR.iter().copied()) + { + assert_eq!(f, g); + } + for bits in 0..=>::EXT_TWO_ADICITY { + let f = >::ext_two_adic_generator(bits); + let g = >::ext_two_adic_generator(bits); + for (f_i, g_i) in f.iter().copied().zip(g.iter().copied()) { + assert_eq!(f_i, g_i); + } + } +} + +/// Ensures injective/permutation monomial operations match `Goldilocks`. +#[test] +fn felt_injective_monomial_matches_goldilocks() { + let inputs = [ + Felt::ZERO, + Felt::ONE, + Felt::new(Felt::ORDER_U64), + Felt::new(u64::MAX), + Felt::new(100), + ]; + + for f in inputs { + let g: Goldilocks = f.into(); + assert_eq!( + f.injective_exp_n().injective_exp_root_n(), + g.injective_exp_n().injective_exp_root_n(), + ); + assert_eq!( + >::injective_exp_root_n(&f), + >::injective_exp_root_n(&g), + ); + } +} + +/// Ensures `from_prime_subfield` is a transparent wrapper. +#[test] +fn felt_from_prime_subfield_is_transparent() { + let g = Goldilocks::new(u64::MAX); + let f = Felt::from_prime_subfield(g); + assert_eq!(f, g); +} diff --git a/miden-field/src/utils.rs b/miden-field/src/utils.rs new file mode 100644 index 0000000000..7c46ed9198 --- /dev/null +++ b/miden-field/src/utils.rs @@ -0,0 +1,58 @@ +use alloc::string::String; +use core::fmt::Write; + +use thiserror::Error; + +/// Renders an array of bytes as hex into a String. +pub fn bytes_to_hex_string(data: [u8; N]) -> String { + let mut s = String::with_capacity(N + 2); + + s.push_str("0x"); + for byte in data.iter() { + write!(s, "{byte:02x}").expect("formatting hex failed"); + } + + s +} + +/// Defines errors which can occur during parsing of hexadecimal strings. +#[derive(Debug, Error)] +pub enum HexParseError { + #[error("expected hex data to have length {expected}, including the 0x prefix, found {actual}")] + InvalidLength { expected: usize, actual: usize }, + #[error("hex encoded data must start with 0x prefix")] + MissingPrefix, + #[error("hex encoded data must contain only characters [0-9a-fA-F]")] + InvalidChar, + #[error("hex encoded values of a Digest must be inside the field modulus")] + OutOfRange, +} + +/// Parses a hex string into an array of bytes of known size. +pub fn hex_to_bytes(value: &str) -> Result<[u8; N], HexParseError> { + let expected: usize = (N * 2) + 2; + if value.len() != expected { + return Err(HexParseError::InvalidLength { expected, actual: value.len() }); + } + + if !value.starts_with("0x") { + return Err(HexParseError::MissingPrefix); + } + + let mut data = value.bytes().skip(2).map(|v| match v { + b'0'..=b'9' => Ok(v - b'0'), + b'a'..=b'f' => Ok(v - b'a' + 10), + b'A'..=b'F' => Ok(v - b'A' + 10), + _ => Err(HexParseError::InvalidChar), + }); + + let mut decoded = [0u8; N]; + for byte in decoded.iter_mut() { + // These `unwrap` calls are okay because the length was checked above + let high: u8 = data.next().unwrap()?; + let low: u8 = data.next().unwrap()?; + *byte = (high << 4) + low; + } + + Ok(decoded) +} diff --git a/miden-field/src/wasm_miden.rs b/miden-field/src/wasm_miden.rs new file mode 100644 index 0000000000..458bbcbb48 --- /dev/null +++ b/miden-field/src/wasm_miden.rs @@ -0,0 +1,336 @@ +//! On-chain implementation of [`crate::Felt`]. + +#[repr(transparent)] +#[derive(Copy, Clone, Debug, Default)] +/// A `Felt` represented as an on-chain felt. +pub struct Felt { + /// The backing type is `f32` which will be treated as a felt by the compiler. + /// We're basically hijacking the Wasm `f32` type and treat as felt. + pub inner: f32, + // We cannot define this type as `Felt(f32)` since there is no struct tuple support in WIT. + // For the type remapping to work the bindings are expecting the remapped type to be the + // same shape as the one generated from WIT. + // + // In WIT it's defined as + // ```wit + // record felt { + // inner: f32, + // } + //``` + // see sdk/base-macros/wit/miden.wit so we have to define it like that here. +} + +unsafe extern "C" { + #[link_name = "intrinsics::felt::from_u64_unchecked"] + pub(crate) fn extern_from_u64_unchecked(value: u64) -> Felt; + + #[link_name = "intrinsics::felt::from_u32"] + pub(crate) fn extern_from_u32(value: u32) -> Felt; + + #[link_name = "intrinsics::felt::as_u64"] + pub(crate) fn extern_as_u64(felt: Felt) -> u64; + + #[link_name = "intrinsics::felt::sub"] + pub(crate) fn extern_sub(a: Felt, b: Felt) -> Felt; + + #[link_name = "intrinsics::felt::mul"] + pub(crate) fn extern_mul(a: Felt, b: Felt) -> Felt; + + #[link_name = "intrinsics::felt::div"] + pub(crate) fn extern_div(a: Felt, b: Felt) -> Felt; + + #[link_name = "intrinsics::felt::neg"] + pub(crate) fn extern_neg(a: Felt) -> Felt; + + #[link_name = "intrinsics::felt::inv"] + pub(crate) fn extern_inv(a: Felt) -> Felt; + + #[link_name = "intrinsics::felt::pow2"] + pub(crate) fn extern_pow2(a: Felt) -> Felt; + + #[link_name = "intrinsics::felt::exp"] + pub(crate) fn extern_exp(a: Felt, b: Felt) -> Felt; + + #[link_name = "intrinsics::felt::eq"] + pub(crate) fn extern_eq(a: Felt, b: Felt) -> i32; + + #[link_name = "intrinsics::felt::gt"] + pub(crate) fn extern_gt(a: Felt, b: Felt) -> i32; + + #[link_name = "intrinsics::felt::lt"] + pub(crate) fn extern_lt(a: Felt, b: Felt) -> i32; + + #[link_name = "intrinsics::felt::ge"] + pub(crate) fn extern_ge(a: Felt, b: Felt) -> i32; + + #[link_name = "intrinsics::felt::le"] + pub(crate) fn extern_le(a: Felt, b: Felt) -> i32; + + #[link_name = "intrinsics::felt::is_odd"] + pub(crate) fn extern_is_odd(a: Felt) -> i32; + + #[link_name = "intrinsics::felt::add"] + pub(crate) fn extern_add(a: Felt, b: Felt) -> Felt; +} + +impl Felt { + /// The field modulus, `2^64 - 2^32 + 1`. + pub const ORDER_U64: u64 = 0xffff_ffff_0000_0001; + + /// Field element representing zero. + pub const ZERO: Self = Self { inner: f32::from_bits(0) }; + + /// Field element representing one. + pub const ONE: Self = Self { inner: f32::from_bits(1) }; + + /// Field element representing two. + pub const TWO: Self = Self { inner: f32::from_bits(2) }; + + /// Creates a new field element from any `u64`. + #[inline(always)] + pub fn new(value: u64) -> Self { + unsafe { extern_from_u64_unchecked(value) } + } + + #[inline(always)] + pub fn from_canonical_checked(int: u64) -> Option { + (int < Self::ORDER_U64).then(|| Self::new(int)) + } + + #[inline(always)] + pub fn from_u8(int: u8) -> Self { + int.into() + } + + #[inline(always)] + pub fn from_u16(int: u16) -> Self { + int.into() + } + + /// Creates a new field element from any `u32`. + pub fn from_u32(value: u32) -> Self { + unsafe { extern_from_u32(value) } + } + + /// Returns the representative of this felt in canonical form in the range `[0, ORDER_U64)`. + #[inline(always)] + pub fn as_canonical_u64(&self) -> u64 { + unsafe { extern_as_u64(*self) } + } + + /// Returns true if this felt is odd. + #[inline(always)] + pub fn is_odd(&self) -> bool { + unsafe { extern_is_odd(*self) != 0 } + } + + /// Returns true if this felt is zero. + #[inline(always)] + pub fn is_zero(&self) -> bool { + *self == Self::ZERO + } + + /// Returns the multiplicative inverse of this felt. + /// + /// # Panics + /// Panics if `self` is zero. + #[inline(always)] + pub fn inv(&self) -> Self { + debug_assert!(!self.is_zero(), "attempted to invert zero"); + unsafe { extern_inv(*self) } + } + + /// Returns `Some(self.inv())` if `self` is non-zero, and `None` otherwise. + #[inline(always)] + pub fn try_inverse(&self) -> Option { + (!self.is_zero()).then(|| self.inv()) + } + + /// Returns `2 * self`. + #[inline(always)] + pub fn double(&self) -> Self { + *self + *self + } + + /// Returns `self^2`. + #[inline(always)] + pub fn square(&self) -> Self { + unsafe { extern_pow2(*self) } + } + + /// Computes `self^power`. + #[inline(always)] + pub fn exp(&self, power: Self) -> Self { + unsafe { extern_exp(*self, power) } + } +} + +impl From for u64 { + fn from(felt: Felt) -> u64 { + felt.as_canonical_u64() + } +} + +impl From for Felt { + fn from(value: u32) -> Self { + unsafe { extern_from_u32(value) } + } +} + +impl From for Felt { + fn from(value: u16) -> Self { + Self::from(value as u32) + } +} + +impl From for Felt { + fn from(value: u8) -> Self { + Self::from(value as u32) + } +} + +#[cfg(target_pointer_width = "32")] +impl From for Felt { + fn from(value: usize) -> Self { + Self::from(value as u32) + } +} + +impl core::ops::Add for Felt { + type Output = Self; + + #[inline(always)] + fn add(self, other: Self) -> Self { + unsafe { extern_add(self, other) } + } +} + +impl core::ops::AddAssign for Felt { + #[inline(always)] + fn add_assign(&mut self, other: Self) { + *self = *self + other; + } +} + +impl core::ops::Sub for Felt { + type Output = Self; + + #[inline(always)] + fn sub(self, other: Self) -> Self { + unsafe { extern_sub(self, other) } + } +} + +impl core::ops::SubAssign for Felt { + #[inline(always)] + fn sub_assign(&mut self, other: Self) { + *self = *self - other; + } +} + +impl core::ops::Mul for Felt { + type Output = Self; + + #[inline(always)] + fn mul(self, other: Self) -> Self { + unsafe { extern_mul(self, other) } + } +} + +impl core::ops::MulAssign for Felt { + #[inline(always)] + fn mul_assign(&mut self, other: Self) { + *self = *self * other; + } +} + +impl core::ops::Div for Felt { + type Output = Self; + + #[inline(always)] + fn div(self, other: Self) -> Self { + unsafe { extern_div(self, other) } + } +} + +impl core::ops::DivAssign for Felt { + #[inline(always)] + fn div_assign(&mut self, other: Self) { + *self = *self / other; + } +} + +impl core::ops::Neg for Felt { + type Output = Self; + + #[inline(always)] + fn neg(self) -> Self { + unsafe { extern_neg(self) } + } +} + +impl PartialEq for Felt { + #[inline(always)] + fn eq(&self, other: &Self) -> bool { + unsafe { extern_eq(*self, *other) == 1 } + } +} + +impl Eq for Felt {} + +impl PartialOrd for Felt { + #[inline(always)] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + + #[inline(always)] + fn gt(&self, other: &Self) -> bool { + unsafe { extern_gt(*self, *other) != 0 } + } + + #[inline(always)] + fn ge(&self, other: &Self) -> bool { + unsafe { extern_ge(*self, *other) != 0 } + } + + #[inline(always)] + fn lt(&self, other: &Self) -> bool { + unsafe { extern_lt(*self, *other) != 0 } + } + + #[inline(always)] + fn le(&self, other: &Self) -> bool { + unsafe { extern_le(*self, *other) != 0 } + } +} + +impl Ord for Felt { + #[inline(always)] + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + if self.lt(other) { + core::cmp::Ordering::Less + } else if self.gt(other) { + core::cmp::Ordering::Greater + } else { + core::cmp::Ordering::Equal + } + } +} + +// Note: public `assert` helpers live in `sdk/field/src/lib.rs` to preserve their stable paths in +// emitted WASM and expected-file tests. + +impl core::fmt::Display for Felt { + #[inline] + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(&self.as_canonical_u64(), f) + } +} + +impl core::hash::Hash for Felt { + #[inline] + fn hash(&self, state: &mut H) { + core::hash::Hash::hash(&self.as_canonical_u64(), state); + } +} diff --git a/miden-crypto/src/word/lexicographic.rs b/miden-field/src/word/lexicographic.rs similarity index 93% rename from miden-crypto/src/word/lexicographic.rs rename to miden-field/src/word/lexicographic.rs index b41502dd91..f5f091500d 100644 --- a/miden-crypto/src/word/lexicographic.rs +++ b/miden-field/src/word/lexicographic.rs @@ -1,9 +1,11 @@ use core::cmp::Ordering; +#[cfg(not(all(target_family = "wasm", miden)))] use p3_field::PrimeField64; +#[cfg(not(all(target_family = "wasm", miden)))] use super::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; -use crate::{Felt, WORD_SIZE, Word}; +use crate::{Felt, Word, word::WORD_SIZE_FELTS}; // LEXICOGRAPHIC WORD // ================================================================================================ @@ -32,8 +34,8 @@ impl> LexicographicWord { } } -impl From<[Felt; WORD_SIZE]> for LexicographicWord { - fn from(value: [Felt; WORD_SIZE]) -> Self { +impl From<[Felt; WORD_SIZE_FELTS]> for LexicographicWord { + fn from(value: [Felt; WORD_SIZE_FELTS]) -> Self { Self(value.into()) } } @@ -90,6 +92,7 @@ impl + Copy> Ord for LexicographicWord { // SERIALIZATION // ================================================================================================ +#[cfg(not(all(target_family = "wasm", miden)))] impl + Copy> Serializable for LexicographicWord { fn write_into(&self, target: &mut W) { self.0.into().write_into(target); @@ -100,6 +103,7 @@ impl + Copy> Serializable for LexicographicWord { } } +#[cfg(not(all(target_family = "wasm", miden)))] impl + From> Deserializable for LexicographicWord { fn read_from(source: &mut R) -> Result { let word = Word::read_from(source)?; diff --git a/miden-crypto/src/word/mod.rs b/miden-field/src/word/mod.rs similarity index 61% rename from miden-crypto/src/word/mod.rs rename to miden-field/src/word/mod.rs index e6fc2f270b..97d7741ff5 100644 --- a/miden-crypto/src/word/mod.rs +++ b/miden-field/src/word/mod.rs @@ -1,30 +1,29 @@ //! A [Word] type used in the Miden protocol and associated utilities. use alloc::{string::String, vec::Vec}; +#[cfg(not(all(target_family = "wasm", miden)))] +use core::fmt::Display; use core::{ cmp::Ordering, - fmt::Display, hash::{Hash, Hasher}, ops::{Deref, DerefMut, Index, IndexMut, Range}, slice, }; +#[cfg(not(all(target_family = "wasm", miden)))] +use miden_serde_utils::{ + ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, +}; use thiserror::Error; -const WORD_SIZE_FELT: usize = 4; -const WORD_SIZE_BYTES: usize = 32; +pub const WORD_SIZE_FELTS: usize = 4; +pub const WORD_SIZE_BYTES: usize = 32; -use p3_field::integers::QuotientMap; +#[cfg(not(all(target_family = "wasm", miden)))] +use p3_field::{PrimeCharacteristicRing, PrimeField64, integers::QuotientMap}; -use super::{Felt, ZERO}; -use crate::{ - field::{PrimeCharacteristicRing, PrimeField64}, - rand::Randomizable, - utils::{ - ByteReader, ByteWriter, Deserializable, DeserializationError, HexParseError, Serializable, - bytes_to_hex_string, hex_to_bytes, - }, -}; +use super::Felt; +use crate::utils::bytes_to_hex_string; mod lexicographic; pub use lexicographic::LexicographicWord; @@ -36,18 +35,81 @@ mod tests; // ================================================================================================ /// A unit of data consisting of 4 field elements. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "serde", serde(into = "String", try_from = "&str"))] -pub struct Word([Felt; WORD_SIZE_FELT]); +#[derive(Default, Copy, Clone, Eq, PartialEq)] +#[cfg_attr( + not(all(target_family = "wasm", miden)), + derive(serde::Deserialize, serde::Serialize) +)] +#[cfg_attr( + not(all(target_family = "wasm", miden)), + serde(into = "String", try_from = "&str") +)] +#[repr(C)] +#[cfg_attr(all(target_family = "wasm", miden), repr(align(16)))] +pub struct Word { + /// The underlying elements of this word. + pub a: Felt, + pub b: Felt, + pub c: Felt, + pub d: Felt, + // The fields have to be public since the WIT->Rust bindings generation uses the fields + // directly. + // We cannot define this type as `Word([Felt;4])` since there is no struct tuple support + // and fixed array support is not complete in WIT. For the type remapping to work the + // bindings are expecting the remapped type to be the same shape as the one generated from + // WIT. + // + // see sdk/base-macros/wit/miden.wit in the compiler repo, so we have to define it like that + // here. +} + +// Compile-time assertions to ensure `Word` has the same layout as `[Felt; 4]`. This is relied upon +// in `as_elements_array`/`as_elements_array_mut`. +const _: () = { + assert!(core::mem::size_of::() == WORD_SIZE_FELTS * core::mem::size_of::()); + assert!(core::mem::offset_of!(Word, a) == 0); + assert!(core::mem::offset_of!(Word, b) == core::mem::size_of::()); + assert!(core::mem::offset_of!(Word, c) == 2 * core::mem::size_of::()); + assert!(core::mem::offset_of!(Word, d) == 3 * core::mem::size_of::()); +}; + +impl core::fmt::Debug for Word { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("Word").field(&self.into_elements()).finish() + } +} impl Word { /// The serialized size of the word in bytes. pub const SERIALIZED_SIZE: usize = WORD_SIZE_BYTES; /// Creates a new [`Word`] from the given field elements. - pub const fn new(value: [Felt; WORD_SIZE_FELT]) -> Self { - Self(value) + pub const fn new(value: [Felt; WORD_SIZE_FELTS]) -> Self { + let [a, b, c, d] = value; + Self { a, b, c, d } + } + + /// Returns the elements of this word as an array. + pub const fn into_elements(self) -> [Felt; WORD_SIZE_FELTS] { + [self.a, self.b, self.c, self.d] + } + + /// Returns the elements of this word as an array reference. + /// + /// # Safety + /// This assumes the four fields of [`Word`] are laid out contiguously with no padding, in + /// the same order as `[Felt; 4]`. + fn as_elements_array(&self) -> &[Felt; WORD_SIZE_FELTS] { + unsafe { &*(&self.a as *const Felt as *const [Felt; WORD_SIZE_FELTS]) } + } + + /// Returns the elements of this word as a mutable array reference. + /// + /// # Safety + /// This assumes the four fields of [`Word`] are laid out contiguously with no padding, in + /// the same order as `[Felt; 4]`. + fn as_elements_array_mut(&mut self) -> &mut [Felt; WORD_SIZE_FELTS] { + unsafe { &mut *(&mut self.a as *mut Felt as *mut [Felt; WORD_SIZE_FELTS]) } } /// Parses a hex string into a new [`Word`]. @@ -61,10 +123,11 @@ impl Word { /// This function is usually used via the `word!` macro. /// /// ``` - /// use miden_crypto::{Felt, Word, word}; + /// use miden_field::{Felt, Word, word}; /// let word = word!("0x1000000000000000200000000000000030000000000000004000000000000000"); /// assert_eq!(word, Word::new([Felt::new(16), Felt::new(32), Felt::new(48), Felt::new(64)])); /// ``` + #[cfg(not(all(target_family = "wasm", miden)))] pub const fn parse(hex: &str) -> Result { const fn parse_hex_digit(digit: u8) -> Result { match digit { @@ -128,47 +191,48 @@ impl Word { /// Returns a new [Word] consisting of four ZERO elements. pub const fn empty() -> Self { - Self([Felt::ZERO; WORD_SIZE_FELT]) + Self::new([Felt::ZERO; WORD_SIZE_FELTS]) } /// Returns true if the word consists of four ZERO elements. pub fn is_empty(&self) -> bool { - self.0[0] == Felt::ZERO - && self.0[1] == Felt::ZERO - && self.0[2] == Felt::ZERO - && self.0[3] == Felt::ZERO + let elements = self.as_elements_array(); + elements[0] == Felt::ZERO + && elements[1] == Felt::ZERO + && elements[2] == Felt::ZERO + && elements[3] == Felt::ZERO } /// Returns the word as a slice of field elements. pub fn as_elements(&self) -> &[Felt] { - self.as_ref() + self.as_elements_array() } /// Returns the word as a byte array. pub fn as_bytes(&self) -> [u8; WORD_SIZE_BYTES] { let mut result = [0; WORD_SIZE_BYTES]; - result[..8].copy_from_slice(&self.0[0].as_canonical_u64().to_le_bytes()); - result[8..16].copy_from_slice(&self.0[1].as_canonical_u64().to_le_bytes()); - result[16..24].copy_from_slice(&self.0[2].as_canonical_u64().to_le_bytes()); - result[24..].copy_from_slice(&self.0[3].as_canonical_u64().to_le_bytes()); + let elements = self.as_elements_array(); + result[..8].copy_from_slice(&elements[0].as_canonical_u64().to_le_bytes()); + result[8..16].copy_from_slice(&elements[1].as_canonical_u64().to_le_bytes()); + result[16..24].copy_from_slice(&elements[2].as_canonical_u64().to_le_bytes()); + result[24..].copy_from_slice(&elements[3].as_canonical_u64().to_le_bytes()); result } /// Returns an iterator over the elements of multiple words. - pub(crate) fn words_as_elements_iter<'a, I>(words: I) -> impl Iterator + pub fn words_as_elements_iter<'a, I>(words: I) -> impl Iterator where I: Iterator, { - words.flat_map(|d| d.0.iter()) + words.flat_map(|d| d.as_elements().iter()) } /// Returns all elements of multiple words as a slice. pub fn words_as_elements(words: &[Self]) -> &[Felt] { - let p = words.as_ptr(); - let len = words.len() * WORD_SIZE_FELT; - unsafe { slice::from_raw_parts(p as *const Felt, len) } + let len = words.len() * WORD_SIZE_FELTS; + unsafe { slice::from_raw_parts(words.as_ptr() as *const Felt, len) } } /// Returns hexadecimal representation of this word prefixed with `0x`. @@ -178,7 +242,17 @@ impl Word { /// Returns internal elements of this word as a vector. pub fn to_vec(&self) -> Vec { - self.0.to_vec() + self.as_elements().to_vec() + } + + /// Returns a copy of this word with its elements in reverse order. + pub fn reversed(&self) -> Self { + Word { + a: self.d, + b: self.c, + c: self.b, + d: self.a, + } } } @@ -189,16 +263,16 @@ impl Hash for Word { } impl Deref for Word { - type Target = [Felt; WORD_SIZE_FELT]; + type Target = [Felt; WORD_SIZE_FELTS]; fn deref(&self) -> &Self::Target { - &self.0 + self.as_elements_array() } } impl DerefMut for Word { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 + self.as_elements_array_mut() } } @@ -206,13 +280,13 @@ impl Index for Word { type Output = Felt; fn index(&self, index: usize) -> &Self::Output { - &self.0[index] + &self.as_elements_array()[index] } } impl IndexMut for Word { fn index_mut(&mut self, index: usize) -> &mut Self::Output { - &mut self.0[index] + &mut self.as_elements_array_mut()[index] } } @@ -220,13 +294,13 @@ impl Index> for Word { type Output = [Felt]; fn index(&self, index: Range) -> &Self::Output { - &self.0[index] + &self.as_elements_array()[index] } } impl IndexMut> for Word { fn index_mut(&mut self, index: Range) -> &mut Self::Output { - &mut self.0[index] + &mut self.as_elements_array_mut()[index] } } @@ -245,10 +319,10 @@ impl Ord for Word { // We must iterate over and compare each element individually. A simple bytestring // comparison would be inappropriate because the `Word`s are represented in // "lexicographical" order. - self.0 + self.as_elements_array() .iter() .map(Felt::as_canonical_u64) - .zip(other.0.iter().map(Felt::as_canonical_u64)) + .zip(other.as_elements_array().iter().map(Felt::as_canonical_u64)) .fold(Ordering::Equal, |ord, (a, b)| match ord { Ordering::Equal => a.cmp(&b), _ => ord, @@ -262,25 +336,13 @@ impl PartialOrd for Word { } } +#[cfg(not(all(target_family = "wasm", miden)))] impl Display for Word { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "{}", self.to_hex()) } } -impl Randomizable for Word { - const VALUE_SIZE: usize = WORD_SIZE_BYTES; - - fn from_random_bytes(bytes: &[u8]) -> Option { - let bytes_array: Option<[u8; 32]> = bytes.try_into().ok(); - if let Some(bytes_array) = bytes_array { - Self::try_from(bytes_array).ok() - } else { - None - } - } -} - // CONVERSIONS: FROM WORD // ================================================================================================ @@ -289,7 +351,7 @@ impl Randomizable for Word { pub enum WordError { /// Hex-encoded field elements parsed are invalid. #[error("hex encoded values of a word are invalid")] - HexParse(#[from] HexParseError), + HexParse(#[from] crate::utils::HexParseError), /// Field element conversion failed due to invalid value. #[error("failed to convert to field element: {0}")] InvalidFieldElement(String), @@ -301,7 +363,7 @@ pub enum WordError { TypeConversion(&'static str), } -impl TryFrom<&Word> for [bool; WORD_SIZE_FELT] { +impl TryFrom<&Word> for [bool; WORD_SIZE_FELTS] { type Error = WordError; fn try_from(value: &Word) -> Result { @@ -309,7 +371,7 @@ impl TryFrom<&Word> for [bool; WORD_SIZE_FELT] { } } -impl TryFrom for [bool; WORD_SIZE_FELT] { +impl TryFrom for [bool; WORD_SIZE_FELTS] { type Error = WordError; fn try_from(value: Word) -> Result { @@ -317,16 +379,17 @@ impl TryFrom for [bool; WORD_SIZE_FELT] { if v <= 1 { Some(v == 1) } else { None } } + let [a, b, c, d] = value.into_elements(); Ok([ - to_bool(value.0[0].as_canonical_u64()).ok_or(WordError::TypeConversion("bool"))?, - to_bool(value.0[1].as_canonical_u64()).ok_or(WordError::TypeConversion("bool"))?, - to_bool(value.0[2].as_canonical_u64()).ok_or(WordError::TypeConversion("bool"))?, - to_bool(value.0[3].as_canonical_u64()).ok_or(WordError::TypeConversion("bool"))?, + to_bool(a.as_canonical_u64()).ok_or(WordError::TypeConversion("bool"))?, + to_bool(b.as_canonical_u64()).ok_or(WordError::TypeConversion("bool"))?, + to_bool(c.as_canonical_u64()).ok_or(WordError::TypeConversion("bool"))?, + to_bool(d.as_canonical_u64()).ok_or(WordError::TypeConversion("bool"))?, ]) } } -impl TryFrom<&Word> for [u8; WORD_SIZE_FELT] { +impl TryFrom<&Word> for [u8; WORD_SIZE_FELTS] { type Error = WordError; fn try_from(value: &Word) -> Result { @@ -334,32 +397,21 @@ impl TryFrom<&Word> for [u8; WORD_SIZE_FELT] { } } -impl TryFrom for [u8; WORD_SIZE_FELT] { +impl TryFrom for [u8; WORD_SIZE_FELTS] { type Error = WordError; fn try_from(value: Word) -> Result { + let [a, b, c, d] = value.into_elements(); Ok([ - value.0[0] - .as_canonical_u64() - .try_into() - .map_err(|_| WordError::TypeConversion("u8"))?, - value.0[1] - .as_canonical_u64() - .try_into() - .map_err(|_| WordError::TypeConversion("u8"))?, - value.0[2] - .as_canonical_u64() - .try_into() - .map_err(|_| WordError::TypeConversion("u8"))?, - value.0[3] - .as_canonical_u64() - .try_into() - .map_err(|_| WordError::TypeConversion("u8"))?, + a.as_canonical_u64().try_into().map_err(|_| WordError::TypeConversion("u8"))?, + b.as_canonical_u64().try_into().map_err(|_| WordError::TypeConversion("u8"))?, + c.as_canonical_u64().try_into().map_err(|_| WordError::TypeConversion("u8"))?, + d.as_canonical_u64().try_into().map_err(|_| WordError::TypeConversion("u8"))?, ]) } } -impl TryFrom<&Word> for [u16; WORD_SIZE_FELT] { +impl TryFrom<&Word> for [u16; WORD_SIZE_FELTS] { type Error = WordError; fn try_from(value: &Word) -> Result { @@ -367,32 +419,21 @@ impl TryFrom<&Word> for [u16; WORD_SIZE_FELT] { } } -impl TryFrom for [u16; WORD_SIZE_FELT] { +impl TryFrom for [u16; WORD_SIZE_FELTS] { type Error = WordError; fn try_from(value: Word) -> Result { + let [a, b, c, d] = value.into_elements(); Ok([ - value.0[0] - .as_canonical_u64() - .try_into() - .map_err(|_| WordError::TypeConversion("u16"))?, - value.0[1] - .as_canonical_u64() - .try_into() - .map_err(|_| WordError::TypeConversion("u16"))?, - value.0[2] - .as_canonical_u64() - .try_into() - .map_err(|_| WordError::TypeConversion("u16"))?, - value.0[3] - .as_canonical_u64() - .try_into() - .map_err(|_| WordError::TypeConversion("u16"))?, + a.as_canonical_u64().try_into().map_err(|_| WordError::TypeConversion("u16"))?, + b.as_canonical_u64().try_into().map_err(|_| WordError::TypeConversion("u16"))?, + c.as_canonical_u64().try_into().map_err(|_| WordError::TypeConversion("u16"))?, + d.as_canonical_u64().try_into().map_err(|_| WordError::TypeConversion("u16"))?, ]) } } -impl TryFrom<&Word> for [u32; WORD_SIZE_FELT] { +impl TryFrom<&Word> for [u32; WORD_SIZE_FELTS] { type Error = WordError; fn try_from(value: &Word) -> Result { @@ -400,57 +441,41 @@ impl TryFrom<&Word> for [u32; WORD_SIZE_FELT] { } } -impl TryFrom for [u32; WORD_SIZE_FELT] { +impl TryFrom for [u32; WORD_SIZE_FELTS] { type Error = WordError; fn try_from(value: Word) -> Result { + let [a, b, c, d] = value.into_elements(); Ok([ - value.0[0] - .as_canonical_u64() - .try_into() - .map_err(|_| WordError::TypeConversion("u32"))?, - value.0[1] - .as_canonical_u64() - .try_into() - .map_err(|_| WordError::TypeConversion("u32"))?, - value.0[2] - .as_canonical_u64() - .try_into() - .map_err(|_| WordError::TypeConversion("u32"))?, - value.0[3] - .as_canonical_u64() - .try_into() - .map_err(|_| WordError::TypeConversion("u32"))?, + a.as_canonical_u64().try_into().map_err(|_| WordError::TypeConversion("u32"))?, + b.as_canonical_u64().try_into().map_err(|_| WordError::TypeConversion("u32"))?, + c.as_canonical_u64().try_into().map_err(|_| WordError::TypeConversion("u32"))?, + d.as_canonical_u64().try_into().map_err(|_| WordError::TypeConversion("u32"))?, ]) } } -impl From<&Word> for [u64; WORD_SIZE_FELT] { +impl From<&Word> for [u64; WORD_SIZE_FELTS] { fn from(value: &Word) -> Self { (*value).into() } } -impl From for [u64; WORD_SIZE_FELT] { +impl From for [u64; WORD_SIZE_FELTS] { fn from(value: Word) -> Self { - [ - value.0[0].as_canonical_u64(), - value.0[1].as_canonical_u64(), - value.0[2].as_canonical_u64(), - value.0[3].as_canonical_u64(), - ] + value.into_elements().map(|felt| felt.as_canonical_u64()) } } -impl From<&Word> for [Felt; WORD_SIZE_FELT] { +impl From<&Word> for [Felt; WORD_SIZE_FELTS] { fn from(value: &Word) -> Self { (*value).into() } } -impl From for [Felt; WORD_SIZE_FELT] { +impl From for [Felt; WORD_SIZE_FELTS] { fn from(value: Word) -> Self { - value.0 + value.into_elements() } } @@ -466,6 +491,7 @@ impl From for [u8; WORD_SIZE_BYTES] { } } +#[cfg(not(all(target_family = "wasm", miden)))] impl From<&Word> for String { /// The returned string starts with `0x`. fn from(value: &Word) -> Self { @@ -473,6 +499,7 @@ impl From<&Word> for String { } } +#[cfg(not(all(target_family = "wasm", miden)))] impl From for String { /// The returned string starts with `0x`. fn from(value: Word) -> Self { @@ -483,27 +510,27 @@ impl From for String { // CONVERSIONS: TO WORD // ================================================================================================ -impl From<&[bool; WORD_SIZE_FELT]> for Word { - fn from(value: &[bool; WORD_SIZE_FELT]) -> Self { +impl From<&[bool; WORD_SIZE_FELTS]> for Word { + fn from(value: &[bool; WORD_SIZE_FELTS]) -> Self { (*value).into() } } -impl From<[bool; WORD_SIZE_FELT]> for Word { - fn from(value: [bool; WORD_SIZE_FELT]) -> Self { +impl From<[bool; WORD_SIZE_FELTS]> for Word { + fn from(value: [bool; WORD_SIZE_FELTS]) -> Self { [value[0] as u32, value[1] as u32, value[2] as u32, value[3] as u32].into() } } -impl From<&[u8; WORD_SIZE_FELT]> for Word { - fn from(value: &[u8; WORD_SIZE_FELT]) -> Self { +impl From<&[u8; WORD_SIZE_FELTS]> for Word { + fn from(value: &[u8; WORD_SIZE_FELTS]) -> Self { (*value).into() } } -impl From<[u8; WORD_SIZE_FELT]> for Word { - fn from(value: [u8; WORD_SIZE_FELT]) -> Self { - Self([ +impl From<[u8; WORD_SIZE_FELTS]> for Word { + fn from(value: [u8; WORD_SIZE_FELTS]) -> Self { + Self::new([ Felt::from_u8(value[0]), Felt::from_u8(value[1]), Felt::from_u8(value[2]), @@ -512,15 +539,15 @@ impl From<[u8; WORD_SIZE_FELT]> for Word { } } -impl From<&[u16; WORD_SIZE_FELT]> for Word { - fn from(value: &[u16; WORD_SIZE_FELT]) -> Self { +impl From<&[u16; WORD_SIZE_FELTS]> for Word { + fn from(value: &[u16; WORD_SIZE_FELTS]) -> Self { (*value).into() } } -impl From<[u16; WORD_SIZE_FELT]> for Word { - fn from(value: [u16; WORD_SIZE_FELT]) -> Self { - Self([ +impl From<[u16; WORD_SIZE_FELTS]> for Word { + fn from(value: [u16; WORD_SIZE_FELTS]) -> Self { + Self::new([ Felt::from_u16(value[0]), Felt::from_u16(value[1]), Felt::from_u16(value[2]), @@ -529,15 +556,15 @@ impl From<[u16; WORD_SIZE_FELT]> for Word { } } -impl From<&[u32; WORD_SIZE_FELT]> for Word { - fn from(value: &[u32; WORD_SIZE_FELT]) -> Self { +impl From<&[u32; WORD_SIZE_FELTS]> for Word { + fn from(value: &[u32; WORD_SIZE_FELTS]) -> Self { (*value).into() } } -impl From<[u32; WORD_SIZE_FELT]> for Word { - fn from(value: [u32; WORD_SIZE_FELT]) -> Self { - Self([ +impl From<[u32; WORD_SIZE_FELTS]> for Word { + fn from(value: [u32; WORD_SIZE_FELTS]) -> Self { + Self::new([ Felt::from_u32(value[0]), Felt::from_u32(value[1]), Felt::from_u32(value[2]), @@ -546,20 +573,20 @@ impl From<[u32; WORD_SIZE_FELT]> for Word { } } -impl TryFrom<&[u64; WORD_SIZE_FELT]> for Word { +impl TryFrom<&[u64; WORD_SIZE_FELTS]> for Word { type Error = WordError; - fn try_from(value: &[u64; WORD_SIZE_FELT]) -> Result { + fn try_from(value: &[u64; WORD_SIZE_FELTS]) -> Result { (*value).try_into() } } -impl TryFrom<[u64; WORD_SIZE_FELT]> for Word { +impl TryFrom<[u64; WORD_SIZE_FELTS]> for Word { type Error = WordError; - fn try_from(value: [u64; WORD_SIZE_FELT]) -> Result { + fn try_from(value: [u64; WORD_SIZE_FELTS]) -> Result { let err = || WordError::InvalidFieldElement("value >= field modulus".into()); - Ok(Self([ + Ok(Self::new([ Felt::from_canonical_checked(value[0]).ok_or_else(err)?, Felt::from_canonical_checked(value[1]).ok_or_else(err)?, Felt::from_canonical_checked(value[2]).ok_or_else(err)?, @@ -568,15 +595,15 @@ impl TryFrom<[u64; WORD_SIZE_FELT]> for Word { } } -impl From<&[Felt; WORD_SIZE_FELT]> for Word { - fn from(value: &[Felt; WORD_SIZE_FELT]) -> Self { - Self(*value) +impl From<&[Felt; WORD_SIZE_FELTS]> for Word { + fn from(value: &[Felt; WORD_SIZE_FELTS]) -> Self { + Self::new(*value) } } -impl From<[Felt; WORD_SIZE_FELT]> for Word { - fn from(value: [Felt; WORD_SIZE_FELT]) -> Self { - Self(value) +impl From<[Felt; WORD_SIZE_FELTS]> for Word { + fn from(value: [Felt; WORD_SIZE_FELTS]) -> Self { + Self::new(value) } } @@ -605,7 +632,7 @@ impl TryFrom<[u8; WORD_SIZE_BYTES]> for Word { let c: Felt = Felt::from_canonical_checked(c).ok_or_else(err)?; let d: Felt = Felt::from_canonical_checked(d).ok_or_else(err)?; - Ok(Word([a, b, c, d])) + Ok(Self::new([a, b, c, d])) } } @@ -624,24 +651,26 @@ impl TryFrom<&[Felt]> for Word { type Error = WordError; fn try_from(value: &[Felt]) -> Result { - let value: [Felt; WORD_SIZE_FELT] = value + let value: [Felt; WORD_SIZE_FELTS] = value .try_into() - .map_err(|_| WordError::InvalidInputLength("elements", WORD_SIZE_FELT, value.len()))?; + .map_err(|_| WordError::InvalidInputLength("elements", WORD_SIZE_FELTS, value.len()))?; Ok(value.into()) } } +#[cfg(not(all(target_family = "wasm", miden)))] impl TryFrom<&str> for Word { type Error = WordError; /// Expects the string to start with `0x`. fn try_from(value: &str) -> Result { - hex_to_bytes::(value) + crate::utils::hex_to_bytes::(value) .map_err(WordError::HexParse) .and_then(Word::try_from) } } +#[cfg(not(all(target_family = "wasm", miden)))] impl TryFrom for Word { type Error = WordError; @@ -651,6 +680,7 @@ impl TryFrom for Word { } } +#[cfg(not(all(target_family = "wasm", miden)))] impl TryFrom<&String> for Word { type Error = WordError; @@ -663,6 +693,7 @@ impl TryFrom<&String> for Word { // SERIALIZATION / DESERIALIZATION // ================================================================================================ +#[cfg(not(all(target_family = "wasm", miden)))] impl Serializable for Word { fn write_into(&self, target: &mut W) { target.write_bytes(&self.as_bytes()); @@ -673,9 +704,10 @@ impl Serializable for Word { } } +#[cfg(not(all(target_family = "wasm", miden)))] impl Deserializable for Word { fn read_from(source: &mut R) -> Result { - let mut inner: [Felt; WORD_SIZE_FELT] = [ZERO; WORD_SIZE_FELT]; + let mut inner: [Felt; WORD_SIZE_FELTS] = [Felt::ZERO; WORD_SIZE_FELTS]; for inner in inner.iter_mut() { let e = source.read_u64()?; if e >= Felt::ORDER_U64 { @@ -686,7 +718,7 @@ impl Deserializable for Word { *inner = Felt::new(e); } - Ok(Self(inner)) + Ok(Self::new(inner)) } } @@ -697,7 +729,7 @@ impl IntoIterator for Word { type IntoIter = <[Felt; 4] as IntoIterator>::IntoIter; fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() + self.into_elements().into_iter() } } @@ -707,6 +739,7 @@ impl IntoIterator for Word { /// Construct a new [Word](super::Word) from a hex value. /// /// Expects a '0x' prefixed hex string followed by up to 64 hex digits. +#[cfg(not(all(target_family = "wasm", miden)))] #[macro_export] macro_rules! word { ($hex:expr) => {{ @@ -722,7 +755,7 @@ macro_rules! word { // ARBITRARY (proptest) // ================================================================================================ -#[cfg(any(test, feature = "testing"))] +#[cfg(all(any(test, feature = "testing"), not(all(target_family = "wasm", miden))))] mod arbitrary { use proptest::prelude::*; @@ -733,17 +766,7 @@ mod arbitrary { type Strategy = BoxedStrategy; fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - prop::collection::vec(any::(), 4) - .prop_map(|vals| { - Word::new([ - Felt::new(vals[0]), - Felt::new(vals[1]), - Felt::new(vals[2]), - Felt::new(vals[3]), - ]) - }) - .no_shrink() - .boxed() + prop::array::uniform4(any::()).prop_map(Word::new).no_shrink().boxed() } } } diff --git a/miden-field/src/word/tests.rs b/miden-field/src/word/tests.rs new file mode 100644 index 0000000000..2b4e6dd796 --- /dev/null +++ b/miden-field/src/word/tests.rs @@ -0,0 +1,262 @@ +use alloc::{collections::BTreeMap, string::String, vec::Vec}; + +use miden_serde_utils::SliceReader; +use p3_field::PrimeField64; +use proptest::prelude::*; + +use super::{Deserializable, Felt, Serializable, WORD_SIZE_BYTES, WORD_SIZE_FELTS, Word}; +use crate::word; + +// TESTS +// ================================================================================================ + +/// Returns a strategy which generates a `[u64; 4]` where all values are canonical field elements. +fn any_word_elements_u64_canonical() -> BoxedStrategy<[u64; WORD_SIZE_FELTS]> { + prop::array::uniform4(0u64..Felt::ORDER_U64).no_shrink().boxed() +} + +proptest! { + #[test] + fn word_is_equal_to_itself(word in any::()) { + use core::cmp::Ordering; + + prop_assert_eq!(word, word); + prop_assert_eq!(word.cmp(&word), Ordering::Equal); + prop_assert_eq!(word.partial_cmp(&word), Some(Ordering::Equal)); + } + + #[test] + fn word_serialization_roundtrip(word in any::()) { + let mut bytes = Vec::new(); + word.write_into(&mut bytes); + prop_assert_eq!(WORD_SIZE_BYTES, bytes.len()); + prop_assert_eq!(bytes.len(), word.get_size_hint()); + + let mut reader = SliceReader::new(&bytes); + let round_trip = Word::read_from(&mut reader).unwrap(); + + prop_assert_eq!(word, round_trip); + } + + #[test] + fn word_encoding_roundtrip(word in any::()) { + let string: String = word.into(); + let round_trip: Word = string.try_into().expect("decoding failed"); + prop_assert_eq!(word, round_trip); + } + + #[test] + fn word_bool_conversion_roundtrip(v in any::<[bool; WORD_SIZE_FELTS]>()) { + let word: Word = v.into(); + prop_assert_eq!(v, <[bool; WORD_SIZE_FELTS]>::try_from(word).unwrap()); + + let word: Word = (&v).into(); + prop_assert_eq!(v, <[bool; WORD_SIZE_FELTS]>::try_from(&word).unwrap()); + } + + #[test] + fn word_u8_conversion_roundtrip(v in any::<[u8; WORD_SIZE_FELTS]>()) { + let word: Word = v.into(); + prop_assert_eq!(v, <[u8; WORD_SIZE_FELTS]>::try_from(word).unwrap()); + + let word: Word = (&v).into(); + prop_assert_eq!(v, <[u8; WORD_SIZE_FELTS]>::try_from(&word).unwrap()); + } + + #[test] + fn word_u16_conversion_roundtrip(v in any::<[u16; WORD_SIZE_FELTS]>()) { + let word: Word = v.into(); + prop_assert_eq!(v, <[u16; WORD_SIZE_FELTS]>::try_from(word).unwrap()); + + let word: Word = (&v).into(); + prop_assert_eq!(v, <[u16; WORD_SIZE_FELTS]>::try_from(&word).unwrap()); + } + + #[test] + fn word_u32_conversion_roundtrip(v in any::<[u32; WORD_SIZE_FELTS]>()) { + let word: Word = v.into(); + prop_assert_eq!(v, <[u32; WORD_SIZE_FELTS]>::try_from(word).unwrap()); + + let word: Word = (&v).into(); + prop_assert_eq!(v, <[u32; WORD_SIZE_FELTS]>::try_from(&word).unwrap()); + } + + #[test] + fn word_u64_conversion_roundtrip(v in any_word_elements_u64_canonical()) { + let word: Word = v.try_into().unwrap(); + let round_trip: [u64; WORD_SIZE_FELTS] = word.into(); + prop_assert_eq!(v, round_trip); + + let word: Word = (&v).try_into().unwrap(); + let round_trip: [u64; WORD_SIZE_FELTS] = (&word).into(); + prop_assert_eq!(v, round_trip); + } + + #[test] + fn word_felt_conversion_roundtrip(elements in prop::array::uniform4(any::())) { + let elements = elements.map(Felt::new); + + let word: Word = elements.into(); + let round_trip: [Felt; WORD_SIZE_FELTS] = word.into(); + prop_assert_eq!(elements, round_trip); + + let word: Word = (&elements).into(); + let round_trip: [Felt; WORD_SIZE_FELTS] = (&word).into(); + prop_assert_eq!(elements, round_trip); + } + + #[test] + fn word_bytes_conversion_roundtrip(word in any::()) { + let bytes: [u8; WORD_SIZE_BYTES] = word.into(); + let round_trip: Word = bytes.try_into().unwrap(); + prop_assert_eq!(word, round_trip); + + let bytes: [u8; WORD_SIZE_BYTES] = (&word).into(); + let round_trip: Word = (&bytes).try_into().unwrap(); + prop_assert_eq!(word, round_trip); + } + + #[test] + fn word_string_conversion_roundtrip(word in any::()) { + let string: String = word.into(); + let round_trip: Word = string.try_into().unwrap(); + prop_assert_eq!(word, round_trip); + + let string: String = (&word).into(); + let round_trip: Word = (&string).try_into().unwrap(); + prop_assert_eq!(word, round_trip); + } + + #[test] + fn word_reversed_roundtrip(word in any::()) { + prop_assert_eq!(word, word.reversed().reversed()); + } +} + +#[test] +fn word_elements_array_layout_roundtrip() { + let mut word = Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); + + let elements = word.as_elements_array(); + assert_eq!(elements, &[Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); + + let base = core::ptr::addr_of!(word.a); + assert_eq!(elements.as_ptr(), base); + assert_eq!(core::ptr::addr_of!(word.b), unsafe { base.add(1) }); + assert_eq!(core::ptr::addr_of!(word.c), unsafe { base.add(2) }); + assert_eq!(core::ptr::addr_of!(word.d), unsafe { base.add(3) }); + + let elements_mut = word.as_elements_array_mut(); + elements_mut[2] = Felt::new(42); + assert_eq!(word.c, Felt::new(42)); +} + +proptest! { + #[test] + fn word_index_matches_into_elements(word in any::()) { + let elements = word.into_elements(); + for idx in 0..WORD_SIZE_FELTS { + prop_assert_eq!(word[idx], elements[idx]); + } + } + + #[test] + fn word_index_mut_updates_all_elements(word in any::(), values in any::<[u64; WORD_SIZE_FELTS]>()) { + let mut word = word; + + let mut expected = word.into_elements(); + for idx in 0..WORD_SIZE_FELTS { + let value = values[idx]; + expected[idx] = Felt::new(value); + word[idx] = Felt::new(value); + } + prop_assert_eq!(word.into_elements(), expected); + } + + #[test] + fn word_index_mut_range_updates_slice(word in any::(), v0 in any::(), v1 in any::()) { + let mut word = word; + let expected = [Felt::new(v0), Felt::new(v1)]; + + word[1..3].copy_from_slice(&expected); + prop_assert_eq!(word[1], expected[0]); + prop_assert_eq!(word[2], expected[1]); + } +} + +#[rstest::rstest] +#[case::missing_prefix("1234")] +#[case::invalid_character("1234567890abcdefg")] +#[case::too_long("0xx00000000000000000000000000000000000000000000000000000000000000001")] +#[case::overflow_felt0("0x01000000ffffffff000000000000000000000000000000000000000000000000")] +#[case::overflow_felt1("0x000000000000000001000000ffffffff00000000000000000000000000000000")] +#[case::overflow_felt2("0x0000000000000000000000000000000001000000ffffffff0000000000000000")] +#[case::overflow_felt3("0x00000000000000000000000000000000000000000000000001000000ffffffff")] +#[should_panic] +fn word_macro_invalid(#[case] bad_input: &str) { + word!(bad_input); +} + +#[rstest::rstest] +#[case::each_digit("0x1234567890abcdef")] +#[case::empty("0x")] +#[case::zero("0x0")] +#[case::zero_full("0x0000000000000000000000000000000000000000000000000000000000000000")] +#[case::one_lsb("0x1")] +#[case::one_msb("0x0000000000000000000000000000000000000000000000000000000000000001")] +#[case::one_partial("0x0001")] +#[case::odd("0x123")] +#[case::even("0x1234")] +#[case::touch_each_felt("0x00000000000123450000000000067890000000000000abcd00000000000000ef")] +#[case::unique_felt("0x111111111111111155555555555555559999999999999999cccccccccccccccc")] +#[case::digits_on_repeat("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")] +fn word_macro(#[case] input: &str) { + use alloc::format; + + let uut = word!(input); + + // Right pad to 64 hex digits (66 including prefix). This is required by the + // Word::try_from(String) implementation. + let padded_input = format!("{input:<66}").replace(" ", "0"); + let expected = crate::Word::try_from(padded_input.as_str()).unwrap(); + + assert_eq!(uut, expected); +} + +#[rstest::rstest] +#[case::first_nibble("0x1000000000000000000000000000000000000000000000000000000000000000", crate::Word::new([Felt::new(16), Felt::new(0), Felt::new(0), Felt::new(0)]))] +#[case::second_nibble("0x0100000000000000000000000000000000000000000000000000000000000000", crate::Word::new([Felt::new(1), Felt::new(0), Felt::new(0), Felt::new(0)]))] +#[case::all_first_nibbles("0x1000000000000000100000000000000010000000000000001000000000000000", crate::Word::new([Felt::new(16), Felt::new(16), Felt::new(16), Felt::new(16)]))] +#[case::all_first_nibbles_asc("0x1000000000000000200000000000000030000000000000004000000000000000", crate::Word::new([Felt::new(16), Felt::new(32), Felt::new(48), Felt::new(64)]))] +fn word_macro_endianness(#[case] input: &str, #[case] expected: crate::Word) { + let uut = word!(input); + assert_eq!(uut, expected); +} + +proptest! { + #[test] + fn word_ord_is_consistent_with_partialeq(a in any::(), b in any::()) { + use core::cmp::Ordering; + + prop_assert_eq!(a == b, a.cmp(&b) == Ordering::Equal); + prop_assert_eq!(b == a, b.cmp(&a) == Ordering::Equal); + } + + #[test] + fn word_ord_supports_btreemap_key_usage(word in any::()) { + let mut map: BTreeMap = BTreeMap::new(); + map.insert(word, 1); + + // Round-trip via bytes to create an equivalent key. + let bytes: [u8; WORD_SIZE_BYTES] = word.into(); + let key2: Word = bytes.try_into().unwrap(); + prop_assert_eq!(word, key2); + + prop_assert!(map.contains_key(&key2)); + prop_assert_eq!(map.get(&key2), Some(&1)); + + map.insert(key2, 2); + prop_assert_eq!(map.len(), 1); + prop_assert_eq!(map.get(&word), Some(&2)); + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 6744e56e15..81d504e3f6 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -2,4 +2,4 @@ channel = "1.90" components = ["clippy", "rust-src", "rustfmt"] profile = "minimal" -targets = ["wasm32-unknown-unknown"] +targets = ["wasm32-unknown-unknown", "wasm32-wasip2"]