@@ -79,6 +79,59 @@ pub extern "C" fn cairo_native__u256_square_root(lo: u128, hi: u128) -> u128 {
7979 . expect ( "the square root of a u256 always fits in a u128" )
8080}
8181
82+ /// Compute `(lhs * rhs) mod STARK_PRIME` for two felt252 values, each given as
83+ /// a 32-byte little-endian buffer, writing the field product to `dst`.
84+ ///
85+ /// felt252 values are stored canonically. A naive `Felt::from_bytes_le(lhs) *
86+ /// Felt::from_bytes_le(rhs)` then `.to_bytes_le()` costs four Montgomery
87+ /// multiplications: two canonical->Montgomery conversions on the inputs, the
88+ /// multiply, and one Montgomery->canonical conversion on the output.
89+ ///
90+ /// We cut that to two by exploiting that `Felt::from_raw`/`to_raw` are free
91+ /// (they reinterpret the limbs as the internal Montgomery representation,
92+ /// without any conversion):
93+ /// - `from_raw(canonical(a))` yields a `Felt` whose *value* is `a * R⁻¹`,
94+ /// - multiplying it by `from_bytes_le(b)` (value `b`) gives value `a * b * R⁻¹`,
95+ /// whose raw representation is exactly `a * b`,
96+ /// - so `to_raw` reads the canonical product straight out.
97+ ///
98+ /// Only the `from_bytes_le(rhs)` conversion and the multiply itself cost a
99+ /// Montgomery multiplication.
100+ pub extern "C" fn cairo_native__felt252_mul ( dst : & mut [ u8 ; 32 ] , lhs : & [ u8 ; 32 ] , rhs : & [ u8 ; 32 ] ) {
101+ // value = a * R⁻¹ (free: reinterpret canonical bytes as raw limbs).
102+ let lhs = felt_from_raw_le_bytes ( lhs) ;
103+ // value = b (one Montgomery multiplication).
104+ let rhs = Felt :: from_bytes_le ( rhs) ;
105+ // value = a * b * R⁻¹, so its raw representation is the canonical `a * b`.
106+ * dst = felt_raw_to_le_bytes ( & ( lhs * rhs) ) ;
107+ }
108+
109+ /// Build a `Felt` by interpreting a 32-byte little-endian buffer as the felt's
110+ /// raw internal (Montgomery) representation. The inverse of
111+ /// [`felt_raw_to_le_bytes`]. Zero-cost: no canonical<->Montgomery conversion.
112+ ///
113+ /// `Felt::from_raw` takes limbs most-significant-first, while the buffer is
114+ /// little-endian, so the limbs are read least-significant-first then reversed.
115+ fn felt_from_raw_le_bytes ( buffer : & [ u8 ; 32 ] ) -> Felt {
116+ let mut limbs = [ 0u64 ; 4 ] ;
117+ for ( i, limb) in limbs. iter_mut ( ) . enumerate ( ) {
118+ * limb = u64:: from_le_bytes ( buffer[ i * 8 ..i * 8 + 8 ] . try_into ( ) . unwrap ( ) ) ;
119+ }
120+ limbs. reverse ( ) ;
121+ Felt :: from_raw ( limbs)
122+ }
123+
124+ /// Little-endian image of a `Felt`'s raw internal (Montgomery) representation.
125+ /// The inverse of [`felt_from_raw_le_bytes`]. Zero-cost.
126+ fn felt_raw_to_le_bytes ( value : & Felt ) -> [ u8 ; 32 ] {
127+ let limbs = value. to_raw_reversed ( ) ; // least-significant limb first
128+ let mut buffer = [ 0u8 ; 32 ] ;
129+ for ( i, limb) in limbs. iter ( ) . enumerate ( ) {
130+ buffer[ i * 8 ..i * 8 + 8 ] . copy_from_slice ( & limb. to_le_bytes ( ) ) ;
131+ }
132+ buffer
133+ }
134+
82135/// Allocate `size` bytes with `align` alignment from the per-execution arena.
83136pub unsafe extern "C" fn cairo_native__arena_alloc ( size : u64 , align : u64 ) -> * mut u8 {
84137 EXECUTION_ARENA . with ( |arena| {
@@ -976,6 +1029,91 @@ mod tests {
9761029 os:: fd:: AsRawFd ,
9771030 } ;
9781031
1032+ /// The 2-CIOS `cairo_native__felt252_mul` must equal the canonical field
1033+ /// product for every input, including 0, 1, -1, and `PRIME`-adjacent values.
1034+ #[ test]
1035+ fn felt252_mul_matches_field_product ( ) {
1036+ let cases = [
1037+ Felt :: ZERO ,
1038+ Felt :: ONE ,
1039+ Felt :: THREE ,
1040+ Felt :: from ( -1 ) ,
1041+ Felt :: from ( -2 ) ,
1042+ Felt :: from ( 1234567890123456789u64 ) ,
1043+ Felt :: from_hex_unchecked (
1044+ "0x4d6e41de886ac83938da3456ccf1481182687989ead34d9d35236f0864575a0" ,
1045+ ) ,
1046+ Felt :: MAX ,
1047+ ] ;
1048+ for & a in & cases {
1049+ for & b in & cases {
1050+ let mut dst = [ 0u8 ; 32 ] ;
1051+ cairo_native__felt252_mul ( & mut dst, & a. to_bytes_le ( ) , & b. to_bytes_le ( ) ) ;
1052+ assert_eq ! (
1053+ Felt :: from_bytes_le( & dst) ,
1054+ a * b,
1055+ "felt252_mul({a:#x}, {b:#x}) gave the wrong product"
1056+ ) ;
1057+ }
1058+ }
1059+ }
1060+
1061+ /// `felt_from_raw_le_bytes`/`felt_raw_to_le_bytes` must round-trip the raw
1062+ /// (Montgomery) representation.
1063+ #[ test]
1064+ fn felt_raw_le_bytes_round_trip ( ) {
1065+ for value in [ Felt :: ZERO , Felt :: ONE , Felt :: from ( -1 ) , Felt :: MAX ] {
1066+ assert_eq ! ( felt_from_raw_le_bytes( & felt_raw_to_le_bytes( & value) ) , value) ;
1067+ }
1068+ }
1069+
1070+ /// Clock-independent head-to-head of the mul implementations in one process:
1071+ /// the 2-CIOS `cairo_native__felt252_mul` vs the naive 4-CIOS form
1072+ /// (from_bytes_le x2 + mul + to_bytes_le). Run with:
1073+ /// cargo test --release -p cairo-native --lib felt252_mul_microbench -- --nocapture --ignored
1074+ #[ test]
1075+ #[ ignore = "timing microbenchmark; run manually with --nocapture" ]
1076+ fn felt252_mul_microbench ( ) {
1077+ use std:: hint:: black_box;
1078+ use std:: time:: Instant ;
1079+
1080+ let a = Felt :: from_hex_unchecked (
1081+ "0x4d6e41de886ac83938da3456ccf1481182687989ead34d9d35236f0864575a0" ,
1082+ )
1083+ . to_bytes_le ( ) ;
1084+ let b = Felt :: from ( 1234567890123456789u64 ) . to_bytes_le ( ) ;
1085+ let n = 20_000_000u64 ;
1086+
1087+ // 4-CIOS naive form.
1088+ let t = Instant :: now ( ) ;
1089+ let mut acc = [ 0u8 ; 32 ] ;
1090+ for _ in 0 ..n {
1091+ let lhs = Felt :: from_bytes_le ( black_box ( & a) ) ;
1092+ let rhs = Felt :: from_bytes_le ( black_box ( & b) ) ;
1093+ acc = ( lhs * rhs) . to_bytes_le ( ) ;
1094+ }
1095+ black_box ( acc) ;
1096+ let four = t. elapsed ( ) ;
1097+
1098+ // 2-CIOS form (the shipping implementation).
1099+ let t = Instant :: now ( ) ;
1100+ let mut dst = [ 0u8 ; 32 ] ;
1101+ for _ in 0 ..n {
1102+ cairo_native__felt252_mul ( & mut dst, black_box ( & a) , black_box ( & b) ) ;
1103+ black_box ( & dst) ;
1104+ }
1105+ let two = t. elapsed ( ) ;
1106+
1107+ println ! (
1108+ "felt252_mul over {n} iters: 4-CIOS={:?} ({:.1} ns/op), 2-CIOS={:?} ({:.1} ns/op), speedup={:.2}x" ,
1109+ four,
1110+ four. as_nanos( ) as f64 / n as f64 ,
1111+ two,
1112+ two. as_nanos( ) as f64 / n as f64 ,
1113+ four. as_nanos( ) as f64 / two. as_nanos( ) as f64 ,
1114+ ) ;
1115+ }
1116+
9791117 pub fn felt252_short_str ( value : & str ) -> Felt {
9801118 let values: Vec < _ > = value
9811119 . chars ( )
0 commit comments