Skip to content

refactor: swap Goldilocks re-export to the Felt type unified for off-chain and on-chain code#819

Merged
bobbinth merged 28 commits intomainfrom
greenhat-unified-felt
Feb 23, 2026
Merged

refactor: swap Goldilocks re-export to the Felt type unified for off-chain and on-chain code#819
bobbinth merged 28 commits intomainfrom
greenhat-unified-felt

Conversation

@greenhat
Copy link
Collaborator

@greenhat greenhat commented Feb 6, 2026

Close #771

This PR swaps the p3_goldilocks::Goldilocks as Felt re-export to the wrapper Felt(p3_goldilocks::Goldilocks) type for non-Miden targets. For the Miden target Felt(f32) is compiled (see explanation in #771).
Besides that Word type is changed to have the WIT-compatible shape (struct with a named field).

All Felt methods and implemented traits are delegating everything to the underlying Goldilocks type and its trait implementations. So the Felt should be 100% API equivalent to the Goldilocks type and can be used as a drop-in replacement. This allows us to release it as a v0.22.x patch version so that it can be included in VM v0.21 release. That's why I made this PR against the main branch.

The corresponding PR in the VM repo that updates VM to the version of miden-crypto from this branch is 0xMiden/miden-vm#2649.

The corresponding PR in the compiler repo migrating to the miden-field crate from this PR is 0xMiden/compiler#923

In the follow-up PRs:

@greenhat greenhat changed the title refactor: add Felt type unified for off-chain and on-chain code refactor: swap Goldilocks re-export to the Felt type unified for off-chain and on-chain code Feb 6, 2026
@greenhat greenhat force-pushed the greenhat-unified-felt branch from f38f76c to bff706b Compare February 9, 2026 07:59
make `Word` type to have the WIT-compatible shape
Copy link
Contributor

@huitseeker huitseeker left a comment

Choose a reason for hiding this comment

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

Some concerns with unsafe conversions, and questions inline.

///
/// # Safety
/// This assumes `(Felt, Felt, Felt, Felt)` has the same memory layout as `[Felt; 4]`.
fn as_elements_array(&self) -> &[Felt; WORD_SIZE_FELT] {
Copy link
Contributor

Choose a reason for hiding this comment

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

The cast from &(Felt, Felt, Felt, Felt) to &[Felt; 4] indeed assumes identical memory layout, but Rust does not guarantee tuple layout. This could lead to UB if the compiler changes layout.

I'd suggest adding compile-time assertions like const_assert!(size_of::<(Felt, Felt, Felt, Felt)>() == size_of::<[Felt; 4]>()); at least (and a proptest) to verify the assumption of equivalence at build (/test) time.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thank you! I converted the Word type to have named fields instead of a tuple and added a test with a roundtrip in 0e06cae.

@@ -161,14 +208,13 @@ impl Word {
where
I: Iterator<Item = &'a Self>,
{
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] {
Copy link
Contributor

Choose a reason for hiding this comment

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

words_as_elements casts &[Word] to &[Felt] via raw pointer. With #[repr(C, align(16))] on Word, this assumes no padding between elements and specific field ordering. Suggest adding const_assert! checks for size and alignment, or using the safe words_as_elements_iter instead where performance allows.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Addressed above in #819 (comment)

@@ -0,0 +1,20 @@
//! 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.

Copy link
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
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.

Comment on lines +46 to +49
// 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it not be possible to use:

  #[repr(C)]
  pub struct Word {
      a: Felt,
      b: Felt,
      c: Felt,
      d: Felt,
  }

from a WIT PoV, or do you need the tuple?

Because that is layout-compatible with [Felt; 4].

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thank you! From the WIT it does not have to be a tuple, so I changed it to your suggestion.

@greenhat greenhat marked this pull request as draft February 10, 2026 10:13
Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Looks good! Thank you! Not a full review yet, but I left some comments inline.

Comment on lines +11 to +17
pub fn rand_u64() -> u64 {
let mut bytes = [0u8; 8];
let rng = &mut rand::rng();
rng.fill(&mut bytes[..]);
let bytes = bytes[..8].try_into().unwrap();
u64::from_le_bytes(bytes)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this not be just:

pub fn rand_u64() -> u64 {
    rand::random()
}

And if so, maybe we can just use rand::random() instead of this function directly?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure. I also planned to convert the existing tests to proptest in this PR as well. Although it might be better to do it in the new PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

I did a very cursory review of this file.

@greenhat
Copy link
Collaborator Author

Thank you for the review! I've gone through all the review notes and implemented some of them right away and put the rest in the TODO in the PR desc. I'll be going through the TODO and will ping you when I'm done.

The notable change that I made after the @huitseeker review was to move the Word type to the miden-field crate. To avoid compiling miden-crypto and its dependencies for the miden (on-chain) target.

I organized the commits addressing the review notes to make it easier to review so I suggest reviewing the changes on a per-commit basis.

Copy link
Contributor

@huitseeker huitseeker left a comment

Choose a reason for hiding this comment

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

On wasm+miden, word! still expands to Word::parse, but parse is behind cfg(not(all(target_family = "wasm", miden))). I think that breaks compile for on-chain users. Please gate word! for this target or add a Miden-safe parse path.

self.as_elements().to_vec()
}

pub fn reversed(&self) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

Small: Word::reversed() has no direct test. A short round-trip test would lock the behavior.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done in a897b5b

@greenhat greenhat force-pushed the greenhat-unified-felt branch from 88c1e88 to 21199a3 Compare February 19, 2026 08:09
@greenhat
Copy link
Collaborator Author

On wasm+miden, word! still expands to Word::parse, but parse is behind cfg(not(all(target_family = "wasm", miden))). I think that breaks compile for on-chain users. Please gate word! for this target or add a Miden-safe parse path.

Thank you! Done in 04f5af6

@greenhat
Copy link
Collaborator Author

I addressed all the review notes and this PR is ready.

Since my last comment at #819 (comment) I did (notable):

I tried to structure the commits for reviewing so I suggest reviewing the changes on a per-commit basis.

@greenhat greenhat marked this pull request as ready for review February 19, 2026 12:41
Copy link
Contributor

@huitseeker huitseeker left a comment

Choose a reason for hiding this comment

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

Most everything looks good, I think we're just missing the NEG_ONE, thank you!


/// Field element representing two.
pub const TWO: Self = Self { inner: f32::from_bits(2) };

Copy link
Contributor

Choose a reason for hiding this comment

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

The native implementation has NEG_ONE constant but this WASM implementation is missing it. This could cause compilation failures for code that expects it to be available. Should we add it for API parity?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch! But since Felt for miden target is a wrapper on f32 there is no way to encode the 2^64 - 2^32 using the Rust const because it'd involve a call to the compiler intrinsics.

// `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
Contributor

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

/// 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]) }
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Consider adding compile-time assertions to verify that size_of::<Word>() == 4 * size_of::<Felt>() and that fields have no padding. This would catch layout changes at compile time rather than risking UB. The static_assertions crate or a simple const _: () = assert!(...) could work here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added in 59dadc1

@greenhat
Copy link
Collaborator Author

Most everything looks good, I think we're just missing the NEG_ONE, thank you!

Thank you! I addressed all your notes.

@greenhat greenhat requested a review from huitseeker February 20, 2026 12:20
Copy link
Contributor

@huitseeker huitseeker left a comment

Choose a reason for hiding this comment

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

Thanks a bunch!

Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Looks good! Thank you! I left one question inline.

@bobbinth bobbinth merged commit e3186e3 into main Feb 23, 2026
29 checks passed
@bobbinth bobbinth deleted the greenhat-unified-felt branch February 23, 2026 21:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants