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.
This commit is contained in:
Severiano Jaramillo 2024-05-05 15:00:54 -07:00
parent 564e262f86
commit 6aa5a827ae
4 changed files with 353 additions and 0 deletions

View file

@ -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))
}

View file

@ -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)) }

View file

@ -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] }
}

View file

@ -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())
}
}