Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ba5aa2b
refactor: add `Felt` type unified for off-chain and on-chain code,
greenhat Jan 27, 2026
1519870
refactor: move `Word` and derived types from `miden-crypto` to `miden…
greenhat Feb 11, 2026
2340896
chore: add `make build_target_miden` task and `targe-miden` CI job in…
greenhat Feb 13, 2026
d852c32
fix: bring wasm+miden Felt API on par with native
greenhat Feb 13, 2026
06d007b
fix: build `miden-field` for wasm+miden target
greenhat Feb 13, 2026
c5ae258
fix: imports for `LexicographicWord`, remove `AsRef` impl for `Word`
greenhat Feb 16, 2026
0e06cae
refactor: change the shape of the `Word` type from a tuple to named f…
greenhat Feb 16, 2026
d0a0ae6
chore: formatting
greenhat Feb 16, 2026
2374a2b
refactor: make the inner type private in `Felt`
greenhat Feb 16, 2026
9ab51d9
chore: make `miden-field` crate a workspace dependency
greenhat Feb 16, 2026
2adaca4
chore: add README.md for the `miden-field` crate
greenhat Feb 16, 2026
d973a9b
refactor: use `format!`
greenhat Feb 16, 2026
b818ccc
fix: re-export `miden_field::word` module from `miden-crypto`
greenhat Feb 17, 2026
f3f7bda
refactor: rename `miden_field::WORD_SIZE_FELT` to `WORD_SIZE_FELTS`
greenhat Feb 17, 2026
81f9ce6
fix: `cargo check --all-targets --no-default-features` for the `miden…
greenhat Feb 17, 2026
a08abee
test: implement `Arbitrary` for `Felt`
greenhat Feb 17, 2026
ad0c03f
refactor: `native.rs` -> `native/mod.rs`
greenhat Feb 17, 2026
ea183ce
chore: add tests to ensure equivalence of `Felt` vs `Goldilocks`
greenhat Feb 18, 2026
2d67e4c
chore: convert `Word` tests to proptests
greenhat Feb 18, 2026
21199a3
refactor: re-export `p3-field` traits in `miden-field`
greenhat Feb 19, 2026
04f5af6
fix: gate `word!` to be available in the off-chain target only
greenhat Feb 19, 2026
a897b5b
test: add the roundtrip test for `Word::reversed`
greenhat Feb 19, 2026
daab99c
fix: don't clobber `RUSTFLAGS` env var `build-target-miden` task
greenhat Feb 19, 2026
f3e1a5a
chore: add doc comment for `Word::reversed`
greenhat Feb 19, 2026
440b59e
refactor: define `align(16)` for `Word` only for miden target
greenhat Feb 19, 2026
59dadc1
test: add a compile-time check that `Word` has the same layout as `[F…
greenhat Feb 20, 2026
7e5a2cb
fix: check that `MIDENC_TARGET_IS_MIDEN_VM` env var is not empty
greenhat Feb 20, 2026
12a70dc
chore: add comment explaining why `Word` fields have to be public
greenhat Feb 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
18 changes: 18 additions & 0 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion miden-crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ 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" }
Expand All @@ -99,6 +99,7 @@ hashbrown = { features = ["serde"], optional = true, version
hkdf = { default-features = false, version = "0.12" }
k256 = { features = ["ecdh", "ecdsa"], version = "0.13" }
miden-crypto-derive.workspace = true
miden-field = { path = "../miden-field" }
miden-serde-utils.workspace = true
Comment thread
bobbinth marked this conversation as resolved.
num = { default-features = false, features = ["alloc", "libm"], version = "0.4" }
num-complex = { default-features = false, version = "0.4" }
Expand Down Expand Up @@ -142,6 +143,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 = { path = "../miden-field", features = ["testing"] }
proptest = { default-features = false, features = ["alloc"], version = "1.7" }
rand-utils = { package = "winter-rand-utils", version = "0.13" }
rstest = { version = "0.26" }
Expand Down
3 changes: 1 addition & 2 deletions miden-crypto/src/hash/blake/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
8 changes: 4 additions & 4 deletions miden-crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ pub mod ies;
pub mod merkle;
pub mod rand;
pub mod utils;
pub mod word;
Comment thread
bobbinth marked this conversation as resolved.

use miden_field::word;
// RE-EXPORTS
// ================================================================================================
pub use p3_goldilocks::Goldilocks as Felt;
pub use word::{Word, WordError};
pub use miden_field::Felt;
pub use miden_field::{Word, WordError};
Comment thread
bobbinth marked this conversation as resolved.
Outdated

pub mod field {
//! Traits and utilities for working with the Goldilocks finite field (i.e.,
Expand Down Expand Up @@ -140,7 +140,7 @@ pub type Set<V> = alloc::collections::BTreeSet<V>;
// ================================================================================================

/// 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;
Expand Down
14 changes: 14 additions & 0 deletions miden-crypto/src/rand/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Pseudo-random element generation.

use miden_field::word::WORD_SIZE_BYTES;
use p3_field::PrimeField64;
use rand::RngCore;

Expand Down Expand Up @@ -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<Self> {
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<const N: usize> Randomizable for [u8; N] {
const VALUE_SIZE: usize = N;

Expand Down
55 changes: 1 addition & 54 deletions miden-crypto/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -41,59 +40,7 @@ pub fn word_to_hex(w: &Word) -> Result<String, fmt::Error> {
Ok(s)
}

/// Renders an array of bytes as hex into a String.
pub fn bytes_to_hex_string<const N: usize>(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<const N: usize>(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
// ================================================================================================
Expand Down
43 changes: 43 additions & 0 deletions miden-field/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Comment thread
bobbinth marked this conversation as resolved.
Outdated
repository.workspace = true
rust-version.workspace = true
version.workspace = true

[lib]
crate-type = ["rlib"]

# dependendies for off-chain target
[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"], 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" }

# dependendies for on-chain target
[dependencies]
thiserror = { default-features = false, version = "2.0" }
Comment thread
bobbinth marked this conversation as resolved.
Outdated

[features]
default = []
testing = ["dep:proptest"]

[dev-dependencies]
rand = { default-features = false, version = "0.9" }
rstest = { version = "0.26" }
Comment thread
bobbinth marked this conversation as resolved.
Outdated

[lints]
workspace = true
13 changes: 13 additions & 0 deletions miden-field/build.rs
Original file line number Diff line number Diff line change
@@ -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() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: This accepts any value (even empty string) for MIDENC_TARGET_IS_MIDEN_VM. Consider checking for a non-empty value, true or 1 to avoid accidentally triggering the miden target when the variable is set but empty.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch! Thanks! Done in 7e5a2cb

println!("cargo::rustc-cfg=miden");
}
}
26 changes: 26 additions & 0 deletions miden-field/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! A unified `Felt` for Miden Rust code.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The new miden-field crate has no tests. Since this is cryptographic code used throughout the codebase, consider adding unit tests for both the native and WASM implementations to ensure correctness of field operations.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure adding tests for miden target would be possible here since implementation of these methods would be delegated to compiler intrinsics. IIUC, this would require the whole compiler pipeline to be present. So, maybe a better places for these is in the compiler repo?

For native implementation, I think the main thing we'd be testing is that we didn't mix up delegated function calls. Maybe there is a way to add a couple of tests which would run all (most?) operations on some random elements and make sure we get the same result for Felt and native Goldilocks?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The miden target tests for Felt are living in the compiler repo since they are implemented in the compiler intrinsics which mostly delegate to the VM ops. We have a proptest test suite where we test individual ops and they are also covered in the integration test suite where we are compiling and running some contracts.

For native implementation, the only thing worth testing is that we're delegating properly to the underlying Goldilocks implementation. I'll come up with something.

//!
//! 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::{Word, WordError};
Loading
Loading