diff --git a/src/libfuncs/felt252.rs b/src/libfuncs/felt252.rs index baac49973..8a7944766 100644 --- a/src/libfuncs/felt252.rs +++ b/src/libfuncs/felt252.rs @@ -7,7 +7,7 @@ use crate::{ runtime_bindings::{ExtendedEuclideanWidth, RuntimeBindingsMeta}, MetadataStorage, }, - utils::{felt_to_unsigned, ProgramRegistryExt, PRIME}, + utils::{felt_to_unsigned, get_integer_layout, ProgramRegistryExt, PRIME}, }; use cairo_lang_sierra::{ extensions::{ @@ -128,21 +128,41 @@ pub fn build_binary_operation<'ctx, 'this>( entry.trunci(result, felt252_ty, location)? } Felt252BinaryOperator::Mul => { - let lhs = entry.extui(lhs, i512, location)?; - let rhs = entry.extui(rhs, i512, location)?; - let result = entry.muli(lhs, rhs, location)?; - - let prime = entry.const_int_from_type(context, location, PRIME.clone(), i512)?; - let result_mod = entry.append_op_result(arith::remui(result, prime, location))?; - let is_out_of_range = - entry.cmpi(context, CmpiPredicate::Uge, result, prime, location)?; + let layout_i256 = get_integer_layout(256); + let lhs_ptr = + helper + .init_block() + .alloca1(context, location, i256, layout_i256.align())?; + let rhs_ptr = + helper + .init_block() + .alloca1(context, location, i256, layout_i256.align())?; + let dst_ptr = + helper + .init_block() + .alloca1(context, location, i256, layout_i256.align())?; + + let lhs_i256 = entry.extui(lhs, i256, location)?; + let rhs_i256 = entry.extui(rhs, i256, location)?; + entry.store(context, location, lhs_ptr, lhs_i256)?; + entry.store(context, location, rhs_ptr, rhs_i256)?; - let result = entry.append_op_result(arith::select( - is_out_of_range, - result_mod, - result, + let runtime_bindings_meta = metadata + .get_mut::() + .to_native_assert_error( + "Unable to get the RuntimeBindingsMeta from MetadataStorage", + )?; + runtime_bindings_meta.felt252_mul( + context, + helper.module, + entry, + dst_ptr, + lhs_ptr, + rhs_ptr, location, - ))?; + )?; + + let result = entry.load(context, location, dst_ptr, i256)?; entry.trunci(result, felt252_ty, location)? } Felt252BinaryOperator::Div => { diff --git a/src/metadata/runtime_bindings.rs b/src/metadata/runtime_bindings.rs index b9af83f23..09d619fa3 100644 --- a/src/metadata/runtime_bindings.rs +++ b/src/metadata/runtime_bindings.rs @@ -53,6 +53,7 @@ enum RuntimeBinding { U64SquareRoot, U128SquareRoot, U256SquareRoot, + Felt252Mul, CircuitArithOperation, DictIntoEntries, QM31Add, @@ -87,6 +88,7 @@ impl RuntimeBinding { Self::U64SquareRoot => "cairo_native__u64_square_root", Self::U128SquareRoot => "cairo_native__u128_square_root", Self::U256SquareRoot => "cairo_native__u256_square_root", + Self::Felt252Mul => "cairo_native__felt252_mul", Self::CircuitArithOperation => "cairo_native__circuit_arith_operation", Self::DictIntoEntries => "cairo_native__dict_into_entries", Self::QM31Add => "cairo_native__libfunc__qm31__qm31_add", @@ -134,6 +136,7 @@ impl RuntimeBinding { Self::U64SquareRoot => cairo_native__u64_square_root as *const (), Self::U128SquareRoot => cairo_native__u128_square_root as *const (), Self::U256SquareRoot => cairo_native__u256_square_root as *const (), + Self::Felt252Mul => cairo_native__felt252_mul as *const (), Self::ArenaAlloc => cairo_native__arena_alloc as *const (), Self::ExtendedEuclideanAlgorithm(_) | Self::CircuitArithOperation => return None, #[cfg(feature = "with-cheatcode")] @@ -349,6 +352,34 @@ impl RuntimeBindingsMeta { .into()) } + /// Register if necessary, then invoke `cairo_native__felt252_mul`, which + /// computes `(lhs * rhs) mod STARK_PRIME`. The pointers reference 32-byte + /// little-endian felt252 buffers; the result is written to `dst_ptr`. + #[allow(clippy::too_many_arguments)] + pub fn felt252_mul<'c, 'a>( + &mut self, + context: &'c Context, + module: &Module, + block: &'a Block<'c>, + dst_ptr: Value<'c, '_>, + lhs_ptr: Value<'c, '_>, + rhs_ptr: Value<'c, '_>, + location: Location<'c>, + ) -> Result> + where + 'c: 'a, + { + let function = + self.build_function(context, module, block, location, RuntimeBinding::Felt252Mul)?; + + Ok(block.append_operation( + OperationBuilder::new("llvm.call", location) + .add_operands(&[function]) + .add_operands(&[dst_ptr, lhs_ptr, rhs_ptr]) + .build()?, + )) + } + /// Builds, if necessary, the circuit operation function, used to perform /// circuit arithmetic operations. /// @@ -973,6 +1004,7 @@ pub fn setup_runtime(find_symbol_ptr: impl Fn(&str) -> Option<*mut c_void>) { RuntimeBinding::U64SquareRoot, RuntimeBinding::U128SquareRoot, RuntimeBinding::U256SquareRoot, + RuntimeBinding::Felt252Mul, #[cfg(feature = "with-cheatcode")] RuntimeBinding::VtableCheatcode, ] { diff --git a/src/runtime.rs b/src/runtime.rs index b1380c02a..72adbf212 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -79,6 +79,57 @@ pub extern "C" fn cairo_native__u256_square_root(lo: u128, hi: u128) -> u128 { .expect("the square root of a u256 always fits in a u128") } +/// Compute `(lhs * rhs) mod STARK_PRIME`. Each input buffer is the 32-byte +/// little-endian *canonical* integer; the canonical product is written to `dst`. +/// +/// A `Felt` stores its value `v` in Montgomery form: its internal limbs — what +/// we call its *raw* bytes — are `v · R mod p`, with `R = 2^256 mod p`. So: +/// - `from_bytes_le`/`to_bytes_le` convert canonical integer <-> `Felt`, each +/// costing one Montgomery multiplication; +/// - `from_raw`/`to_raw` move the raw (Montgomery-limb) bytes in/out with no +/// conversion at all — they are free. +/// +/// The naive `from_bytes_le(a) * from_bytes_le(b)` then `to_bytes_le` is four +/// Montgomery multiplications. Reinterpreting `a`'s canonical bytes as raw +/// Montgomery limbs (`from_raw`) cuts it to two. Tracking the value and the raw +/// bytes of each `Felt` (canonical inputs `a` = `lhs`, `b` = `rhs`): +pub extern "C" fn cairo_native__felt252_mul(dst: &mut [u8; 32], lhs: &[u8; 32], rhs: &[u8; 32]) { + // value = a * R⁻¹ raw bytes = a (free: read a as raw limbs) + let lhs = felt_from_raw_le_bytes(lhs); + // value = b raw bytes = b * R (1 Montgomery mul) + let rhs = Felt::from_bytes_le(rhs); + // value = a * b * R⁻¹ raw bytes = a * b (1 Montgomery mul) + let product = lhs * rhs; + // read the raw bytes (= a * b), the canonical product. + *dst = felt_raw_to_le_bytes(&product); +} + +/// Build a `Felt` by interpreting a 32-byte little-endian buffer as the felt's +/// raw internal (Montgomery) representation. The inverse of +/// [`felt_raw_to_le_bytes`]. Zero-cost: no canonical<->Montgomery conversion. +/// +/// `Felt::from_raw` takes limbs most-significant-first, while the buffer is +/// little-endian, so the limbs are read least-significant-first then reversed. +fn felt_from_raw_le_bytes(buffer: &[u8; 32]) -> Felt { + let mut limbs = [0u64; 4]; + for (i, limb) in limbs.iter_mut().enumerate() { + *limb = u64::from_le_bytes(buffer[i * 8..i * 8 + 8].try_into().unwrap()); + } + limbs.reverse(); + Felt::from_raw(limbs) +} + +/// Little-endian image of a `Felt`'s raw internal (Montgomery) representation. +/// The inverse of [`felt_from_raw_le_bytes`]. Zero-cost. +fn felt_raw_to_le_bytes(value: &Felt) -> [u8; 32] { + let limbs = value.to_raw_reversed(); // least-significant limb first + let mut buffer = [0u8; 32]; + for (i, limb) in limbs.iter().enumerate() { + buffer[i * 8..i * 8 + 8].copy_from_slice(&limb.to_le_bytes()); + } + buffer +} + /// Allocate `size` bytes with `align` alignment from the per-execution arena. pub unsafe extern "C" fn cairo_native__arena_alloc(size: u64, align: u64) -> *mut u8 { EXECUTION_ARENA.with(|arena| { @@ -976,6 +1027,35 @@ mod tests { os::fd::AsRawFd, }; + /// The 2-CIOS `cairo_native__felt252_mul` must equal the canonical field + /// product for every input, including 0, 1, -1, and `PRIME`-adjacent values. + #[test] + fn felt252_mul_matches_field_product() { + let cases = [ + Felt::ZERO, + Felt::ONE, + Felt::THREE, + Felt::from(-1), + Felt::from(-2), + Felt::from(1234567890123456789u64), + Felt::from_hex_unchecked( + "0x4d6e41de886ac83938da3456ccf1481182687989ead34d9d35236f0864575a0", + ), + Felt::MAX, + ]; + for &a in &cases { + for &b in &cases { + let mut dst = [0u8; 32]; + cairo_native__felt252_mul(&mut dst, &a.to_bytes_le(), &b.to_bytes_le()); + assert_eq!( + Felt::from_bytes_le(&dst), + a * b, + "felt252_mul({a:#x}, {b:#x}) gave the wrong product" + ); + } + } + } + pub fn felt252_short_str(value: &str) -> Felt { let values: Vec<_> = value .chars()