authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-09-07 21:03:09+02:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-10-01 12:06:17+02:00
log71341840a0040835002c430c45f266500853b2f5
treebacd5abb4b7c17080a8325d7fcbaec68c4111122
parentd5a8438044627bb7d6b7d28c6ecda50a6c6998b5

crypto.ff: add division by a small integer and GCD for public values

Implement fast division by a small integer and GCD for public values (works with operands with different active width).

1 files changed, 79 insertions(+), 1 deletions(-)

lib/std/crypto/ff.zig+79-1
......@@ -53,8 +53,10 @@ pub const FieldElementError = error{NonCanonical};
5353/// Invalid representation (Montgomery vs non-Montgomery domain.)
5454pub const RepresentationError = error{UnexpectedRepresentation};
5555
56pub const DivisionByZeroError = error{DivisionByZero};
57
5658/// The set of all possible errors `std.crypto.ff` functions can return.
57pub const Error = OverflowError || InvalidModulusError || NullExponentError || FieldElementError || RepresentationError;
59pub const Error = OverflowError || InvalidModulusError || NullExponentError || FieldElementError || RepresentationError || DivisionByZeroError;
5860
5961/// An unsigned big integer with a fixed maximum size (`max_bits`), suitable for cryptographic operations.
6062/// Storage rounds up to whole limbs and can hold up to `capacity_bits` bits.
......@@ -298,6 +300,54 @@ pub fn Uint(comptime max_bits: comptime_int) type {
298300 @memset(x_limbs[active..], 0);
299301 }
300302
303 /// Divides by `divisor` in place and returns the remainder.
304 /// Returns `error.DivisionByZero` without changing the integer if `divisor` is zero.
305 pub fn divRemPublic(x: *Self, divisor: usize) DivisionByZeroError!usize {
306 if (divisor == 0) return error.DivisionByZero;
307 const Wide = @Int(.unsigned, 2 * @bitSizeOf(Limb));
308 const x_limbs = x.limbs();
309 var rem: Limb = 0;
310 var i = x.limbs_len;
311 while (i != 0) {
312 i -= 1;
313 const num = (@as(Wide, rem) << t_bits) | x_limbs[i];
314 x_limbs[i] = @intCast(num / divisor);
315 rem = @intCast(num % divisor);
316 }
317 return rem;
318 }
319
320 /// Returns the greatest common divisor, with gcd(x, 0) = x.
321 /// Returns `error.DivisionByZero` if both operands are zero.
322 pub fn gcdPublic(x: Self, y: Self) DivisionByZeroError!Self {
323 if (x.isZero() and y.isZero()) return error.DivisionByZero;
324 if (x.isZero()) return y;
325 if (y.isZero()) return x;
326
327 var a = x;
328 var b = y;
329 const len = @max(a.limbs_len, b.limbs_len);
330 a.expandTo(len);
331 b.expandTo(len);
332
333 const a_shift = a.trailingZeroBitsPublic();
334 const b_shift = b.trailingZeroBitsPublic();
335 a.shiftRightPublic(a_shift);
336 b.shiftRightPublic(b_shift);
337 while (true) {
338 switch (a.compare(b)) {
339 .eq => break,
340 .lt => mem.swap(Self, &a, &b),
341 .gt => {},
342 }
343 _ = a.subWithOverflow(b);
344 a.shiftRightPublic(a.trailingZeroBitsPublic());
345 }
346
347 a.shiftLeft(@min(a_shift, b_shift)); // GCD always fits in either input, overflow is never an issue
348 return a;
349 }
350
301351 fn expandTo(x: *Self, new_len: usize) void {
302352 assert(new_len >= x.limbs_len and new_len <= x.limbs_buffer.len);
303353 @memset(x.limbs_buffer[x.limbs_len..new_len], 0);
......@@ -1371,6 +1421,34 @@ test "Uint bit measurement and shifts" {
13711421 try testing.expectEqual(1, x.limbs_len);
13721422}
13731423
1424test "Uint division and gcd" {
1425 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;
1426
1427 const U = Uint(256);
1428 const v: u256 = (1 << 255) + (1 << t_bits) + 13;
1429 for ([_]usize{ 1, 7, math.maxInt(TLimb), (1 << t_bits) + 5, math.maxInt(usize) }) |divisor| {
1430 var x = try U.fromPrimitive(u256, v);
1431 try testing.expectEqual(v % divisor, try x.divRemPublic(divisor));
1432 try testing.expectEqual(v / divisor, try x.toPrimitive(u256));
1433 try expectWellFormedLimbs(x);
1434 }
1435 var x = (try U.fromPrimitive(u16, 1000)).normalize();
1436 try testing.expectError(error.DivisionByZero, x.divRemPublic(0));
1437 try testing.expectEqual(6, try x.divRemPublic(7));
1438 try testing.expectEqual(142, try x.toPrimitive(u16));
1439 try testing.expectEqual(1, x.limbs_len);
1440 try testing.expectEqual(142, try x.divRemPublic(1000));
1441 try testing.expectEqual(0, try x.divRemPublic(7));
1442
1443 for ([_][3]u256{ .{ 0, 5, 5 }, .{ 6, 6, 6 }, .{ 17, 4, 1 }, .{ 15 << 200, 21 << 100, 3 << 100 } }) |c| {
1444 const a = (try U.fromPrimitive(u256, c[0])).normalize();
1445 const b = try U.fromPrimitive(u256, c[1]);
1446 try testing.expectEqual(c[2], try (try a.gcdPublic(b)).toPrimitive(u256));
1447 try testing.expectEqual(c[2], try (try b.gcdPublic(a)).toPrimitive(u256));
1448 }
1449 try testing.expectError(error.DivisionByZero, U.zero.gcdPublic(U.zero));
1450}
1451
13741452test "Uint addition and multiply-add" {
13751453 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;
13761454