From 91f2ad02676ddd2fc9f55ccf5e4e9dcfab8dd905 Mon Sep 17 00:00:00 2001
From: Satya This delegates to the pure-Java Jacobian ladder. It has a uniform operation
+ * schedule, but it is not a JVM constant-time guarantee. This delegates to the pure-Java Jacobian ladder. It has a uniform operation
+ * schedule, but it is not a JVM constant-time guarantee. Always performs both a doubling and an addition per bit, regardless of bit value.
- * The bit only determines which accumulator receives which result. This prevents
- * timing side-channels that leak scalar bits.
Use this when the scalar is secret (e.g., blinding factors r, s in Groth16).
+ *Use this only as the pure-Java fixed-schedule path. High-value secret-bearing + * workloads should use a native provider with a stronger side-channel contract.
* - * @param scalar non-negative scalar (fixed 255-bit processing for BLS12-381 Fr) + * @param scalar non-negative scalar with {@code bitLength() <= 256} */ public JacobianG1BLS381 ctScalarMul(BigInteger scalar) { if (scalar.signum() == 0) return INFINITY; if (scalar.signum() < 0) return negate().ctScalarMul(scalar.negate()); + if (scalar.bitLength() > 256) { + throw new IllegalArgumentException("ctScalarMul scalar must fit in 256 bits"); + } if (this.isInfinity()) return INFINITY; // Montgomery ladder with a fixed operation schedule per bit. diff --git a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381.java index 0da4eb6..b99de9e 100644 --- a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381.java +++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381.java @@ -124,10 +124,19 @@ public JacobianG2BLS381 scalarMul(BigInteger scalar) { return result; } - /** Constant-time scalar multiplication using Montgomery ladder. */ + /** + * Fixed-schedule scalar multiplication using a Montgomery ladder. + * + *This keeps a uniform operation schedule, but it is not a JVM constant-time + * guarantee: bit access, branching, point special cases, and field reductions remain + * variable-time.
+ */ public JacobianG2BLS381 ctScalarMul(BigInteger scalar) { if (scalar.signum() == 0) return INFINITY; if (scalar.signum() < 0) return negate().ctScalarMul(scalar.negate()); + if (scalar.bitLength() > 256) { + throw new IllegalArgumentException("ctScalarMul scalar must fit in 256 bits"); + } if (this.isInfinity()) return INFINITY; JacobianG2BLS381 r0 = INFINITY; diff --git a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381.java index 325f2d2..7b20f6b 100644 --- a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381.java +++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381.java @@ -95,8 +95,12 @@ public static MontFp381 fromLong(long val) { /** * Creates a MontFp381 from 6 Montgomery-form limbs (little-endian: l0 is least significant). * The limbs must already be in Montgomery form — no conversion is performed. + * The represented Montgomery residue must be canonical ({@code < p}). */ public static MontFp381 fromMontLimbs(long l0, long l1, long l2, long l3, long l4, long l5) { + if (geqMod(l0, l1, l2, l3, l4, l5)) { + throw new IllegalArgumentException("Montgomery Fp limbs must be canonical"); + } return new MontFp381(l0, l1, l2, l3, l4, l5); } @@ -173,9 +177,9 @@ public MontFp381 dbl() { /** * Field inversion via Fermat's little theorem: a^{-1} = a^{p-2} mod p. * - *Uses a fixed-length square-and-multiply chain over the exponent p-2, - * providing constant-time behavior (the same number of multiplications - * regardless of the input value).
+ *Uses a fixed public exponent ({@code p - 2}). This keeps the exponent schedule + * independent of the input value, but the pure-Java field operations are not a full + * JVM constant-time guarantee.
*/ public MontFp381 inverse() { if (isZero()) throw new ArithmeticException("Cannot invert zero"); diff --git a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381.java index cc2332e..06af0eb 100644 --- a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381.java +++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381.java @@ -76,6 +76,9 @@ public static MontFr381 fromLong(long val) { } static MontFr381 fromMontLimbs(long l0, long l1, long l2, long l3) { + if (geqMod(l0, l1, l2, l3)) { + throw new IllegalArgumentException("Montgomery Fr limbs must be canonical"); + } return new MontFr381(l0, l1, l2, l3); } @@ -136,7 +139,8 @@ public MontFr381 square() { /** * Field inversion via Fermat's little theorem: a^{-1} = a^{r-2} mod r. - * Uses fixed-length square-and-multiply for constant-time behavior. + * Uses a fixed public exponent schedule, but the pure-Java field operations are + * not a full JVM constant-time guarantee. */ public MontFr381 inverse() { if (isZero()) throw new ArithmeticException("Cannot invert zero"); diff --git a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/Bls12381Provider.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/Bls12381Provider.java index 5d63a89..bd53c06 100644 --- a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/Bls12381Provider.java +++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/Bls12381Provider.java @@ -13,7 +13,8 @@ * *Scalar multiplication methods reduce the scalar modulo the BLS12-381 * scalar-field order. They are for public scalars. Protocol code that multiplies - * by secret scalars must use a backend with an explicit constant-time contract.
+ * by secret scalars must use the secret-scalar methods and select a provider with + * a side-channel contract appropriate for the deployment. */ public interface Bls12381Provider { String id(); @@ -51,14 +52,16 @@ default G2Point g2Negate(G2Point point) { G2Point g2ScalarMul(G2Point point, BigInteger scalar); /** - * Multiply a G1 point by a secret scalar using this provider's side-channel hardened path. + * Multiply a G1 point by a secret scalar using this provider's secret-scalar path. + * Consult the provider documentation for its side-channel guarantees. */ default G1Point g1SecretScalarMul(G1Point point, BigInteger scalar) { throw new UnsupportedOperationException(id() + " does not declare a secret-scalar G1 multiplication contract"); } /** - * Multiply a G2 point by a secret scalar using this provider's side-channel hardened path. + * Multiply a G2 point by a secret scalar using this provider's secret-scalar path. + * Consult the provider documentation for its side-channel guarantees. */ default G2Point g2SecretScalarMul(G2Point point, BigInteger scalar) { throw new UnsupportedOperationException(id() + " does not declare a secret-scalar G2 multiplication contract"); diff --git a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/PureJavaBls12381Provider.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/PureJavaBls12381Provider.java index d274d42..3c6f24e 100644 --- a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/PureJavaBls12381Provider.java +++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/PureJavaBls12381Provider.java @@ -11,6 +11,11 @@ /** * Default provider backed by ZeroJ's pure Java BLS12-381 implementation. + * + *This provider is correctness-first and portable. Its secret-scalar methods use the + * module's fixed-schedule Java ladders, but they do not provide a full JVM constant-time + * guarantee. Production secret-bearing workloads should prefer a native provider with an + * audited side-channel contract, such as {@code zeroj-blst}.
*/ public final class PureJavaBls12381Provider implements Bls12381Provider { public static final PureJavaBls12381Provider INSTANCE = new PureJavaBls12381Provider(); diff --git a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381CodecsTest.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381CodecsTest.java index 1f1d9eb..3a294ad 100644 --- a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381CodecsTest.java +++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381CodecsTest.java @@ -11,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.*; class Bls12381CodecsTest { + private static final int SORT_FLAG = 0x20; @Test void g1Uncompressed_roundTripsGenerator() { @@ -54,6 +55,34 @@ void g2Compressed_generatorMatchesZcashEncoding() { Bls12381Codecs.g2ToCompressed(Bls12381Generators.G2)); } + @Test + void g1Compressed_sortBitDistinguishesNegation() { + byte[] encoded = Bls12381Codecs.g1ToCompressed(Bls12381Generators.G1); + byte[] negated = Bls12381Codecs.g1ToCompressed(Bls12381Generators.G1.negate()); + + assertDiffersOnlyBySortBit(encoded, negated); + assertEquals(Bls12381Generators.G1, Bls12381Codecs.g1FromCompressed(encoded)); + assertEquals(Bls12381Generators.G1.negate(), Bls12381Codecs.g1FromCompressed(negated)); + + byte[] flipped = encoded.clone(); + flipped[0] ^= SORT_FLAG; + assertEquals(Bls12381Generators.G1.negate(), Bls12381Codecs.g1FromCompressed(flipped)); + } + + @Test + void g2Compressed_sortBitDistinguishesNegation() { + byte[] encoded = Bls12381Codecs.g2ToCompressed(Bls12381Generators.G2); + byte[] negated = Bls12381Codecs.g2ToCompressed(Bls12381Generators.G2.negate()); + + assertDiffersOnlyBySortBit(encoded, negated); + assertEquals(Bls12381Generators.G2, Bls12381Codecs.g2FromCompressed(encoded)); + assertEquals(Bls12381Generators.G2.negate(), Bls12381Codecs.g2FromCompressed(negated)); + + byte[] flipped = encoded.clone(); + flipped[0] ^= SORT_FLAG; + assertEquals(Bls12381Generators.G2.negate(), Bls12381Codecs.g2FromCompressed(flipped)); + } + @Test void infinity_roundTrips() { assertEquals(G1Point.INFINITY, Bls12381Codecs.g1FromUncompressed(Bls12381Codecs.g1ToUncompressed(G1Point.INFINITY))); @@ -132,4 +161,13 @@ private static byte[] hexToBytes(String hex) { } return out; } + + private static void assertDiffersOnlyBySortBit(byte[] left, byte[] right) { + assertEquals(SORT_FLAG, (left[0] ^ right[0]) & 0xff); + byte[] leftMasked = left.clone(); + byte[] rightMasked = right.clone(); + leftMasked[0] &= ~SORT_FLAG; + rightMasked[0] &= ~SORT_FLAG; + assertArrayEquals(leftMasked, rightMasked); + } } diff --git a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381HashToCurveTest.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381HashToCurveTest.java index 057b366..5106ead 100644 --- a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381HashToCurveTest.java +++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381HashToCurveTest.java @@ -66,6 +66,26 @@ void expandMessageXmd_usesOversizeDstReduction() { Bls12381Hash.expandMessageXmdSha256(msg, oversizedDst, 32)); } + @Test + void expandMessageXofShake256_matchesRfc9380Vector() { + byte[] msg = "abc".getBytes(StandardCharsets.US_ASCII); + byte[] dst = "QUUX-V01-CS02-with-expander-SHAKE256".getBytes(StandardCharsets.US_ASCII); + + assertArrayEquals( + hexToBytes("b39e493867e2767216792abce1f2676c197c0692aed061560ead251821808e07"), + Bls12381Hash.expandMessageXofShake256(msg, dst, 32)); + } + + @Test + void hashToScalarXofShake256_matchesOfficialBbsFixture() { + byte[] message = hexToBytes("9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02"); + byte[] dst = hexToBytes("4242535f424c53313233383147315f584f463a5348414b452d3235365f535357555f524f5f4832475f484d32535f4832535f"); + + assertEquals( + new BigInteger("0500031f786fde5326aa9370dd7ffe9535ec7a52cf2b8f432cad5d9acfb73cd3", 16), + Bls12381Hash.hashToScalarXofShake256(message, dst)); + } + @Test void hashToG2_matchesRfc9380EmptyMessageVector() { assertG2( @@ -125,4 +145,12 @@ private static byte[] concat(byte[] left, byte[] right) { System.arraycopy(right, 0, out, left.length, right.length); return out; } + + private static byte[] hexToBytes(String hex) { + byte[] out = new byte[hex.length() / 2]; + for (int i = 0; i < out.length; i++) { + out[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16); + } + return out; + } } diff --git a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG1BLS381Test.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG1BLS381Test.java index 71fbc77..c41231e 100644 --- a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG1BLS381Test.java +++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG1BLS381Test.java @@ -84,6 +84,14 @@ void ctScalarMul_byOrder_returnsInfinity() { assertTrue(JacobianG1BLS381.GENERATOR.ctScalarMul(R).isInfinity()); } + @Test + void ctScalarMul_rejectsScalarsAbove256Bits() { + BigInteger scalar = BigInteger.ONE.shiftLeft(256).add(BigInteger.ONE); + + assertThrows(IllegalArgumentException.class, + () -> JacobianG1BLS381.GENERATOR.ctScalarMul(scalar)); + } + @Test void crossValidate_withVerifierG1Point() { var g = JacobianG1BLS381.GENERATOR.toAffine(); diff --git a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381Test.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381Test.java index 707b66f..7c5175c 100644 --- a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381Test.java +++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381Test.java @@ -58,4 +58,21 @@ void scalarMul_small_isOnCurve() { assertTrue(p.toAffine().isOnCurve(), k + " * G2 must be on twist curve"); } } + + @Test + void ctScalarMul_matchesScalarMul() { + for (int k = 1; k <= 10; k++) { + var expected = JacobianG2BLS381.GENERATOR.scalarMul(BigInteger.valueOf(k)).toAffine(); + var actual = JacobianG2BLS381.GENERATOR.ctScalarMul(BigInteger.valueOf(k)).toAffine(); + assertEquals(expected, actual, "ctScalarMul(" + k + ") mismatch"); + } + } + + @Test + void ctScalarMul_rejectsScalarsAbove256Bits() { + BigInteger scalar = BigInteger.ONE.shiftLeft(256).add(BigInteger.ONE); + + assertThrows(IllegalArgumentException.class, + () -> JacobianG2BLS381.GENERATOR.ctScalarMul(scalar)); + } } diff --git a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp6Fp12Test.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp6Fp12Test.java new file mode 100644 index 0000000..1209ae1 --- /dev/null +++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp6Fp12Test.java @@ -0,0 +1,68 @@ +package com.bloxbean.cardano.zeroj.bls12381.field; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class Fp6Fp12Test { + + @Test + void fp6_squareMatchesMul() { + Fp6 a = fp6(42, 7, 3, 5, 11, 13); + + assertEquals(a.mul(a), a.square()); + } + + @Test + void fp6_inverseRoundTrip() { + Fp6 a = fp6(42, 7, 3, 5, 11, 13); + + assertEquals(Fp6.ONE, a.mul(a.inv())); + } + + @Test + void fp12_squareMatchesMul() { + Fp12 a = fp12( + fp6(42, 7, 3, 5, 11, 13), + fp6(17, 19, 23, 29, 31, 37)); + + assertEquals(a.mul(a), a.square()); + } + + @Test + void fp12_inverseRoundTrip() { + Fp12 a = fp12( + fp6(42, 7, 3, 5, 11, 13), + fp6(17, 19, 23, 29, 31, 37)); + + assertTrue(a.mul(a.inv()).isOne()); + } + + @Test + void fp12_distributesOverAddition() { + Fp12 a = fp12( + fp6(42, 7, 3, 5, 11, 13), + fp6(17, 19, 23, 29, 31, 37)); + Fp12 b = fp12( + fp6(41, 43, 47, 53, 59, 61), + fp6(67, 71, 73, 79, 83, 89)); + Fp12 c = fp12( + fp6(97, 101, 103, 107, 109, 113), + fp6(127, 131, 137, 139, 149, 151)); + + assertEquals(a.mul(b).add(a.mul(c)), a.mul(b.add(c))); + } + + private static Fp12 fp12(Fp6 c0, Fp6 c1) { + return new Fp12(c0, c1); + } + + private static Fp6 fp6(long c00, long c01, long c10, long c11, long c20, long c21) { + return new Fp6(fp2(c00, c01), fp2(c10, c11), fp2(c20, c21)); + } + + private static Fp2 fp2(long c0, long c1) { + return Fp2.of(Fp.of(c0), Fp.of(c1)); + } +} diff --git a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381Test.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381Test.java index 4e6f576..37c7b97 100644 --- a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381Test.java +++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381Test.java @@ -104,6 +104,28 @@ void boundary_modulusWraps() { assertTrue(MontFp381.fromBigInteger(P.add(BigInteger.ONE)).isOne()); } + @Test + void fromMontLimbs_acceptsCanonicalLimbs() { + long[] limbs = MontFp381.ONE.toLimbs(); + + assertTrue(MontFp381.fromMontLimbs( + limbs[0], limbs[1], limbs[2], limbs[3], limbs[4], limbs[5]).isOne()); + } + + @Test + void fromMontLimbs_rejectsNonCanonicalLimbs() { + assertThrows(IllegalArgumentException.class, + () -> MontFp381.fromMontLimbs( + MontFp381.MOD0, MontFp381.MOD1, MontFp381.MOD2, + MontFp381.MOD3, MontFp381.MOD4, MontFp381.MOD5)); + + assertThrows(IllegalArgumentException.class, + () -> MontFp381.fromMontLimbs( + -8988751357500304535L, 2419637729810828715L, + 8198777404514432018L, -5182139003232109836L, + 5314520057811676541L, -622504768958473957L)); + } + @RepeatedTest(100) void square_random_matchesMul() { BigInteger a = randomFp(); diff --git a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381Test.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381Test.java index 67c155e..009c536 100644 --- a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381Test.java +++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381Test.java @@ -152,6 +152,19 @@ void boundary_modulusWraps() { assertTrue(MontFr381.fromBigInteger(R.add(BigInteger.ONE)).isOne()); } + @Test + void fromMontLimbs_acceptsCanonicalLimbs() { + assertTrue(MontFr381.fromMontLimbs( + MontFr381.RONE0, MontFr381.RONE1, MontFr381.RONE2, MontFr381.RONE3).isOne()); + } + + @Test + void fromMontLimbs_rejectsNonCanonicalLimbs() { + assertThrows(IllegalArgumentException.class, + () -> MontFr381.fromMontLimbs( + MontFr381.MOD0, MontFr381.MOD1, MontFr381.MOD2, MontFr381.MOD3)); + } + private BigInteger randomFr() { byte[] bytes = new byte[32]; RNG.nextBytes(bytes); diff --git a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/pairing/BLS12381PairingTest.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/pairing/BLS12381PairingTest.java index ff02cd9..f6459fd 100644 --- a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/pairing/BLS12381PairingTest.java +++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/pairing/BLS12381PairingTest.java @@ -1,5 +1,6 @@ package com.bloxbean.cardano.zeroj.bls12381.pairing; +import com.bloxbean.cardano.zeroj.bls12381.Bls12381Generators; import com.bloxbean.cardano.zeroj.bls12381.ec.*; import com.bloxbean.cardano.zeroj.bls12381.field.*; import org.junit.jupiter.api.Test; @@ -10,14 +11,8 @@ class BLS12381PairingTest { - private static final G1Point G1 = new G1Point( - Fp.of(new BigInteger("17f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb", 16)), - Fp.of(new BigInteger("08b3f481e3aaa0f1a09e30ed741d8ae4fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1", 16))); - private static final G2Point G2 = new G2Point( - Fp2.of(Fp.of(new BigInteger("024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8", 16)), - Fp.of(new BigInteger("13e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e", 16))), - Fp2.of(Fp.of(new BigInteger("0ce5d527727d6e118cc9cdc6da2e351aadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801", 16)), - Fp.of(new BigInteger("0606c4a02ea734cc32acd2b02bc28b99cb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be", 16)))); + private static final G1Point G1 = Bls12381Generators.G1; + private static final G2Point G2 = Bls12381Generators.G2; @Test void cyclotomicPolynomialDivisibleByR() { @@ -71,4 +66,74 @@ void pairingCheck_ePlusNegE_isOne() { new G2Point[]{G2, G2}); assertTrue(result, "e(P,Q)*e(-P,Q) must be 1"); } + + @Test + void generatorPairing_isNonDegenerateAndHasOrderR() { + var e = generatorPairing(); + + assertFalse(e.isOne(), "e(G1,G2) must not be 1"); + assertTrue(e.pow(G1Point.R).isOne(), "e(G1,G2)^r must be 1"); + } + + @Test + void generatorPairing_matchesKnownAnswer() { + assertEquals(generatorPairingKat(), generatorPairing()); + } + + @Test + void pairing_isBilinearForNonTrivialScalars() { + var e = generatorPairing(); + var a = BigInteger.valueOf(5); + var b = BigInteger.valueOf(7); + + var eAg2 = pair(G1.scalarMul(a), G2); + var eG1b = pair(G1, G2.scalarMul(b)); + var eAg1b = pair(G1.scalarMul(a), G2.scalarMul(b)); + + assertEquals(e.pow(a), eAg2, "e([a]G1,G2) must equal e(G1,G2)^a"); + assertEquals(e.pow(b), eG1b, "e(G1,[b]G2) must equal e(G1,G2)^b"); + assertEquals(e.pow(a.multiply(b)), eAg1b, "e([a]G1,[b]G2) must equal e(G1,G2)^(ab)"); + } + + private static Fp12 generatorPairing() { + return pair(G1, G2); + } + + private static Fp12 pair(G1Point p, G2Point q) { + return BLS12381Pairing.finalExponentiation(BLS12381Pairing.millerLoop(p, q)); + } + + // Self-pinned regression vector in ZeroJ's Fp12 tower layout. Bilinearity, + // non-degeneracy, and e^r checks above provide the independent correctness gates. + // Replace or corroborate this with an external coefficient vector when a compatible + // blst/zkcrypto Fp12 serialization is available. + private static Fp12 generatorPairingKat() { + return fp12( + "11619b45f61edfe3b47a15fac19442526ff489dcda25e59121d9931438907dfd448299a87dde3a649bdba96e84d54558", + "153ce14a76a53e205ba8f275ef1137c56a566f638b52d34ba3bf3bf22f277d70f76316218c0dfd583a394b8448d2be7f", + "95668fb4a02fe930ed44767834c915b283b1c6ca98c047bd4c272e9ac3f3ba6ff0b05a93e59c71fba77bce995f04692", + "16deedaa683124fe7260085184d88f7d036b86f53bb5b7f1fc5e248814782065413e7d958d17960109ea006b2afdeb5f", + "9c92cf02f3cd3d2f9d34bc44eee0dd50314ed44ca5d30ce6a9ec0539be7a86b121edc61839ccc908c4bdde256cd6048", + "111061f398efc2a97ff825b04d21089e24fd8b93a47e41e60eae7e9b2a38d54fa4dedced0811c34ce528781ab9e929c7", + "1ecfcf31c86257ab00b4709c33f1c9c4e007659dd5ffc4a735192167ce197058cfb4c94225e7f1b6c26ad9ba68f63bc", + "8890726743a1f94a8193a166800b7787744a8ad8e2f9365db76863e894b7a11d83f90d873567e9d645ccf725b32d26f", + "e61c752414ca5dfd258e9606bac08daec29b3e2c57062669556954fb227d3f1260eedf25446a086b0844bcd43646c10", + "fe63f185f56dd29150fc498bbeea78969e7e783043620db33f75a05a0a2ce5c442beaff9da195ff15164c00ab66bdde", + "10900338a92ed0b47af211636f7cfdec717b7ee43900eee9b5fc24f0000c5874d4801372db478987691c566a8c474978", + "1454814f3085f0e6602247671bc408bbce2007201536818c901dbd4d2095dd86c1ec8b888e59611f60a301af7776be3d"); + } + + private static Fp12 fp12(String... c) { + return new Fp12( + new Fp6(fp2(c[0], c[1]), fp2(c[2], c[3]), fp2(c[4], c[5])), + new Fp6(fp2(c[6], c[7]), fp2(c[8], c[9]), fp2(c[10], c[11]))); + } + + private static Fp2 fp2(String c0, String c1) { + return Fp2.of(fp(c0), fp(c1)); + } + + private static Fp fp(String hex) { + return Fp.of(new BigInteger(hex, 16)); + } } diff --git a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/setup/SetupCacheTest.java b/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/setup/SetupCacheTest.java new file mode 100644 index 0000000..6d864c4 --- /dev/null +++ b/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/setup/SetupCacheTest.java @@ -0,0 +1,29 @@ +package com.bloxbean.cardano.zeroj.crypto.setup; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SetupCacheTest { + + @TempDir + Path tempDir; + + @Test + void srsCacheRoundTripsCanonicalBls381MontgomeryLimbs() throws Exception { + var srs = PowersOfTauBLS381.generate(4); + Path path = tempDir.resolve("srs.bin"); + + SetupCache.saveSrs(srs, path); + var loaded = SetupCache.loadSrs(path); + + assertEquals(srs.power(), loaded.power()); + assertEquals(srs.tauScalar(), loaded.tauScalar()); + assertArrayEquals(srs.tauG1(), loaded.tauG1()); + assertArrayEquals(srs.tauG2(), loaded.tauG2()); + } +}