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