diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1d0ae53c4..5950afdfc 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 376952ed4..5fcec1a11 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 950d51c26..8ec7155b8 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 e08a2245b..e85a659d1 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 ff45aa857..3df2709d1 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="--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 2c55d4373..3649c283c 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 bfd0bd347..a3e6666b4 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 57c810af0..2d929f136 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 18017ba3f..72ae66495 100644 --- a/miden-crypto/src/lib.rs +++ b/miden-crypto/src/lib.rs @@ -15,12 +15,11 @@ pub mod ies; pub mod merkle; pub mod rand; pub mod utils; -pub mod word; +use miden_field::word; // RE-EXPORTS // ================================================================================================ -pub use p3_goldilocks::Goldilocks as Felt; -pub use word::{Word, WordError}; +pub use miden_field::{Felt, LexicographicWord, Word, WordError}; pub mod field { //! Traits and utilities for working with the Goldilocks finite field (i.e., @@ -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_FELT; /// 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 722039721..3243c0e2a 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 de2a93907..e5b772d5b 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-field/Cargo.toml b/miden-field/Cargo.toml new file mode 100644 index 000000000..e42c62fe2 --- /dev/null +++ b/miden-field/Cargo.toml @@ -0,0 +1,43 @@ +[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] +rand = { default-features = false, 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 000000000..bbdbdb976 --- /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 000000000..733da30b9 --- /dev/null +++ b/miden-field/build.rs @@ -0,0 +1,13 @@ +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. + if env::var_os("MIDENC_TARGET_IS_MIDEN_VM").is_some() { + println!("cargo::rustc-cfg=miden"); + } +} diff --git a/miden-field/src/lib.rs b/miden-field/src/lib.rs new file mode 100644 index 000000000..6fda4b4c9 --- /dev/null +++ b/miden-field/src/lib.rs @@ -0,0 +1,26 @@ +//! 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; + +pub use word::{LexicographicWord, Word, WordError}; diff --git a/miden-field/src/native.rs b/miden-field/src/native.rs new file mode 100644 index 000000000..2c93f17c9 --- /dev/null +++ b/miden-field/src/native.rs @@ -0,0 +1,465 @@ +//! 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 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()) + } +} diff --git a/miden-field/src/utils.rs b/miden-field/src/utils.rs new file mode 100644 index 000000000..7c46ed919 --- /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 000000000..458bbcbb4 --- /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 b41502dd9..6dcf7c9ba 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_FELT}; // 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_FELT]> for LexicographicWord { + fn from(value: [Felt; WORD_SIZE_FELT]) -> 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 72% rename from miden-crypto/src/word/mod.rs rename to miden-field/src/word/mod.rs index e6fc2f270..c3b03b16e 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_FELT: 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,10 +35,36 @@ 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, align(16))] +pub struct Word { + /// The underlying elements of this word. + pub a: Felt, + pub b: Felt, + pub c: Felt, + pub d: Felt, + // 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. +} + +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. @@ -47,7 +72,31 @@ impl Word { /// Creates a new [`Word`] from the given field elements. pub const fn new(value: [Felt; WORD_SIZE_FELT]) -> Self { - Self(value) + 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_FELT] { + [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_FELT] { + unsafe { &*(&self.a as *const Felt as *const [Felt; WORD_SIZE_FELT]) } + } + + /// 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_FELT] { + unsafe { &mut *(&mut self.a as *mut Felt as *mut [Felt; WORD_SIZE_FELT]) } } /// Parses a hex string into a new [`Word`]. @@ -61,10 +110,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 +178,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_FELT]) } /// 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) } + unsafe { slice::from_raw_parts(words.as_ptr() as *const Felt, len) } } /// Returns hexadecimal representation of this word prefixed with `0x`. @@ -178,7 +229,16 @@ 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() + } + + pub fn reversed(&self) -> Self { + Word { + a: self.d, + b: self.c, + c: self.b, + d: self.a, + } } } @@ -192,13 +252,13 @@ impl Deref for Word { type Target = [Felt; WORD_SIZE_FELT]; 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 +266,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 +280,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 +305,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 +322,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 +337,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), @@ -317,11 +365,12 @@ 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"))?, ]) } } @@ -338,23 +387,12 @@ impl TryFrom for [u8; WORD_SIZE_FELT] { 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"))?, ]) } } @@ -371,23 +409,12 @@ impl TryFrom for [u16; WORD_SIZE_FELT] { 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"))?, ]) } } @@ -404,23 +431,12 @@ impl TryFrom for [u32; WORD_SIZE_FELT] { 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"))?, ]) } } @@ -433,12 +449,7 @@ impl From<&Word> for [u64; WORD_SIZE_FELT] { impl From for [u64; WORD_SIZE_FELT] { 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()) } } @@ -450,7 +461,7 @@ impl From<&Word> for [Felt; WORD_SIZE_FELT] { impl From for [Felt; WORD_SIZE_FELT] { fn from(value: Word) -> Self { - value.0 + value.into_elements() } } @@ -466,6 +477,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 +485,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 { @@ -503,7 +516,7 @@ impl From<&[u8; WORD_SIZE_FELT]> for Word { impl From<[u8; WORD_SIZE_FELT]> for Word { fn from(value: [u8; WORD_SIZE_FELT]) -> Self { - Self([ + Self::new([ Felt::from_u8(value[0]), Felt::from_u8(value[1]), Felt::from_u8(value[2]), @@ -520,7 +533,7 @@ impl From<&[u16; WORD_SIZE_FELT]> for Word { impl From<[u16; WORD_SIZE_FELT]> for Word { fn from(value: [u16; WORD_SIZE_FELT]) -> Self { - Self([ + Self::new([ Felt::from_u16(value[0]), Felt::from_u16(value[1]), Felt::from_u16(value[2]), @@ -537,7 +550,7 @@ impl From<&[u32; WORD_SIZE_FELT]> for Word { impl From<[u32; WORD_SIZE_FELT]> for Word { fn from(value: [u32; WORD_SIZE_FELT]) -> Self { - Self([ + Self::new([ Felt::from_u32(value[0]), Felt::from_u32(value[1]), Felt::from_u32(value[2]), @@ -559,7 +572,7 @@ impl TryFrom<[u64; WORD_SIZE_FELT]> for Word { fn try_from(value: [u64; WORD_SIZE_FELT]) -> 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)?, @@ -570,13 +583,13 @@ 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) + Self::new(*value) } } impl From<[Felt; WORD_SIZE_FELT]> for Word { fn from(value: [Felt; WORD_SIZE_FELT]) -> Self { - Self(value) + Self::new(value) } } @@ -605,7 +618,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])) } } @@ -631,17 +644,19 @@ impl TryFrom<&[Felt]> for Word { } } +#[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 +666,7 @@ impl TryFrom for Word { } } +#[cfg(not(all(target_family = "wasm", miden)))] impl TryFrom<&String> for Word { type Error = WordError; @@ -663,6 +679,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 +690,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_FELT] = [Felt::ZERO; WORD_SIZE_FELT]; for inner in inner.iter_mut() { let e = source.read_u64()?; if e >= Felt::ORDER_U64 { @@ -686,7 +704,7 @@ impl Deserializable for Word { *inner = Felt::new(e); } - Ok(Self(inner)) + Ok(Self::new(inner)) } } @@ -697,7 +715,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() } } diff --git a/miden-crypto/src/word/tests.rs b/miden-field/src/word/tests.rs similarity index 88% rename from miden-crypto/src/word/tests.rs rename to miden-field/src/word/tests.rs index 0fe119824..1c0c0d35e 100644 --- a/miden-crypto/src/word/tests.rs +++ b/miden-field/src/word/tests.rs @@ -1,22 +1,27 @@ -#![cfg(feature = "std")] -use alloc::string::String; +use alloc::{string::String, vec}; +use miden_serde_utils::SliceReader; 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}; +use crate::word; + +/// Generates a random value of type u64 from an RNG. +pub fn rand_u64() -> u64 { + rand::random() +} // 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 e1 = Felt::new(rand_u64()); + let e2 = Felt::new(rand_u64()); + let e3 = Felt::new(rand_u64()); + let e4 = Felt::new(rand_u64()); - let d1 = Word([e1, e2, e3, e4]); + let d1 = Word::new([e1, e2, e3, e4]); let mut bytes = vec![]; d1.write_into(&mut bytes); @@ -31,11 +36,11 @@ fn word_serialization() { #[test] fn word_encoding() { - let word = Word([ - Felt::new(rand_value()), - Felt::new(rand_value()), - Felt::new(rand_value()), - Felt::new(rand_value()), + let word = Word::new([ + Felt::new(rand_u64()), + Felt::new(rand_u64()), + Felt::new(rand_u64()), + Felt::new(rand_u64()), ]); let string: String = word.into(); @@ -46,11 +51,11 @@ fn word_encoding() { #[test] fn test_conversions() { - let word = Word([ - Felt::new(rand_value()), - Felt::new(rand_value()), - Felt::new(rand_value()), - Felt::new(rand_value()), + let word = Word::new([ + Felt::new(rand_u64()), + Felt::new(rand_u64()), + Felt::new(rand_u64()), + Felt::new(rand_u64()), ]); // BY VALUE @@ -122,6 +127,24 @@ fn test_conversions() { assert_eq!(word, v2); } +#[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)); +} + #[test] fn test_index() { let word = Word::new([ @@ -196,6 +219,8 @@ fn word_macro_invalid(#[case] bad_input: &str) { #[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 diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 6744e56e1..81d504e3f 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"]