From 6aa5a827ae41e80d8045a15e73fdc3c9b3e88b63 Mon Sep 17 00:00:00 2001 From: Severiano Jaramillo Date: Sun, 5 May 2024 15:00:54 -0700 Subject: [PATCH] Implement GfP class and arithmetic. - Ported over the GfP class functionality from the derohe project to Kotlin. - Ported over the GfP arithmetic logic too, focusing on the software implementation. We might be able to make these calculations more efficient if we do hardware calculations instead, but that can be left as a future optimization. - Created GfPTest to confirm that the arithmetic operations were implemented correctly. Hard lesson: Go operator precedence is different than Kotlin. --- .../agorise/shared/crypto/bn256/Constants.kt | 29 +++ .../net/agorise/shared/crypto/bn256/GfP.kt | 97 ++++++++++ .../agorise/shared/crypto/bn256/GfPGeneric.kt | 173 ++++++++++++++++++ .../agorise/shared/crypto/bn256/GfPTest.kt | 54 ++++++ 4 files changed, 353 insertions(+) create mode 100644 library/crypto/src/main/kotlin/net/agorise/shared/crypto/bn256/Constants.kt create mode 100644 library/crypto/src/main/kotlin/net/agorise/shared/crypto/bn256/GfP.kt create mode 100644 library/crypto/src/main/kotlin/net/agorise/shared/crypto/bn256/GfPGeneric.kt create mode 100644 library/crypto/src/test/kotlin/net/agorise/shared/crypto/bn256/GfPTest.kt diff --git a/library/crypto/src/main/kotlin/net/agorise/shared/crypto/bn256/Constants.kt b/library/crypto/src/main/kotlin/net/agorise/shared/crypto/bn256/Constants.kt new file mode 100644 index 0000000..c12ef97 --- /dev/null +++ b/library/crypto/src/main/kotlin/net/agorise/shared/crypto/bn256/Constants.kt @@ -0,0 +1,29 @@ +package net.agorise.shared.crypto.bn256 + +import java.math.BigInteger + +@OptIn(ExperimentalUnsignedTypes::class) +object Constants { + /* + * Order is the number of elements in both G₁ and G₂: 36u⁴+36u³+18u²+6u+1. + * Needs to be highly 2-adic for efficient SNARK key and proof generation. + * Order - 1 = 2^28 * 3^2 * 13 * 29 * 983 * 11003 * 237073 * 405928799 * 1670836401704629 * 13818364434197438864469338081. + * Refer to https://eprint.iacr.org/2013/879.pdf and https://eprint.iacr.org/2013/507.pdf for more information on these parameters. + */ + val Order = BigInteger("21888242871839275222246405745257275088548364400416034343698204186575808495617") + + // p2 is p, represented as little-endian 64-bit words. + val p2 = ulongArrayOf(0x3c208c16d87cfd47UL, 0x97816a916871ca8dUL, 0xb85045b68181585dUL, 0x30644e72e131a029UL) + + // np is the negative inverse of p, mod 2^256. + val np = ulongArrayOf(0x87d20782e4866389UL, 0x9ede7d651eca6ac9UL, 0xd8afcbd01833da80UL, 0xf57a22b791888c6bUL) + + // rN1 is R^-1 where R = 2^256 mod p. + val rN1 = GfP(ulongArrayOf(0xed84884a014afa37UL, 0xeb2022850278edf8UL, 0xcf63e9cfb74492d9UL, 0x2e67157159e5c639UL)) + + // r2 is R^2 where R = 2^256 mod p. + val r2 = GfP(ulongArrayOf(0xf32cfc5b538afa89UL, 0xb5e71911d44501fbUL, 0x47ab1eff0a417ff6UL, 0x06d89f71cab8351fUL)) + + // r3 is R^3 where R = 2^256 mod p. + val r3 = GfP(ulongArrayOf(0xb1cd6dafda1530dfUL, 0x62f210e6a7283db6UL, 0xef7f0b0c0ada0afbUL, 0x20fd6e902d592544UL)) +} diff --git a/library/crypto/src/main/kotlin/net/agorise/shared/crypto/bn256/GfP.kt b/library/crypto/src/main/kotlin/net/agorise/shared/crypto/bn256/GfP.kt new file mode 100644 index 0000000..4057ca0 --- /dev/null +++ b/library/crypto/src/main/kotlin/net/agorise/shared/crypto/bn256/GfP.kt @@ -0,0 +1,97 @@ +@file:OptIn(ExperimentalUnsignedTypes::class) + +package net.agorise.shared.crypto.bn256 + +/** + * GfP implementation ported over from https://github.com/deroproject/derohe/blob/main/cryptography/bn256/gfp.go + */ +class GfP(val data: ULongArray) { + + constructor() : this(ulongArrayOf(0UL, 0UL, 0UL, 0UL)) + + constructor(x: ULong) : this(ulongArrayOf(x, 0UL, 0UL, 0UL)) + + init { + if (data.size != 4) { + throw Exception("bn256: invalid field element size") + } + } + + override fun toString(): String { + return String.format("%016x %016x %016x %016x", data[3].toLong(), data[2].toLong(), data[1].toLong(), data[0].toLong()) + } + + fun set(f: GfP) { + data[0] = f.data[0] + data[1] = f.data[1] + data[2] = f.data[2] + data[3] = f.data[3] + } + + fun invert(f: GfP) { + val bits = ulongArrayOf(0x3c208c16d87cfd45UL, 0x97816a916871ca8dUL, 0xb85045b68181585dUL, 0x30644e72e131a029UL) + val sum = Constants.rN1.copy() + val power = f.copy() + + for (word in 0 until 4) { + for (bit in 0 until 64) { + if ((bits[word] shr bit) and 1UL == 1UL) { + gfpMul(sum, sum, power) + } + gfpMul(power, power, power) + } + } + + gfpMul(sum, sum, Constants.r3) + set(sum) + } + + fun marshal(out: ByteArray) { + for (w in 0 until 4) { + for (b in 0 until 8) { + out[8 * w + b] = (data[3 - w] shr (56 - 8 * b)).toByte() + } + } + } + + fun unmarshal(`in`: ByteArray) { + for (w in 0 until 4) { + data[3 - w] = 0UL + for (b in 0 until 8) { + data[3 - w] += `in`[8 * w + b].toULong() shl (56 - 8 * b) + } + } + + for (i in 3 downTo 0) { + if (data[i] < Constants.p2[i]) { + return + } + if (data[i] > Constants.p2[i]) { + throw Exception("bn256: coordinate exceeds modulus") + } + } + + throw Exception("bn256: coordinate equals modulus") + } + + fun copy(): GfP { return GfP(ulongArrayOf(data[0], data[1], data[2], data[3])) } + + companion object { + fun newGfP(x: Long): GfP { + val out = if (x >= 0) { + GfP(x.toULong()) + } else { + val negatedX = -x + val gfP = GfP(negatedX.toULong()) + gfpNeg(gfP, gfP) + gfP + } + + montEncode(out, out) + return out + } + } +} + +fun montEncode(c: GfP, a: GfP) { gfpMul(c, a, Constants.r2) } +fun montDecode(c: GfP, a: GfP) { gfpMul(c, a, GfP(1UL)) } diff --git a/library/crypto/src/main/kotlin/net/agorise/shared/crypto/bn256/GfPGeneric.kt b/library/crypto/src/main/kotlin/net/agorise/shared/crypto/bn256/GfPGeneric.kt new file mode 100644 index 0000000..193313d --- /dev/null +++ b/library/crypto/src/main/kotlin/net/agorise/shared/crypto/bn256/GfPGeneric.kt @@ -0,0 +1,173 @@ +@file:OptIn(ExperimentalUnsignedTypes::class) + +package net.agorise.shared.crypto.bn256 + +// GfP arithmetic software implementation. We can add hardware implementation later on if necessary. +// Ported from https://github.com/deroproject/derohe/blob/main/cryptography/bn256/gfp_generic.go + +fun gfpCarry(a: GfP, head: ULong) { + val b = GfP() + + var carry = 0UL + for ((i, pi) in Constants.p2.withIndex()) { + val ai = a.data[i] + val bi = ai - pi - carry + b.data[i] = bi + carry = ((pi and ai.inv()) or ((pi or ai.inv()) and bi)) shr 63 + } + carry = carry and head.inv() + + // If b is negative, then return a. Else return b. + carry = 0UL - carry + val nCarry = carry.inv() + for (i in 0 until 4) { + a.data[i] = (a.data[i] and carry) or (b.data[i] and nCarry) + } +} + +fun gfpNeg(c: GfP, a: GfP) { + var carry = 0UL + for ((i, pi) in Constants.p2.withIndex()) { + val ai = a.data[i] + val ci = pi - ai - carry + c.data[i] = ci + carry = ((ai and pi.inv()) or ((ai or pi.inv()) and ci)) shr 63 + } + gfpCarry(c, 0UL) +} + +fun gfpAdd(c: GfP, a: GfP, b: GfP) { + var carry = 0UL + for ((i, ai) in a.data.withIndex()) { + val bi = b.data[i] + val ci = ai + bi + carry + c.data[i] = ci + carry = ((ai and bi) or ((ai or bi) and ci.inv())) shr 63 + } + gfpCarry(c, carry) +} + +fun gfpSub(c: GfP, a: GfP, b: GfP) { + val t = GfP() + + var carry = 0UL + for ((i, pi) in Constants.p2.withIndex()) { + val bi = b.data[i] + val ti = pi - bi - carry + t.data[i] = ti + carry = ((bi and pi.inv()) or ((bi or pi.inv()) and ti)) shr 63 + } + + carry = 0UL + for ((i, ai) in a.data.withIndex()) { + val ti = t.data[i] + val ci = ai + ti + carry + c.data[i] = ci + carry = ((ai and ti) or ((ai or ti) and ci.inv())) shr 63 + } + gfpCarry(c, carry) +} + +fun gfpMul(c: GfP, a: GfP, b: GfP) { + val T = mul(a.data, b.data) + val m = halfMul(ulongArrayOf(T[0], T[1], T[2], T[3]), Constants.np) + val t = mul(ulongArrayOf(m[0], m[1], m[2], m[3]), Constants.p2) + + var carry = 0UL + for ((i, Ti) in T.withIndex()) { + val ti = t[i] + val zi = Ti + ti + carry + T[i] = zi + carry = (Ti and ti or (Ti or ti) and zi.inv()) shr 63 + } + + c.data[0] = T[4] + c.data[1] = T[5] + c.data[2] = T[6] + c.data[3] = T[7] + gfpCarry(c, carry) +} + +private const val mask16 = 0x0000ffffUL +private const val mask32 = 0xffffffffUL + +private fun mul(a: ULongArray, b: ULongArray): ULongArray { + val buff = ULongArray(32) + for ((i, ai) in a.withIndex()) { + val (a0, a1, a2, a3) = listOf(ai and mask16, (ai shr 16) and mask16, (ai shr 32) and mask16, ai shr 48) + + for ((j, bj) in b.withIndex()) { + val (b0, b2) = listOf(bj and mask32, bj shr 32) + + val off = 4 * (i + j) + buff[off + 0] += a0 * b0 + buff[off + 1] += a1 * b0 + buff[off + 2] += a2 * b0 + a0 * b2 + buff[off + 3] += a3 * b0 + a1 * b2 + buff[off + 4] += a2 * b2 + buff[off + 5] += a3 * b2 + } + } + + for (i in 1 until 4) { + val shift = 16 * i + + var head = 0UL + var carry = 0UL + for (j in 0 until 8) { + val block = 4 * j + + val xi = buff[block] + val yi = (buff[block + i] shl shift) + head + val zi = xi + yi + carry + buff[block] = zi + carry = ((xi and yi) or ((xi or yi) and zi.inv())) shr 63 + + head = buff[block + i] shr (64 - shift) + } + } + + return ULongArray(8) { buff[it * 4] } // return only the first 8 elements +} + +private fun halfMul(a: ULongArray, b: ULongArray): ULongArray { + val buff = ULongArray(18) + for ((i, ai) in a.withIndex()) { + val (a0, a1, a2, a3) = listOf(ai and mask16, (ai shr 16) and mask16, (ai shr 32) and mask16, ai shr 48) + + for ((j, bj) in b.withIndex()) { + if (i + j > 3) { + break + } + val (b0, b2) = listOf(bj and mask32, bj shr 32) + + val off = 4 * (i + j) + buff[off + 0] += a0 * b0 + buff[off + 1] += a1 * b0 + buff[off + 2] += a2 * b0 + a0 * b2 + buff[off + 3] += a3 * b0 + a1 * b2 + buff[off + 4] += a2 * b2 + buff[off + 5] += a3 * b2 + } + } + + for (i in 1 until 4) { + val shift = 16 * i + + var head = 0UL + var carry = 0UL + for (j in 0 until 4) { + val block = 4 * j + + val xi = buff[block] + val yi = (buff[block + i] shl shift) + head + val zi = xi + yi + carry + buff[block] = zi + carry = ((xi and yi) or ((xi or yi) and zi.inv())) shr 63 + + head = buff[block + i] shr (64 - shift) + } + } + + return ULongArray(4) { buff[it * 4] } +} diff --git a/library/crypto/src/test/kotlin/net/agorise/shared/crypto/bn256/GfPTest.kt b/library/crypto/src/test/kotlin/net/agorise/shared/crypto/bn256/GfPTest.kt new file mode 100644 index 0000000..eb981d1 --- /dev/null +++ b/library/crypto/src/test/kotlin/net/agorise/shared/crypto/bn256/GfPTest.kt @@ -0,0 +1,54 @@ +package net.agorise.shared.crypto.bn256 + +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalUnsignedTypes::class) +class GfPTest { + @Test + fun `given a GfP value - when gfpNeg is called - then result is correct`() { + val inputGfP = GfP(ulongArrayOf(0x0123456789abcdefUL, 0xfedcba9876543210UL, 0xdeadbeefdeadbeefUL, 0xfeebdaedfeebdaedUL)) + val expectedGfP = GfP(ulongArrayOf(0xfedcba9876543211UL, 0x0123456789abcdefUL, 0x2152411021524110UL, 0x0114251201142512UL)) + val actualGfP = GfP() + + gfpNeg(actualGfP, inputGfP) + + assertEquals(expectedGfP.data.toList(), actualGfP.data.toList()) + } + + @Test + fun `given two GfP values - when gfpAdd is called - then result is correct`() { + val aGfP = GfP(ulongArrayOf(0x0123456789abcdefUL, 0xfedcba9876543210UL, 0xdeadbeefdeadbeefUL, 0xfeebdaedfeebdaedUL)) + val bGfP = GfP(ulongArrayOf(0xfedcba9876543210UL, 0x0123456789abcdefUL, 0xfeebdaedfeebdaedUL, 0xdeadbeefdeadbeefUL)) + val expectedGfP = GfP(ulongArrayOf(0xc3df73e9278302b8UL, 0x687e956e978e3572UL, 0x254954275c18417fUL, 0xad354b6afc67f9b4UL)) + val actualGfP = GfP() + + gfpAdd(actualGfP, aGfP, bGfP) + + assertEquals(expectedGfP.data.toList(), actualGfP.data.toList()) + } + + @Test + fun `given two GfP values - when gfpSub is called - then result is correct`() { + val aGfP = GfP(ulongArrayOf(0x0123456789abcdefUL, 0xfedcba9876543210UL, 0xdeadbeefdeadbeefUL, 0xfeebdaedfeebdaedUL)) + val bGfP = GfP(ulongArrayOf(0xfedcba9876543210UL, 0x0123456789abcdefUL, 0xfeebdaedfeebdaedUL, 0xdeadbeefdeadbeefUL)) + val expectedGfP = GfP(ulongArrayOf(0x02468acf13579bdfUL, 0xfdb97530eca86420UL, 0xdfc1e401dfc1e402UL, 0x203e1bfe203e1bfdUL)) + val actualGfP = GfP() + + gfpSub(actualGfP, aGfP, bGfP) + + assertEquals(expectedGfP.data.toList(), actualGfP.data.toList()) + } + + @Test + fun `given two GfP values - when gfpMul is called - then result is correct`() { + val aGfP = GfP(ulongArrayOf(0x0123456789abcdefUL, 0xfedcba9876543210UL, 0xdeadbeefdeadbeefUL, 0xfeebdaedfeebdaedUL)) + val bGfP = GfP(ulongArrayOf(0xfedcba9876543210UL, 0x0123456789abcdefUL, 0xfeebdaedfeebdaedUL, 0xdeadbeefdeadbeefUL)) + val expectedGfP = GfP(ulongArrayOf(0xcbcbd377f7ad22d3UL, 0x3b89ba5d849379bfUL, 0x87b61627bd38b6d2UL, 0xc44052a2a0e654b2UL)) + val actualGfP = GfP() + + gfpMul(actualGfP, aGfP, bGfP) + + assertEquals(expectedGfP.data.toList(), actualGfP.data.toList()) + } +}