diff --git a/build.gradle b/build.gradle index 6603baf..fea1eca 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,6 @@ dependencies { checkstyle 'com.puppycrawl.tools:checkstyle:10.4' // dependencies implementation 'org.bitcoinj:bitcoinj-core:0.16.3' - implementation 'net.i2p.crypto:eddsa:0.3.0' // test support dependencies testSupportImplementation 'org.testcontainers:testcontainers:1.19.8' diff --git a/src/main/java/com/lmax/solana4j/encoding/SolanaProgramDerivedAddress.java b/src/main/java/com/lmax/solana4j/encoding/SolanaProgramDerivedAddress.java index d7d738e..e63b0a7 100644 --- a/src/main/java/com/lmax/solana4j/encoding/SolanaProgramDerivedAddress.java +++ b/src/main/java/com/lmax/solana4j/encoding/SolanaProgramDerivedAddress.java @@ -2,7 +2,7 @@ import com.lmax.solana4j.api.ProgramDerivedAddress; import com.lmax.solana4j.api.PublicKey; -import net.i2p.crypto.eddsa.math.GroupElement; +import com.lmax.solana4j.util.Ed25519; import org.bitcoinj.core.Sha256Hash; import java.nio.ByteBuffer; @@ -12,7 +12,6 @@ import static com.lmax.solana4j.api.PublicKey.PUBLIC_KEY_LENGTH; import static java.util.Objects.requireNonNull; -import static net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable.ED_25519_CURVE_SPEC; final class SolanaProgramDerivedAddress implements ProgramDerivedAddress { @@ -53,8 +52,7 @@ private static boolean isOffCurve(final byte[] programAddress) { try { - final GroupElement point = ED_25519_CURVE_SPEC.getCurve().createPoint(programAddress, false); - return !point.isOnCurve(); + return !Ed25519.isOnCurve(programAddress); } catch (final IllegalArgumentException e) { diff --git a/src/main/java/com/lmax/solana4j/util/Ed25519.java b/src/main/java/com/lmax/solana4j/util/Ed25519.java new file mode 100644 index 0000000..6de3208 --- /dev/null +++ b/src/main/java/com/lmax/solana4j/util/Ed25519.java @@ -0,0 +1,87 @@ +package com.lmax.solana4j.util; + +import java.math.BigInteger; +import java.nio.ByteBuffer; + +public class Ed25519 +{ + private static final BigInteger P = new BigInteger("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed", 16); + private static final BigInteger D = new BigInteger("52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3", 16); + private static final BigInteger PM5D8 = P.subtract(BigInteger.valueOf(5)).divide(BigInteger.valueOf(8)); + + /** + * Checks if the public key represented by a 32-byte array is on the Ed25519 curve. + * + * @param publicKeyBytes The 32-byte array representing the public key. + * @return true if the point is on the curve, false otherwise. + */ + public static boolean isOnCurve(final byte[] publicKeyBytes) + { + if (publicKeyBytes.length != 32) + { + throw new IllegalArgumentException("Public key must be 32 bytes long"); + } + + // elliptic curve equation dx^2y^2 + x^2 = y^2 - 1 (mod P) + // let x^2 = u/v where u = y^2 - 1 & v = dy^2 + 1 (mod P) + byte[] yBytes = publicKeyBytes.clone(); + yBytes[31] &= 0x7F; + // publicKey is in little endian encoding, so we must reverse the byte array before we "math" + final BigInteger y = new BigInteger(1, reverseByteArray(yBytes)); + final BigInteger y2 = y.multiply(y).mod(P); + + final BigInteger u = y2.subtract(BigInteger.ONE).mod(P); + final BigInteger v = D.multiply(y2).add(BigInteger.ONE).mod(P); + + final BigInteger x1 = calculateCandidateRoot(u, v); + + return isCandidateRootValid(x1, v, u); + } + + private static BigInteger calculateCandidateRoot(final BigInteger u, final BigInteger v) + { + final BigInteger v3 = v.modPow(BigInteger.TEN, P).multiply(v).mod(P); + final BigInteger v7 = v3.multiply(v3).multiply(v).mod(P); + + final BigInteger uv3 = u.multiply(v3).mod(P); + + final BigInteger uv7 = u.multiply(v7).mod(P); + final BigInteger uv7Pow22523 = uv7.modPow(PM5D8, P); + + // after some transformations, given the properties of the e25519 curve we can say + // x^2 = (u/v) + // x1 = (u/v)^(P+3/8) where x1 is the candidate root + // x1 = uv^3(uv^7)^(P-5/8) + return uv3.multiply(uv7Pow22523).mod(P); + } + + private static boolean isCandidateRootValid(final BigInteger x1, final BigInteger v, final BigInteger u) + { + // given x1 = uv^3(uv^7)^(P-5/8) we can check to see if the candidate root satisfies + // vx^2 = u (mod P) for x = x1 is a candidate root + // vx^2 = -u (mod P) for x = x1 is a candidate root + // if no roots then the point does not lie on the curve + final BigInteger vx12 = v.multiply(x1.modPow(BigInteger.TWO, P)).mod(P); + + if (vx12.equals(u)) + { + return true; + } + + return vx12.equals(u.negate().mod(P)); + } + + private static byte[] reverseByteArray(final byte[] array) + { + final ByteBuffer buffer = ByteBuffer.allocate(array.length); + + for (int i = array.length - 1; i >= 0; i--) + { + buffer.put(array[i]); + } + + buffer.flip(); + + return buffer.array(); + } +} \ No newline at end of file diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 5f86b5f..27e14f1 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -8,7 +8,6 @@ */ module com.lmax.solana4j { requires org.bitcoinj.core; - requires net.i2p.crypto.eddsa; exports com.lmax.solana4j; exports com.lmax.solana4j.api;