Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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;

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};

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"
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" }

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

[dev-dependencies]
rand = { default-features = false, version = "0.9" }
rstest = { version = "0.26" }

[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() {
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
Contributor

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.

//!
//! 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