Skip to content

Commit f876fb2

Browse files
perf(felt252): compute felt252_mul via a 2-CIOS runtime binding
Route felt252 multiplication through a runtime binding instead of the inline i512 multiply-and-reduce. The inline lowering legalized into huge limb sequences that made O3 register allocation of multiplication-heavy contracts pathologically slow (e.g. the Falcon account contract: 655s -> 173s). The binding computes the product with a 2-CIOS canonical multiply, which is also ~1.77x faster than the naive 4-CIOS form. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ff87d86 commit f876fb2

3 files changed

Lines changed: 146 additions & 14 deletions

File tree

src/libfuncs/felt252.rs

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{
77
runtime_bindings::{ExtendedEuclideanWidth, RuntimeBindingsMeta},
88
MetadataStorage,
99
},
10-
utils::{felt_to_unsigned, ProgramRegistryExt, PRIME},
10+
utils::{felt_to_unsigned, get_integer_layout, ProgramRegistryExt, PRIME},
1111
};
1212
use cairo_lang_sierra::{
1313
extensions::{
@@ -128,21 +128,41 @@ pub fn build_binary_operation<'ctx, 'this>(
128128
entry.trunci(result, felt252_ty, location)?
129129
}
130130
Felt252BinaryOperator::Mul => {
131-
let lhs = entry.extui(lhs, i512, location)?;
132-
let rhs = entry.extui(rhs, i512, location)?;
133-
let result = entry.muli(lhs, rhs, location)?;
134-
135-
let prime = entry.const_int_from_type(context, location, PRIME.clone(), i512)?;
136-
let result_mod = entry.append_op_result(arith::remui(result, prime, location))?;
137-
let is_out_of_range =
138-
entry.cmpi(context, CmpiPredicate::Uge, result, prime, location)?;
131+
let layout_i256 = get_integer_layout(256);
132+
let lhs_ptr =
133+
helper
134+
.init_block()
135+
.alloca1(context, location, i256, layout_i256.align())?;
136+
let rhs_ptr =
137+
helper
138+
.init_block()
139+
.alloca1(context, location, i256, layout_i256.align())?;
140+
let dst_ptr =
141+
helper
142+
.init_block()
143+
.alloca1(context, location, i256, layout_i256.align())?;
144+
145+
let lhs_i256 = entry.extui(lhs, i256, location)?;
146+
let rhs_i256 = entry.extui(rhs, i256, location)?;
147+
entry.store(context, location, lhs_ptr, lhs_i256)?;
148+
entry.store(context, location, rhs_ptr, rhs_i256)?;
139149

140-
let result = entry.append_op_result(arith::select(
141-
is_out_of_range,
142-
result_mod,
143-
result,
150+
let runtime_bindings_meta = metadata
151+
.get_mut::<RuntimeBindingsMeta>()
152+
.to_native_assert_error(
153+
"Unable to get the RuntimeBindingsMeta from MetadataStorage",
154+
)?;
155+
runtime_bindings_meta.felt252_mul(
156+
context,
157+
helper.module,
158+
entry,
159+
dst_ptr,
160+
lhs_ptr,
161+
rhs_ptr,
144162
location,
145-
))?;
163+
)?;
164+
165+
let result = entry.load(context, location, dst_ptr, i256)?;
146166
entry.trunci(result, felt252_ty, location)?
147167
}
148168
Felt252BinaryOperator::Div => {

src/metadata/runtime_bindings.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ enum RuntimeBinding {
5353
U64SquareRoot,
5454
U128SquareRoot,
5555
U256SquareRoot,
56+
Felt252Mul,
5657
CircuitArithOperation,
5758
DictIntoEntries,
5859
QM31Add,
@@ -87,6 +88,7 @@ impl RuntimeBinding {
8788
Self::U64SquareRoot => "cairo_native__u64_square_root",
8889
Self::U128SquareRoot => "cairo_native__u128_square_root",
8990
Self::U256SquareRoot => "cairo_native__u256_square_root",
91+
Self::Felt252Mul => "cairo_native__felt252_mul",
9092
Self::CircuitArithOperation => "cairo_native__circuit_arith_operation",
9193
Self::DictIntoEntries => "cairo_native__dict_into_entries",
9294
Self::QM31Add => "cairo_native__libfunc__qm31__qm31_add",
@@ -134,6 +136,7 @@ impl RuntimeBinding {
134136
Self::U64SquareRoot => cairo_native__u64_square_root as *const (),
135137
Self::U128SquareRoot => cairo_native__u128_square_root as *const (),
136138
Self::U256SquareRoot => cairo_native__u256_square_root as *const (),
139+
Self::Felt252Mul => cairo_native__felt252_mul as *const (),
137140
Self::ArenaAlloc => cairo_native__arena_alloc as *const (),
138141
Self::ExtendedEuclideanAlgorithm(_) | Self::CircuitArithOperation => return None,
139142
#[cfg(feature = "with-cheatcode")]
@@ -349,6 +352,34 @@ impl RuntimeBindingsMeta {
349352
.into())
350353
}
351354

355+
/// Register if necessary, then invoke `cairo_native__felt252_mul`, which
356+
/// computes `(lhs * rhs) mod STARK_PRIME`. The pointers reference 32-byte
357+
/// little-endian felt252 buffers; the result is written to `dst_ptr`.
358+
#[allow(clippy::too_many_arguments)]
359+
pub fn felt252_mul<'c, 'a>(
360+
&mut self,
361+
context: &'c Context,
362+
module: &Module,
363+
block: &'a Block<'c>,
364+
dst_ptr: Value<'c, '_>,
365+
lhs_ptr: Value<'c, '_>,
366+
rhs_ptr: Value<'c, '_>,
367+
location: Location<'c>,
368+
) -> Result<OperationRef<'c, 'a>>
369+
where
370+
'c: 'a,
371+
{
372+
let function =
373+
self.build_function(context, module, block, location, RuntimeBinding::Felt252Mul)?;
374+
375+
Ok(block.append_operation(
376+
OperationBuilder::new("llvm.call", location)
377+
.add_operands(&[function])
378+
.add_operands(&[dst_ptr, lhs_ptr, rhs_ptr])
379+
.build()?,
380+
))
381+
}
382+
352383
/// Builds, if necessary, the circuit operation function, used to perform
353384
/// circuit arithmetic operations.
354385
///
@@ -973,6 +1004,7 @@ pub fn setup_runtime(find_symbol_ptr: impl Fn(&str) -> Option<*mut c_void>) {
9731004
RuntimeBinding::U64SquareRoot,
9741005
RuntimeBinding::U128SquareRoot,
9751006
RuntimeBinding::U256SquareRoot,
1007+
RuntimeBinding::Felt252Mul,
9761008
#[cfg(feature = "with-cheatcode")]
9771009
RuntimeBinding::VtableCheatcode,
9781010
] {

src/runtime.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,57 @@ 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`. Each input buffer is the 32-byte
83+
/// little-endian *canonical* integer; the canonical product is written to `dst`.
84+
///
85+
/// A `Felt` stores its value `v` in Montgomery form: its internal limbs — what
86+
/// we call its *raw* bytes — are `v · R mod p`, with `R = 2^256 mod p`. So:
87+
/// - `from_bytes_le`/`to_bytes_le` convert canonical integer <-> `Felt`, each
88+
/// costing one Montgomery multiplication;
89+
/// - `from_raw`/`to_raw` move the raw (Montgomery-limb) bytes in/out with no
90+
/// conversion at all — they are free.
91+
///
92+
/// The naive `from_bytes_le(a) * from_bytes_le(b)` then `to_bytes_le` is four
93+
/// Montgomery multiplications. Reinterpreting `a`'s canonical bytes as raw
94+
/// Montgomery limbs (`from_raw`) cuts it to two. Tracking the value and the raw
95+
/// bytes of each `Felt` (canonical inputs `a` = `lhs`, `b` = `rhs`):
96+
pub extern "C" fn cairo_native__felt252_mul(dst: &mut [u8; 32], lhs: &[u8; 32], rhs: &[u8; 32]) {
97+
// value = a * R⁻¹ raw bytes = a (free: read a as raw limbs)
98+
let lhs = felt_from_raw_le_bytes(lhs);
99+
// value = b raw bytes = b · R (1 Montgomery mul)
100+
let rhs = Felt::from_bytes_le(rhs);
101+
// value = a * b * R⁻¹ raw bytes = a · b (1 Montgomery mul)
102+
let product = lhs * rhs;
103+
// read the raw bytes (= a * b), the canonical product.
104+
*dst = felt_raw_to_le_bytes(&product);
105+
}
106+
107+
/// Build a `Felt` by interpreting a 32-byte little-endian buffer as the felt's
108+
/// raw internal (Montgomery) representation. The inverse of
109+
/// [`felt_raw_to_le_bytes`]. Zero-cost: no canonical<->Montgomery conversion.
110+
///
111+
/// `Felt::from_raw` takes limbs most-significant-first, while the buffer is
112+
/// little-endian, so the limbs are read least-significant-first then reversed.
113+
fn felt_from_raw_le_bytes(buffer: &[u8; 32]) -> Felt {
114+
let mut limbs = [0u64; 4];
115+
for (i, limb) in limbs.iter_mut().enumerate() {
116+
*limb = u64::from_le_bytes(buffer[i * 8..i * 8 + 8].try_into().unwrap());
117+
}
118+
limbs.reverse();
119+
Felt::from_raw(limbs)
120+
}
121+
122+
/// Little-endian image of a `Felt`'s raw internal (Montgomery) representation.
123+
/// The inverse of [`felt_from_raw_le_bytes`]. Zero-cost.
124+
fn felt_raw_to_le_bytes(value: &Felt) -> [u8; 32] {
125+
let limbs = value.to_raw_reversed(); // least-significant limb first
126+
let mut buffer = [0u8; 32];
127+
for (i, limb) in limbs.iter().enumerate() {
128+
buffer[i * 8..i * 8 + 8].copy_from_slice(&limb.to_le_bytes());
129+
}
130+
buffer
131+
}
132+
82133
/// Allocate `size` bytes with `align` alignment from the per-execution arena.
83134
pub unsafe extern "C" fn cairo_native__arena_alloc(size: u64, align: u64) -> *mut u8 {
84135
EXECUTION_ARENA.with(|arena| {
@@ -976,6 +1027,35 @@ mod tests {
9761027
os::fd::AsRawFd,
9771028
};
9781029

1030+
/// The 2-CIOS `cairo_native__felt252_mul` must equal the canonical field
1031+
/// product for every input, including 0, 1, -1, and `PRIME`-adjacent values.
1032+
#[test]
1033+
fn felt252_mul_matches_field_product() {
1034+
let cases = [
1035+
Felt::ZERO,
1036+
Felt::ONE,
1037+
Felt::THREE,
1038+
Felt::from(-1),
1039+
Felt::from(-2),
1040+
Felt::from(1234567890123456789u64),
1041+
Felt::from_hex_unchecked(
1042+
"0x4d6e41de886ac83938da3456ccf1481182687989ead34d9d35236f0864575a0",
1043+
),
1044+
Felt::MAX,
1045+
];
1046+
for &a in &cases {
1047+
for &b in &cases {
1048+
let mut dst = [0u8; 32];
1049+
cairo_native__felt252_mul(&mut dst, &a.to_bytes_le(), &b.to_bytes_le());
1050+
assert_eq!(
1051+
Felt::from_bytes_le(&dst),
1052+
a * b,
1053+
"felt252_mul({a:#x}, {b:#x}) gave the wrong product"
1054+
);
1055+
}
1056+
}
1057+
}
1058+
9791059
pub fn felt252_short_str(value: &str) -> Felt {
9801060
let values: Vec<_> = value
9811061
.chars()

0 commit comments

Comments
 (0)