From 32189d398fd55d1990197f2c76eba5858b19670e Mon Sep 17 00:00:00 2001 From: Alfonso Bribiesca Date: Thu, 5 Mar 2026 10:45:33 -0600 Subject: [PATCH 1/4] feat: add bls public key validation --- build.gradle | 1 + .../crypto/identities/BlsPublicKey.java | 27 ++++++++ .../builder/ValidatorRegistrationBuilder.java | 5 ++ .../crypto/identities/BlsPublicKeyTest.java | 61 +++++++++++++++++++ .../ValidatorRegistrationBuilderTest.java | 9 +++ 5 files changed, 103 insertions(+) create mode 100644 src/main/java/org/arkecosystem/crypto/identities/BlsPublicKey.java create mode 100644 src/test/java/org/arkecosystem/crypto/identities/BlsPublicKeyTest.java diff --git a/build.gradle b/build.gradle index 1bc811a1..edfb66a2 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,7 @@ dependencies { implementation 'org.bouncycastle:bcprov-jdk15on:1.70' implementation 'org.web3j:core:4.8.7' implementation 'com.google.code.gson:gson:2.11.0' + implementation 'foundation.icon:blst-java:0.3.2' // Test dependencies testImplementation 'org.hamcrest:hamcrest-library:3.0' diff --git a/src/main/java/org/arkecosystem/crypto/identities/BlsPublicKey.java b/src/main/java/org/arkecosystem/crypto/identities/BlsPublicKey.java new file mode 100644 index 00000000..cbfd6b07 --- /dev/null +++ b/src/main/java/org/arkecosystem/crypto/identities/BlsPublicKey.java @@ -0,0 +1,27 @@ +package org.arkecosystem.crypto.identities; + +import org.arkecosystem.crypto.encoding.Hex; +import supranational.blst.P1_Affine; + +public class BlsPublicKey { + + public static boolean validate(String publicKeyHex) { + try { + if (publicKeyHex.startsWith("0x")) { + publicKeyHex = publicKeyHex.substring(2); + } + + if (publicKeyHex.length() != 96) { + return false; + } + + byte[] publicKeyBytes = Hex.decode(publicKeyHex); + + P1_Affine point = new P1_Affine(publicKeyBytes); + + return point.in_group(); + } catch (Exception e) { + return false; + } + } +} diff --git a/src/main/java/org/arkecosystem/crypto/transactions/builder/ValidatorRegistrationBuilder.java b/src/main/java/org/arkecosystem/crypto/transactions/builder/ValidatorRegistrationBuilder.java index 01049455..34fbda5d 100644 --- a/src/main/java/org/arkecosystem/crypto/transactions/builder/ValidatorRegistrationBuilder.java +++ b/src/main/java/org/arkecosystem/crypto/transactions/builder/ValidatorRegistrationBuilder.java @@ -1,11 +1,16 @@ package org.arkecosystem.crypto.transactions.builder; +import org.arkecosystem.crypto.identities.BlsPublicKey; import org.arkecosystem.crypto.transactions.types.AbstractTransaction; import org.arkecosystem.crypto.transactions.types.ValidatorRegistration; public class ValidatorRegistrationBuilder extends AbstractTransactionBuilder { public ValidatorRegistrationBuilder validatorPublicKey(String validatorPublicKey) { + if (!BlsPublicKey.validate(validatorPublicKey)) { + throw new IllegalArgumentException("Invalid BLS public key"); + } + this.transaction.validatorPublicKey = validatorPublicKey; this.transaction.refreshPayloadData(); diff --git a/src/test/java/org/arkecosystem/crypto/identities/BlsPublicKeyTest.java b/src/test/java/org/arkecosystem/crypto/identities/BlsPublicKeyTest.java new file mode 100644 index 00000000..7b3db233 --- /dev/null +++ b/src/test/java/org/arkecosystem/crypto/identities/BlsPublicKeyTest.java @@ -0,0 +1,61 @@ +package org.arkecosystem.crypto.identities; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class BlsPublicKeyTest { + + @Test + public void validBlsPublicKey() { + assertTrue( + BlsPublicKey.validate( + "b4865127896c3c5286296a7b26e7c8002586a3ecf5832bfb59e689336f1f4c75e10491b9dfaed8dfb2c2fbe22d11fa93")); + } + + @Test + public void validBlsPublicKeyWith0xPrefix() { + assertTrue( + BlsPublicKey.validate( + "0xb4865127896c3c5286296a7b26e7c8002586a3ecf5832bfb59e689336f1f4c75e10491b9dfaed8dfb2c2fbe22d11fa93")); + } + + @Test + public void validG1GeneratorPoint() { + assertTrue( + BlsPublicKey.validate( + "97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb")); + } + + @Test + public void invalidLengthTooShort() { + assertFalse(BlsPublicKey.validate("b4865127896c3c5286296a7b26e7c800")); + } + + @Test + public void invalidLengthTooLong() { + assertFalse( + BlsPublicKey.validate( + "b4865127896c3c5286296a7b26e7c8002586a3ecf5832bfb59e689336f1f4c75e10491b9dfaed8dfb2c2fbe22d11fa9300")); + } + + @Test + public void invalidHexCharacters() { + assertFalse( + BlsPublicKey.validate( + "zz865127896c3c5286296a7b26e7c8002586a3ecf5832bfb59e689336f1f4c75e10491b9dfaed8dfb2c2fbe22d11fa93")); + } + + @Test + public void invalidPointNotOnCurve() { + assertFalse( + BlsPublicKey.validate( + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001")); + } + + @Test + public void emptyString() { + assertFalse(BlsPublicKey.validate("")); + } +} diff --git a/src/test/java/org/arkecosystem/crypto/transactions/builder/ValidatorRegistrationBuilderTest.java b/src/test/java/org/arkecosystem/crypto/transactions/builder/ValidatorRegistrationBuilderTest.java index 340fa7e5..5843dc44 100644 --- a/src/test/java/org/arkecosystem/crypto/transactions/builder/ValidatorRegistrationBuilderTest.java +++ b/src/test/java/org/arkecosystem/crypto/transactions/builder/ValidatorRegistrationBuilderTest.java @@ -33,4 +33,13 @@ public void it_should_sign_it_with_a_passphrase() throws Exception { assertEquals(data.get("id"), builder.transaction.getId()); assertTrue(builder.verify()); } + + @Test + public void it_should_throw_on_invalid_bls_public_key() { + assertThrows( + IllegalArgumentException.class, + () -> { + new ValidatorRegistrationBuilder().validatorPublicKey("invalid-bls-key"); + }); + } } From 739d4790d3cf335474f0c81f544c2c6405ae9a36 Mon Sep 17 00:00:00 2001 From: Alfonso Bribiesca Date: Thu, 5 Mar 2026 10:53:01 -0600 Subject: [PATCH 2/4] test: align bls tests with mainsail scenarios --- .../crypto/identities/BlsPublicKeyTest.java | 71 ++++++++----------- 1 file changed, 30 insertions(+), 41 deletions(-) diff --git a/src/test/java/org/arkecosystem/crypto/identities/BlsPublicKeyTest.java b/src/test/java/org/arkecosystem/crypto/identities/BlsPublicKeyTest.java index 7b3db233..669bf72f 100644 --- a/src/test/java/org/arkecosystem/crypto/identities/BlsPublicKeyTest.java +++ b/src/test/java/org/arkecosystem/crypto/identities/BlsPublicKeyTest.java @@ -4,14 +4,20 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; public class BlsPublicKeyTest { - @Test - public void validBlsPublicKey() { - assertTrue( - BlsPublicKey.validate( - "b4865127896c3c5286296a7b26e7c8002586a3ecf5832bfb59e689336f1f4c75e10491b9dfaed8dfb2c2fbe22d11fa93")); + @ParameterizedTest + @ValueSource( + strings = { + "b4865127896c3c5286296a7b26e7c8002586a3ecf5832bfb59e689336f1f4c75e10491b9dfaed8dfb2c2fbe22d11fa93", + "97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb", + "95af988701a6fb60e09da41d2ca1a9e0b49e43501bda4255b3ca01073f490c34102b6bbcafde6333185e9980745d72cb", + }) + public void validBlsPublicKeys(String key) { + assertTrue(BlsPublicKey.validate(key)); } @Test @@ -21,41 +27,24 @@ public void validBlsPublicKeyWith0xPrefix() { "0xb4865127896c3c5286296a7b26e7c8002586a3ecf5832bfb59e689336f1f4c75e10491b9dfaed8dfb2c2fbe22d11fa93")); } - @Test - public void validG1GeneratorPoint() { - assertTrue( - BlsPublicKey.validate( - "97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb")); - } - - @Test - public void invalidLengthTooShort() { - assertFalse(BlsPublicKey.validate("b4865127896c3c5286296a7b26e7c800")); - } - - @Test - public void invalidLengthTooLong() { - assertFalse( - BlsPublicKey.validate( - "b4865127896c3c5286296a7b26e7c8002586a3ecf5832bfb59e689336f1f4c75e10491b9dfaed8dfb2c2fbe22d11fa9300")); - } - - @Test - public void invalidHexCharacters() { - assertFalse( - BlsPublicKey.validate( - "zz865127896c3c5286296a7b26e7c8002586a3ecf5832bfb59e689336f1f4c75e10491b9dfaed8dfb2c2fbe22d11fa93")); - } - - @Test - public void invalidPointNotOnCurve() { - assertFalse( - BlsPublicKey.validate( - "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001")); - } - - @Test - public void emptyString() { - assertFalse(BlsPublicKey.validate("")); + @ParameterizedTest + @ValueSource( + strings = { + "", + "0", + "02b5Gf", + "NOT A VALID PUBLICKEY", + "000000000000000000000000000000000000000000000000000000000000000000", + "02b5Gf00d9de5a3ace28913fe78a15afcfe242926e94d9b517d06d2705b261f992", + "02e0f7449c5588f24492c338f2bc8f7865f755b958d48edb0f2d0056e50c3fd5b7", + "026f969d90fd494b04913eda9e0cf23f66eea5a70dfd5fb3e48f393397421c2b02", + "038c14b793cb19137e323a6d2e2a870bca2e7a493ec1153b3a95feb8a4873f8d08", + "32337416a26d8d49ec27059bd0589c49bb474029c3627715380f4df83fb431aece", + "22337416a26d8d49ec27059bd0589c49bb474029c3627715380f4df83fb431aece", + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + "b4865127896c3c5286296a7b26e7c8002586a3ecf5832bfb59e689336f1f4c75e10491b9dfaed8dfb2c2fbe22d11fa9300", + }) + public void invalidBlsPublicKeys(String key) { + assertFalse(BlsPublicKey.validate(key)); } } From ea4b93edfe399a542ca123165ed19cb561c1ad0b Mon Sep 17 00:00:00 2001 From: Alfonso Bribiesca Date: Thu, 5 Mar 2026 10:54:35 -0600 Subject: [PATCH 3/4] test: split bls tests into named scenarios --- .../crypto/identities/BlsPublicKey.java | 4 + .../crypto/identities/BlsPublicKeyTest.java | 128 ++++++++++++++---- 2 files changed, 102 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/arkecosystem/crypto/identities/BlsPublicKey.java b/src/main/java/org/arkecosystem/crypto/identities/BlsPublicKey.java index cbfd6b07..a8016129 100644 --- a/src/main/java/org/arkecosystem/crypto/identities/BlsPublicKey.java +++ b/src/main/java/org/arkecosystem/crypto/identities/BlsPublicKey.java @@ -7,6 +7,10 @@ public class BlsPublicKey { public static boolean validate(String publicKeyHex) { try { + if (publicKeyHex == null) { + return false; + } + if (publicKeyHex.startsWith("0x")) { publicKeyHex = publicKeyHex.substring(2); } diff --git a/src/test/java/org/arkecosystem/crypto/identities/BlsPublicKeyTest.java b/src/test/java/org/arkecosystem/crypto/identities/BlsPublicKeyTest.java index 669bf72f..98e096b5 100644 --- a/src/test/java/org/arkecosystem/crypto/identities/BlsPublicKeyTest.java +++ b/src/test/java/org/arkecosystem/crypto/identities/BlsPublicKeyTest.java @@ -4,20 +4,28 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; public class BlsPublicKeyTest { - @ParameterizedTest - @ValueSource( - strings = { - "b4865127896c3c5286296a7b26e7c8002586a3ecf5832bfb59e689336f1f4c75e10491b9dfaed8dfb2c2fbe22d11fa93", - "97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb", - "95af988701a6fb60e09da41d2ca1a9e0b49e43501bda4255b3ca01073f490c34102b6bbcafde6333185e9980745d72cb", - }) - public void validBlsPublicKeys(String key) { - assertTrue(BlsPublicKey.validate(key)); + @Test + public void validBlsPublicKeyFromMnemonic() { + assertTrue( + BlsPublicKey.validate( + "b4865127896c3c5286296a7b26e7c8002586a3ecf5832bfb59e689336f1f4c75e10491b9dfaed8dfb2c2fbe22d11fa93")); + } + + @Test + public void validG1GeneratorPoint() { + assertTrue( + BlsPublicKey.validate( + "97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb")); + } + + @Test + public void validSecondBlsPublicKey() { + assertTrue( + BlsPublicKey.validate( + "95af988701a6fb60e09da41d2ca1a9e0b49e43501bda4255b3ca01073f490c34102b6bbcafde6333185e9980745d72cb")); } @Test @@ -27,24 +35,84 @@ public void validBlsPublicKeyWith0xPrefix() { "0xb4865127896c3c5286296a7b26e7c8002586a3ecf5832bfb59e689336f1f4c75e10491b9dfaed8dfb2c2fbe22d11fa93")); } - @ParameterizedTest - @ValueSource( - strings = { - "", - "0", - "02b5Gf", - "NOT A VALID PUBLICKEY", - "000000000000000000000000000000000000000000000000000000000000000000", - "02b5Gf00d9de5a3ace28913fe78a15afcfe242926e94d9b517d06d2705b261f992", - "02e0f7449c5588f24492c338f2bc8f7865f755b958d48edb0f2d0056e50c3fd5b7", - "026f969d90fd494b04913eda9e0cf23f66eea5a70dfd5fb3e48f393397421c2b02", - "038c14b793cb19137e323a6d2e2a870bca2e7a493ec1153b3a95feb8a4873f8d08", - "32337416a26d8d49ec27059bd0589c49bb474029c3627715380f4df83fb431aece", - "22337416a26d8d49ec27059bd0589c49bb474029c3627715380f4df83fb431aece", - "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", - "b4865127896c3c5286296a7b26e7c8002586a3ecf5832bfb59e689336f1f4c75e10491b9dfaed8dfb2c2fbe22d11fa9300", - }) - public void invalidBlsPublicKeys(String key) { - assertFalse(BlsPublicKey.validate(key)); + @Test + public void emptyString() { + assertFalse(BlsPublicKey.validate("")); + } + + @Test + public void singleCharacter() { + assertFalse(BlsPublicKey.validate("0")); + } + + @Test + public void nonHexString() { + assertFalse(BlsPublicKey.validate("NOT A VALID PUBLICKEY")); + } + + @Test + public void invalidHexCharactersMixedWithValid() { + assertFalse(BlsPublicKey.validate("02b5Gf")); + } + + @Test + public void tooShort33BytesZeroPadded() { + assertFalse( + BlsPublicKey.validate( + "000000000000000000000000000000000000000000000000000000000000000000")); + } + + @Test + public void secp256k1CompressedPublicKeyPrefix02() { + assertFalse( + BlsPublicKey.validate( + "02e0f7449c5588f24492c338f2bc8f7865f755b958d48edb0f2d0056e50c3fd5b7")); + } + + @Test + public void secp256k1CompressedPublicKeyPrefix03() { + assertFalse( + BlsPublicKey.validate( + "038c14b793cb19137e323a6d2e2a870bca2e7a493ec1153b3a95feb8a4873f8d08")); + } + + @Test + public void secp256k1CompressedKeyWithInvalidHex() { + assertFalse( + BlsPublicKey.validate( + "02b5Gf00d9de5a3ace28913fe78a15afcfe242926e94d9b517d06d2705b261f992")); + } + + @Test + public void secp256k1UncompressedLengthKeyPrefix32() { + assertFalse( + BlsPublicKey.validate( + "32337416a26d8d49ec27059bd0589c49bb474029c3627715380f4df83fb431aece")); + } + + @Test + public void secp256k1UncompressedLengthKeyPrefix22() { + assertFalse( + BlsPublicKey.validate( + "22337416a26d8d49ec27059bd0589c49bb474029c3627715380f4df83fb431aece")); + } + + @Test + public void correctLengthButInvalidPoint() { + assertFalse( + BlsPublicKey.validate( + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001")); + } + + @Test + public void tooLongBy1Byte() { + assertFalse( + BlsPublicKey.validate( + "b4865127896c3c5286296a7b26e7c8002586a3ecf5832bfb59e689336f1f4c75e10491b9dfaed8dfb2c2fbe22d11fa9300")); + } + + @Test + public void nullInput() { + assertFalse(BlsPublicKey.validate(null)); } } From 5d19be185551b5b2e10804f4f91c29d0a857c2f1 Mon Sep 17 00:00:00 2001 From: Alfonso Bribiesca Date: Thu, 5 Mar 2026 12:15:45 -0600 Subject: [PATCH 4/4] chore: bump github actions to v4/v5 --- .github/workflows/test.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8cbb1b12..278eff06 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,10 +13,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Java 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '17' @@ -29,25 +29,25 @@ jobs: run: ./gradlew test jacocoTestReport - name: Upload Test Results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-results path: build/reports/tests/test - name: Upload Coverage Report - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: code-coverage-report path: build/reports/jacoco/test/html - name: Upload Jacoco XML Report - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: jacoco-xml-report path: build/reports/jacoco/test/jacocoTestReport.xml - name: Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} files: build/reports/jacoco/test/jacocoTestReport.xml