authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-09-07 21:03:02+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
logc6124a9a235fce00344c7a804fe11987dd17e1f6
tree320206bbc42b101b2384ce604b23bcdce7229c33
parent230a1a0ac2935d8734115862319616e7a0143183

crypto.ff: add multiply-add operation with overflow detection

Adding a product to an accumulator is a very useful primitive for RSA and more. So, add mulAddWithOverflow() to do it on integers with a fixed active width.

1 files changed, 62 insertions(+), 0 deletions(-)

lib/std/crypto/ff.zig+62
......@@ -240,6 +240,19 @@ pub fn Uint(comptime max_bits: comptime_int) type {
240240 return x.conditionalSubWithOverflow(true, y);
241241 }
242242
243 /// Adds the product of `x` and `y` to `acc`, wrapping at the active width.
244 /// Returns 1 if any part of the result was discarded.
245 pub fn mulAddWithOverflow(acc: *Self, x: Self, y: Self) u1 {
246 assert(x.limbs_len == acc.limbs_len);
247 assert(y.limbs_len == acc.limbs_len);
248 const n = acc.limbs_len;
249 var wide: [2 * max_limbs_count]Limb = undefined;
250 @memcpy(wide[0..n], acc.limbsConst());
251 addMulVV(wide[0 .. 2 * n], x.limbsConst(), y.limbsConst());
252 @memcpy(acc.limbs(), wide[0..n]);
253 return @intFromBool(!ct.eql(orLimbs(wide[n..][0..n]), 0));
254 }
255
243256 fn expandTo(x: *Self, new_len: usize) void {
244257 assert(new_len >= x.limbs_len and new_len <= x.limbs_buffer.len);
245258 @memset(x.limbs_buffer[x.limbs_len..new_len], 0);
......@@ -402,6 +415,30 @@ fn orLimbs(limbs: []const Limb) Limb {
402415 return t;
403416}
404417
418// Adds `x * y` to `z` and returns the carry.
419fn addMulVVW(z: []Limb, x: []const Limb, y: Limb) Limb {
420 assert(z.len == x.len);
421 var carry: Limb = 0;
422 for (z, x) |*z_limb, x_limb| {
423 const wide = ct.mulWide(x_limb, y);
424 var z_lo = @addWithOverflow(z_limb.*, wide.lo);
425 var z_hi = wide.hi +% z_lo[1];
426 z_lo = @addWithOverflow(z_lo[0], carry);
427 z_hi +%= z_lo[1];
428 z_limb.* = @as(TLimb, @truncate(z_lo[0]));
429 carry = (z_hi << 1) | (z_lo[0] >> t_bits);
430 }
431 return carry;
432}
433
434// Adds `x * y` to the low `x.len` limbs of `z`. The upper limbs need no initialization.
435fn addMulVV(z: []Limb, x: []const Limb, y: []const Limb) void {
436 assert(z.len == x.len + y.len);
437 for (y, 0..) |y_limb, i| {
438 z[i + x.len] = addMulVVW(z[i..][0..x.len], x, y_limb);
439 }
440}
441
405442/// A modulus, defining a finite field.
406443/// All operations within the field are performed modulo this modulus, without heap allocations.
407444/// `max_bits` represents the number of bits in the maximum value the modulus can be set to.
......@@ -1236,4 +1273,29 @@ test "Uint addition and multiply-add" {
12361273 try testing.expect(wrapped.eql(full));
12371274 try testing.expectEqual(1, wrapped.addWithOverflow(one));
12381275 try testing.expect(wrapped.isZero());
1276
1277 for ([_]struct { acc: U, x: U, y: U, overflow: u1, expected: U }{
1278 .{ .acc = U.zero, .x = full, .y = one, .overflow = 0, .expected = full },
1279 .{ .acc = full, .x = U.zero, .y = full, .overflow = 0, .expected = full },
1280 .{ .acc = full, .x = full, .y = U.zero, .overflow = 0, .expected = full },
1281 .{ .acc = U.zero, .x = full, .y = full, .overflow = 1, .expected = one },
1282 .{ .acc = full, .x = one, .y = one, .overflow = 1, .expected = U.zero },
1283 }) |c| {
1284 var acc = c.acc;
1285 try testing.expectEqual(c.overflow, acc.mulAddWithOverflow(c.x, c.y));
1286 try testing.expect(acc.eql(c.expected));
1287 try expectWellFormedLimbs(acc);
1288 }
1289 const a: u256 = (1 << 100) + 3;
1290 const b: u256 = (1 << 80) + 7;
1291 var acc = try U.fromPrimitive(u256, a);
1292 try testing.expectEqual(0, acc.mulAddWithOverflow(try U.fromPrimitive(u256, a), try U.fromPrimitive(u256, b)));
1293 try testing.expectEqual(a + a * b, try acc.toPrimitive(u256));
1294
1295 var short = one.normalize();
1296 const high = (try U.fromPrimitive(u64, 1 << (t_bits - 1))).normalize();
1297 const two = (try U.fromPrimitive(u8, 2)).normalize();
1298 try testing.expectEqual(1, short.mulAddWithOverflow(high, two));
1299 try testing.expectEqual(1, short.limbs_len);
1300 try testing.expect(short.isOne());
12391301}