Skip to content

Commit ef1aa46

Browse files
add functions that compute packed reduced qm31 arithmetics to math_utils
1 parent 5420908 commit ef1aa46

File tree

3 files changed

+358
-0
lines changed

3 files changed

+358
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
#### Upcoming Changes
44

5+
* feat: add functions that compute packed reduced qm31 arithmetics to `math_utils` [#1944](https://github.com/lambdaclass/cairo-vm/pull/1944)
6+
57
* feat: set `encoded_instruction` to be u128 for opcode_extensions to come [#1940](https://github.com/lambdaclass/cairo-vm/pull/1940)
68

79
* feat: add `get_u32_range` to `impl VirtualMachine` add `get_u32` and `get_u32_range` to `impl Memory` [#1936](https://github.com/lambdaclass/cairo-vm/pull/1936)

vm/src/math_utils/mod.rs

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ lazy_static! {
2424
.collect::<Vec<_>>();
2525
}
2626

27+
const STWO_PRIME: u128 = (1 << 31) - 1;
28+
2729
/// Returns the `n`th (up to the `251`th power) power of 2 as a [`Felt252`]
2830
/// in constant time.
2931
/// It silently returns `1` if the input is out of bounds.
@@ -66,6 +68,182 @@ pub fn signed_felt(felt: Felt252) -> BigInt {
6668
}
6769
}
6870

71+
/// Reads four u128 coordinates from a single Felt252.
72+
/// Returns an error if the input has over 144 bits or any coordinate is unreduced.
73+
fn qm31_packed_reduced_read_coordinates(felt: Felt252) -> Result<[u128; 4], MathError> {
74+
let limbs = felt.to_le_digits();
75+
if limbs[3] != 0 || limbs[2] >= 1 << 16 {
76+
return Err(MathError::QM31UpackingError(Box::new(felt)));
77+
}
78+
let coordinates = [
79+
(limbs[0] & ((1 << 36) - 1)) as u128,
80+
((limbs[0] >> 36) + ((limbs[1] & ((1 << 8) - 1)) << 28)) as u128,
81+
((limbs[1] >> 8) & ((1 << 36) - 1)) as u128,
82+
((limbs[1] >> 44) + (limbs[2] << 20)) as u128,
83+
];
84+
for x in coordinates.iter() {
85+
if *x >= STWO_PRIME {
86+
return Err(MathError::QM31UnreducedError(Box::new(felt)));
87+
}
88+
}
89+
Ok(coordinates)
90+
}
91+
92+
/// Reduces four u128 coordinates and packs them into a single Felt252.
93+
fn qm31_coordinates_to_packed_reduced(coordinates: [u128; 4]) -> Felt252 {
94+
let bytes_part1 =
95+
((coordinates[0] % STWO_PRIME) + ((coordinates[1] % STWO_PRIME) << 36)).to_le_bytes();
96+
let bytes_part2 =
97+
((coordinates[2] % STWO_PRIME) + ((coordinates[3] % STWO_PRIME) << 36)).to_le_bytes();
98+
let mut result_bytes = [0u8; 32];
99+
result_bytes[0..9].copy_from_slice(&bytes_part1[0..9]);
100+
result_bytes[9..18].copy_from_slice(&bytes_part2[0..9]);
101+
Felt252::from_bytes_le(&result_bytes)
102+
}
103+
104+
/// Computes the addition of two QM31 elements in reduced form.
105+
/// Returns an error if either operand is not reduced.
106+
pub fn qm31_packed_reduced_add(felt1: Felt252, felt2: Felt252) -> Result<Felt252, MathError> {
107+
let coordinates1 = qm31_packed_reduced_read_coordinates(felt1)?;
108+
let coordinates2 = qm31_packed_reduced_read_coordinates(felt2)?;
109+
let result_unreduced_coordinates = [
110+
coordinates1[0] + coordinates2[0],
111+
coordinates1[1] + coordinates2[1],
112+
coordinates1[2] + coordinates2[2],
113+
coordinates1[3] + coordinates2[3],
114+
];
115+
Ok(qm31_coordinates_to_packed_reduced(
116+
result_unreduced_coordinates,
117+
))
118+
}
119+
120+
/// Computes the negative of a QM31 element in reduced form.
121+
/// Returns an error if the input is not reduced.
122+
pub fn qm31_packed_reduced_neg(felt: Felt252) -> Result<Felt252, MathError> {
123+
let coordinates = qm31_packed_reduced_read_coordinates(felt)?;
124+
Ok(qm31_coordinates_to_packed_reduced([
125+
STWO_PRIME - coordinates[0],
126+
STWO_PRIME - coordinates[1],
127+
STWO_PRIME - coordinates[2],
128+
STWO_PRIME - coordinates[3],
129+
]))
130+
}
131+
132+
/// Computes the subtraction of two QM31 elements in reduced form.
133+
/// Returns an error if either operand is not reduced.
134+
pub fn qm31_packed_reduced_sub(felt1: Felt252, felt2: Felt252) -> Result<Felt252, MathError> {
135+
let coordinates1 = qm31_packed_reduced_read_coordinates(felt1)?;
136+
let coordinates2 = qm31_packed_reduced_read_coordinates(felt2)?;
137+
let result_unreduced_coordinates = [
138+
STWO_PRIME + coordinates1[0] - coordinates2[0],
139+
STWO_PRIME + coordinates1[1] - coordinates2[1],
140+
STWO_PRIME + coordinates1[2] - coordinates2[2],
141+
STWO_PRIME + coordinates1[3] - coordinates2[3],
142+
];
143+
Ok(qm31_coordinates_to_packed_reduced(
144+
result_unreduced_coordinates,
145+
))
146+
}
147+
148+
/// Computes the multiplication of two QM31 elements in reduced form.
149+
/// Returns an error if either operand is not reduced.
150+
pub fn qm31_packed_reduced_mul(felt1: Felt252, felt2: Felt252) -> Result<Felt252, MathError> {
151+
let coordinates1 = qm31_packed_reduced_read_coordinates(felt1)?;
152+
let coordinates2 = qm31_packed_reduced_read_coordinates(felt2)?;
153+
let result_unreduced_coordinates = [
154+
5 * STWO_PRIME * STWO_PRIME + coordinates1[0] * coordinates2[0]
155+
- coordinates1[1] * coordinates2[1]
156+
+ 2 * coordinates1[2] * coordinates2[2]
157+
- 2 * coordinates1[3] * coordinates2[3]
158+
- coordinates1[2] * coordinates2[3]
159+
- coordinates1[3] * coordinates2[2],
160+
STWO_PRIME * STWO_PRIME
161+
+ coordinates1[0] * coordinates2[1]
162+
+ coordinates2[0] * coordinates1[1]
163+
+ 2 * (coordinates1[2] * coordinates2[3] + coordinates1[3] * coordinates2[2])
164+
+ coordinates1[2] * coordinates2[2]
165+
- coordinates1[3] * coordinates2[3],
166+
2 * STWO_PRIME * STWO_PRIME + coordinates1[0] * coordinates2[2]
167+
- coordinates1[1] * coordinates2[3]
168+
+ coordinates1[2] * coordinates2[0]
169+
- coordinates1[3] * coordinates2[1],
170+
coordinates1[0] * coordinates2[3]
171+
+ coordinates1[1] * coordinates2[2]
172+
+ coordinates1[2] * coordinates2[1]
173+
+ coordinates1[3] * coordinates2[0],
174+
];
175+
Ok(qm31_coordinates_to_packed_reduced(
176+
result_unreduced_coordinates,
177+
))
178+
}
179+
180+
/// Computes `v^(STWO_PRIME-2) modulo STWO_PRIME`.
181+
pub fn pow2147483645(v: u128) -> u128 {
182+
let t0 = (sqn(v, 2) * v) % STWO_PRIME;
183+
let t1 = (sqn(t0, 1) * t0) % STWO_PRIME;
184+
let t2 = (sqn(t1, 3) * t0) % STWO_PRIME;
185+
let t3 = (sqn(t2, 1) * t0) % STWO_PRIME;
186+
let t4 = (sqn(t3, 8) * t3) % STWO_PRIME;
187+
let t5 = (sqn(t4, 8) * t3) % STWO_PRIME;
188+
(sqn(t5, 7) * t2) % STWO_PRIME
189+
}
190+
191+
/// Computes `v^(2^n) modulo STWO_PRIME`.
192+
fn sqn(v: u128, n: usize) -> u128 {
193+
let mut u = v;
194+
for _ in 0..n {
195+
u = (u * u) % STWO_PRIME;
196+
}
197+
u
198+
}
199+
200+
/// Computes the inverse of a QM31 element in reduced form.
201+
/// Returns an error if the denominator is zero or either operand is not reduced.
202+
pub fn qm31_packed_reduced_inv(felt: Felt252) -> Result<Felt252, MathError> {
203+
if felt.is_zero() {
204+
return Err(MathError::DividedByZero);
205+
}
206+
let coordinates = qm31_packed_reduced_read_coordinates(felt)?;
207+
208+
let b2_r = (coordinates[2] * coordinates[2] + STWO_PRIME * STWO_PRIME
209+
- coordinates[3] * coordinates[3])
210+
% STWO_PRIME;
211+
let b2_i = (2 * coordinates[2] * coordinates[3]) % STWO_PRIME;
212+
213+
let denom_r = (coordinates[0] * coordinates[0] + STWO_PRIME * STWO_PRIME
214+
- coordinates[1] * coordinates[1]
215+
+ 2 * STWO_PRIME
216+
- 2 * b2_r
217+
+ b2_i)
218+
% STWO_PRIME;
219+
let denom_i =
220+
(2 * coordinates[0] * coordinates[1] + 3 * STWO_PRIME - 2 * b2_i - b2_r) % STWO_PRIME;
221+
222+
let denom_norm_squared = (denom_r * denom_r + denom_i * denom_i) % STWO_PRIME;
223+
let denom_norm_inverse_squared = pow2147483645(denom_norm_squared);
224+
225+
let denom_inverse_r = (denom_r * denom_norm_inverse_squared) % STWO_PRIME;
226+
let denom_inverse_i = ((STWO_PRIME - denom_i) * denom_norm_inverse_squared) % STWO_PRIME;
227+
228+
Ok(qm31_coordinates_to_packed_reduced([
229+
coordinates[0] * denom_inverse_r + STWO_PRIME * STWO_PRIME
230+
- coordinates[1] * denom_inverse_i,
231+
coordinates[0] * denom_inverse_i + coordinates[1] * denom_inverse_r,
232+
coordinates[3] * denom_inverse_i + STWO_PRIME * STWO_PRIME
233+
- coordinates[2] * denom_inverse_r,
234+
2 * STWO_PRIME * STWO_PRIME
235+
- coordinates[2] * denom_inverse_i
236+
- coordinates[3] * denom_inverse_r,
237+
]))
238+
}
239+
240+
/// Computes the division of two QM31 elements in reduced form.
241+
/// Returns an error if the input is zero.
242+
pub fn qm31_packed_reduced_div(felt1: Felt252, felt2: Felt252) -> Result<Felt252, MathError> {
243+
let felt2_inv = qm31_packed_reduced_inv(felt2)?;
244+
qm31_packed_reduced_mul(felt1, felt2_inv)
245+
}
246+
69247
///Returns the integer square root of the nonnegative integer n.
70248
///This is the floor of the exact square root of n.
71249
///Unlike math.sqrt(), this function doesn't have rounding error issues.
@@ -946,6 +1124,180 @@ mod tests {
9461124
)
9471125
}
9481126

1127+
#[test]
1128+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
1129+
fn qm31_packed_reduced_read_coordinates_over_144_bits() {
1130+
let mut felt_bytes = [0u8; 32];
1131+
felt_bytes[18] = 1;
1132+
let felt = Felt252::from_bytes_le(&felt_bytes);
1133+
assert_matches!(
1134+
qm31_packed_reduced_read_coordinates(felt),
1135+
Err(MathError::QM31UpackingError(bx)) if *bx == felt
1136+
);
1137+
}
1138+
1139+
#[test]
1140+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
1141+
fn qm31_packed_reduced_read_coordinates_unreduced() {
1142+
let mut felt_bytes = [0u8; 32];
1143+
felt_bytes[0] = 0xff;
1144+
felt_bytes[1] = 0xff;
1145+
felt_bytes[2] = 0xff;
1146+
felt_bytes[3] = (1 << 7) - 1;
1147+
let felt = Felt252::from_bytes_le(&felt_bytes);
1148+
assert_matches!(
1149+
qm31_packed_reduced_read_coordinates(felt),
1150+
Err(MathError::QM31UnreducedError(bx)) if *bx == felt
1151+
);
1152+
}
1153+
1154+
#[test]
1155+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
1156+
fn qm31_packed_reduced_add_test() {
1157+
let x_coordinates = [1414213562, 1732050807, 1618033988, 1234567890];
1158+
let y_coordinates = [1234567890, 1414213562, 1732050807, 1618033988];
1159+
let x = qm31_coordinates_to_packed_reduced(x_coordinates);
1160+
let y = qm31_coordinates_to_packed_reduced(y_coordinates);
1161+
let res = qm31_packed_reduced_add(x, y).unwrap();
1162+
let res_coordinates = qm31_packed_reduced_read_coordinates(res);
1163+
assert_eq!(
1164+
res_coordinates,
1165+
Ok([
1166+
(1414213562 + 1234567890) % STWO_PRIME,
1167+
(1732050807 + 1414213562) % STWO_PRIME,
1168+
(1618033988 + 1732050807) % STWO_PRIME,
1169+
(1234567890 + 1618033988) % STWO_PRIME,
1170+
])
1171+
);
1172+
}
1173+
1174+
#[test]
1175+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
1176+
fn qm31_packed_reduced_neg_test() {
1177+
let x_coordinates = [1749652895, 834624081, 1930174752, 2063872165];
1178+
let x = qm31_coordinates_to_packed_reduced(x_coordinates);
1179+
let res = qm31_packed_reduced_neg(x).unwrap();
1180+
let res_coordinates = qm31_packed_reduced_read_coordinates(res);
1181+
assert_eq!(
1182+
res_coordinates,
1183+
Ok([
1184+
STWO_PRIME - x_coordinates[0],
1185+
STWO_PRIME - x_coordinates[1],
1186+
STWO_PRIME - x_coordinates[2],
1187+
STWO_PRIME - x_coordinates[3]
1188+
])
1189+
);
1190+
}
1191+
1192+
#[test]
1193+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
1194+
fn qm31_packed_reduced_sub_test() {
1195+
let x_coordinates = [
1196+
(1414213562 + 1234567890) % STWO_PRIME,
1197+
(1732050807 + 1414213562) % STWO_PRIME,
1198+
(1618033988 + 1732050807) % STWO_PRIME,
1199+
(1234567890 + 1618033988) % STWO_PRIME,
1200+
];
1201+
let y_coordinates = [1414213562, 1732050807, 1618033988, 1234567890];
1202+
let x = qm31_coordinates_to_packed_reduced(x_coordinates);
1203+
let y = qm31_coordinates_to_packed_reduced(y_coordinates);
1204+
let res = qm31_packed_reduced_sub(x, y).unwrap();
1205+
let res_coordinates = qm31_packed_reduced_read_coordinates(res);
1206+
assert_eq!(
1207+
res_coordinates,
1208+
Ok([1234567890, 1414213562, 1732050807, 1618033988])
1209+
);
1210+
}
1211+
1212+
#[test]
1213+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
1214+
fn qm31_packed_reduced_mul_test() {
1215+
let x_coordinates = [1414213562, 1732050807, 1618033988, 1234567890];
1216+
let y_coordinates = [1259921049, 1442249570, 1847759065, 2094551481];
1217+
let x = qm31_coordinates_to_packed_reduced(x_coordinates);
1218+
let y = qm31_coordinates_to_packed_reduced(y_coordinates);
1219+
let res = qm31_packed_reduced_mul(x, y).unwrap();
1220+
let res_coordinates = qm31_packed_reduced_read_coordinates(res);
1221+
assert_eq!(
1222+
res_coordinates,
1223+
Ok([947980980, 1510986506, 623360030, 1260310989])
1224+
);
1225+
}
1226+
1227+
#[test]
1228+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
1229+
fn qm31_packed_reduced_inv_test() {
1230+
let x_coordinates = [1259921049, 1442249570, 1847759065, 2094551481];
1231+
let x = qm31_coordinates_to_packed_reduced(x_coordinates);
1232+
let res = qm31_packed_reduced_inv(x).unwrap();
1233+
assert_eq!(qm31_packed_reduced_mul(x, res), Ok(Felt252::from(1)));
1234+
1235+
let x_coordinates = [1, 2, 3, 4];
1236+
let x = qm31_coordinates_to_packed_reduced(x_coordinates);
1237+
let res = qm31_packed_reduced_inv(x).unwrap();
1238+
assert_eq!(qm31_packed_reduced_mul(x, res), Ok(Felt252::from(1)));
1239+
1240+
let x_coordinates = [1749652895, 834624081, 1930174752, 2063872165];
1241+
let x = qm31_coordinates_to_packed_reduced(x_coordinates);
1242+
let res = qm31_packed_reduced_inv(x).unwrap();
1243+
assert_eq!(qm31_packed_reduced_mul(x, res), Ok(Felt252::from(1)));
1244+
}
1245+
1246+
#[test]
1247+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
1248+
fn qm31_packed_reduced_inv_extensive_test() {
1249+
let mut rng = SmallRng::seed_from_u64(11480028852697973135);
1250+
let configurations = vec!["zero", "one", "minus_one", "random"];
1251+
let mut cartesian_product: Vec<[&str; 4]> = vec![];
1252+
for &a in &configurations {
1253+
for &b in &configurations {
1254+
for &c in &configurations {
1255+
for &d in &configurations {
1256+
cartesian_product.push([a, b, c, d]);
1257+
}
1258+
}
1259+
}
1260+
}
1261+
1262+
for test_case in cartesian_product {
1263+
let x_coordinates: [u128; 4] = test_case
1264+
.iter()
1265+
.map(|&x| match x {
1266+
"zero" => 0,
1267+
"one" => 1,
1268+
"minus_one" => STWO_PRIME - 1,
1269+
"random" => rng.gen_range(0..STWO_PRIME),
1270+
_ => unreachable!(),
1271+
})
1272+
.collect::<Vec<u128>>()
1273+
.try_into()
1274+
.unwrap();
1275+
if x_coordinates == [0, 0, 0, 0] {
1276+
continue;
1277+
}
1278+
let x = qm31_coordinates_to_packed_reduced(x_coordinates);
1279+
let res = qm31_packed_reduced_inv(x).unwrap();
1280+
assert_eq!(qm31_packed_reduced_mul(x, res), Ok(Felt252::from(1)));
1281+
}
1282+
}
1283+
1284+
#[test]
1285+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
1286+
fn qm31_packed_reduced_div_test() {
1287+
let x_coordinates = [1259921049, 1442249570, 1847759065, 2094551481];
1288+
let y_coordinates = [1414213562, 1732050807, 1618033988, 1234567890];
1289+
let xy_coordinates = [947980980, 1510986506, 623360030, 1260310989];
1290+
let x = qm31_coordinates_to_packed_reduced(x_coordinates);
1291+
let y = qm31_coordinates_to_packed_reduced(y_coordinates);
1292+
let xy = qm31_coordinates_to_packed_reduced(xy_coordinates);
1293+
1294+
let res = qm31_packed_reduced_div(xy, y).unwrap();
1295+
assert_eq!(res, x);
1296+
1297+
let res = qm31_packed_reduced_div(xy, x).unwrap();
1298+
assert_eq!(res, y);
1299+
}
1300+
9491301
#[cfg(feature = "std")]
9501302
proptest! {
9511303
#[test]

vm/src/types/errors/math_errors.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ pub enum MathError {
6565
"Operation failed: divmod({}, {}, {}), igcdex({}, {}) != 1 ", (*.0).0, (*.0).1, (*.0).2, (*.0).1, (*.0).2
6666
)]
6767
DivModIgcdexNotZero(Box<(BigInt, BigInt, BigInt)>),
68+
#[error("Numbers over 144 bit cannot be packed QM31 elements: {0})")]
69+
QM31UpackingError(Box<Felt252>),
70+
#[error("Number is not a packing of a QM31 in reduced form: {0})")]
71+
QM31UnreducedError(Box<Felt252>),
6872
}
6973

7074
#[cfg(test)]

0 commit comments

Comments
 (0)