Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: multisignature tests #182

Merged
merged 7 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.arkecosystem.crypto.encoding.Hex;
import org.arkecosystem.crypto.identities.PrivateKey;
import org.arkecosystem.crypto.signature.SchnorrSigner;
Expand Down Expand Up @@ -93,7 +91,7 @@ public boolean verify() {
byte[] signature = Hex.decode(this.signature);
byte[] hash = Sha256Hash.hash(Serializer.serialize(this, true, true, false));

return verifier(this.signature).verify(hash, keys, signature);
return verifier().verify(hash, keys, signature);
}

public boolean secondVerify(String secondPublicKey) {
Expand All @@ -102,48 +100,7 @@ public boolean secondVerify(String secondPublicKey) {
byte[] signature = Hex.decode(this.secondSignature);
byte[] hash = Sha256Hash.hash(Serializer.serialize(this, false, true, false));

return verifier(this.secondSignature).verify(hash, keys, signature);
}

public boolean multiVerify(int min, List<String> publicKeys) {
if (publicKeys.isEmpty()) {
throw new RuntimeException("The multi signature asset is invalid.");
}

byte[] hash = Sha256Hash.hash(Serializer.serialize(this, true, true, true));

Set<Integer> publicKeyIndexes = new HashSet<>();
int verifiedSignatures = 0;
boolean verified = false;
for (int i = 0; i < this.signatures.size(); i++) {
String signature = this.signatures.get(i);
int publicKeyIndex = Integer.parseInt(signature.substring(0, 2), 16);

if (!publicKeyIndexes.contains(publicKeyIndex)) {
publicKeyIndexes.add(publicKeyIndex);
} else {
throw new RuntimeException("Duplicate participant in multi signature");
}

String partialSignature = signature.substring(2);
String publicKey = publicKeys.get(publicKeyIndex);

if (verifier(partialSignature)
.verify(
hash,
ECKey.fromPublicOnly(Hex.decode(publicKey)),
Hex.decode(partialSignature))) {
verifiedSignatures++;
}

if (verifiedSignatures == min) {
verified = true;
break;
} else if (signatures.size() - (i + 1 - verifiedSignatures) < min) {
break;
}
}
return verified;
return verifier().verify(hash, keys, signature);
}

public String toJson() {
Expand Down Expand Up @@ -206,7 +163,7 @@ private Signer signer() {
return new SchnorrSigner();
}

private Verifier verifier(String signature) {
private Verifier verifier() {
return new SchnorrVerifier();
}
}
Original file line number Diff line number Diff line change
@@ -1,60 +1,31 @@
package org.arkecosystem.crypto.signature;

import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.google.gson.internal.LinkedTreeMap;
import java.util.Arrays;
import java.util.List;
import org.arkecosystem.crypto.encoding.Hex;
import org.arkecosystem.crypto.identities.PublicKey;
import org.arkecosystem.crypto.transactions.Deserializer;
import org.arkecosystem.crypto.transactions.FixtureLoader;
import org.arkecosystem.crypto.transactions.Serializer;
import org.arkecosystem.crypto.transactions.types.Transaction;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

public class FixtureSignVerificationTest {

private final String passphrase = "my super secret passphrase";
private final String secondPassphrase = "this is a top secret second passphrase";
private final String musigPassphrase1 =
"album pony urban cheap small blade cannon silent run reveal luxury glad predict excess fire beauty hollow reward solar egg exclude leaf sight degree";
private final String musigPassphrase2 =
"hen slogan retire boss upset blame rocket slender area arch broom bring elder few milk bounce execute page evoke once inmate pear marine deliver";
private final String musigPassphrase3 =
"top visa use bacon sun infant shrimp eye bridge fantasy chair sadness stable simple salad canoe raw hill target connect avoid promote spider category";

@ParameterizedTest
@ValueSource(
strings = {
"transactions/transfer/transfer-sign",
"transactions/transfer/transfer-with-vendor-field-sign",
// "transactions/transfer/transfer-multi-sign",

"transactions/vote/vote-sign",
"transactions/vote/unvote-sign",
// "transactions/vote/vote-multi-sign",

"transactions/validator_registration/validator-registration-sign",
// "transactions/validator_registration/validator-registration-multi-sign",

"transactions/validator_resignation/validator-resignation-sign",
// "transactions/validator_resignation/validator-resignation-multi-sign",

"transactions/multi_payment/multi-payment-sign",
"transactions/multi_payment/multi-payment-with-vendor-field-sign",
// "transactions/multi_payment/multi-payment-multi-sign",

"transactions/username_resignation/username-resignation-sign",
// "transactions/username_resignation/username-resignation-multi-sign",

"transactions/username_registration/username-registration-sign",
// "transactions/username_registration/username-registration-multi-sign",

"transactions/multi_signature_registration/multi-signature-registration-sign",
})
void checkSchnorrSignature(String file) {
Expand All @@ -67,117 +38,10 @@ void checkSchnorrSignature(String file) {
if (actual.secondSignature != null) {
checkSecondSignature(actual);
}

if (actual.signatures != null) {
checkMultiSignature(actual);
}
}

@ParameterizedTest
@ValueSource(
strings = {
"transactions/transfer/transfer-sign",
"transactions/transfer/transfer-with-vendor-field-sign",
// "transactions/transfer/transfer-multi-sign",

"transactions/vote/vote-sign",
"transactions/vote/unvote-sign",
// "transactions/vote/vote-multi-sign",

"transactions/validator_registration/validator-registration-sign",
// "transactions/validator_registration/validator-registration-multi-sign",

"transactions/validator_resignation/validator-resignation-sign",
// "transactions/validator_resignation/validator-resignation-multi-sign",

"transactions/multi_payment/multi-payment-sign",
"transactions/multi_payment/multi-payment-with-vendor-field-sign",
// "transactions/multi_payment/multi-payment-multi-sign",

"transactions/username_resignation/username-resignation-sign",
// "transactions/username_resignation/username-resignation-multi-sign",

"transactions/username_registration/username-registration-sign",
// "transactions/username_registration/username-registration-multi-sign",

// "transactions/multi_signature_registration/multi-signature-registration-sign",
})
void checkSigningAgainProducesSameSignature(String file) {
LinkedTreeMap<String, Object> fixture = FixtureLoader.load(file);

Transaction actual = new Deserializer(fixture.get("serialized").toString()).deserialize();

// Remove the signatures from original transaction
Transaction withoutSignatures =
new Deserializer(Hex.encode(Serializer.serialize(actual, true, true, true)))
.deserialize();

// Ensure only the signatures were removed
assertThat(
fixture.get("serialized").toString(),
startsWith(Hex.encode(Serializer.serialize(withoutSignatures))));

reSignUnsigned(actual, withoutSignatures);

if (withoutSignatures.signature != null) {
assertTrue(withoutSignatures.verify());
}

if (withoutSignatures.secondSignature != null) {
checkSecondSignature(withoutSignatures);
}

int signatureLength = 128;

if (withoutSignatures.signatures != null) {
checkMultiSignature(withoutSignatures);

signatureLength = 128 + (withoutSignatures.signatures.size() * 130);
}

String serializedWithoutSignatures = Hex.encode(Serializer.serialize(withoutSignatures));
String serializedFixture = fixture.get("serialized").toString();

// Exclude the last 128 characters (signature) for final comparison
assertThat(
serializedWithoutSignatures.substring(
0, serializedWithoutSignatures.length() - signatureLength),
is(serializedFixture.substring(0, serializedFixture.length() - signatureLength)));
}

private void reSignUnsigned(Transaction actual, Transaction withoutSignatures) {
if (actual.signatures != null) {
int i = 0;
for (String passphrase :
Arrays.asList(musigPassphrase1, musigPassphrase2, musigPassphrase3)) {
withoutSignatures.multiSign(passphrase, i++);
}
if (actual.signature != null) {
withoutSignatures.sign(musigPassphrase1);
}
if (actual.secondSignature != null) {
withoutSignatures.secondSign(secondPassphrase);
}
} else if (actual.secondSignature != null) {
withoutSignatures.sign(passphrase);
withoutSignatures.secondSign(secondPassphrase);
} else if (actual.signature != null) {
withoutSignatures.sign(passphrase);
}
}

private void checkSecondSignature(Transaction actual) {
String secondPublicKey = PublicKey.fromPassphrase(secondPassphrase);
assertTrue(actual.secondVerify(secondPublicKey));
}

private void checkMultiSignature(Transaction actual) {
String key1 = PublicKey.fromPassphrase(musigPassphrase1);
String key2 = PublicKey.fromPassphrase(musigPassphrase2);
String key3 = PublicKey.fromPassphrase(musigPassphrase3);

List<String> publicKeys = Arrays.asList(key1, key2, key3);

assertTrue(actual.multiVerify(2, publicKeys));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.arkecosystem.crypto.transactions.types.Transaction;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -59,4 +60,29 @@ void testMaxPayments() {
() -> actual.addPayment("0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A", 1));
assertEquals("Expected a maximum of 64 payments", exception.getMessage());
}

@Test
void buildMultiSignature() {
Transaction actual =
new MultiPaymentBuilder()
.addPayment("0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A", 1)
.addPayment("0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A", 2)
.addPayment("0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A", 3)
.vendorField("This is a transaction from Java")
.multiSign("secret 1", 0)
.multiSign("secret 2", 1)
.multiSign("secret 3", 2)
.sign("this is a top secret passphrase")
.transaction;

assertTrue(actual.verify());

HashMap actualHashMap = actual.toHashMap();

assertNotNull(actualHashMap.get("signatures"));

List<String> actualSignatures = (List<String>) actualHashMap.get("signatures");

assertEquals(3, actualSignatures.size());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@
import static org.junit.jupiter.api.Assertions.*;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import org.arkecosystem.crypto.enums.Fees;
import org.arkecosystem.crypto.identities.PublicKey;
import org.arkecosystem.crypto.transactions.Deserializer;
import org.arkecosystem.crypto.transactions.types.Transaction;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class MultiSignatureRegistrationBuilderTest {
Expand Down Expand Up @@ -38,7 +35,6 @@ void build() {
.transaction;

assertTrue(actual.verify());
assertTrue(actual.multiVerify(3, publicKeys));

HashMap actualHashMap = actual.toHashMap();

Expand All @@ -54,32 +50,4 @@ void build() {
assertEquals(publicKeys, actualPublicKeys);
assertEquals(3, actualMin);
}

@Test
void checkMultiSignaturePassingInvalidMultiSignatureAsset() {
Exception thrown =
Assertions.assertThrows(
RuntimeException.class,
() -> {
Deserializer deserializer =
new Deserializer(
"ff011e0100000004000200000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d30065cd1d00000000000203029fab3cb2f5e248ae7cbb4de646741da4d73c493b2a03ab5c71507fb2c0dcca9203629f9dbf7f1e91cefa845126189816ceae357bdd1f41bd14787318a7d5b55d48027941d2059f89a26d89e87d3385e261a0ede1234aaeaa487012b69d6b67962dc52b33558cdc62933ff56feb646d1f47a98104bf34b894895d5e816f86e556f87fce8485e55aa32dfa1cd86456a66a58ef7a68dff4af51e2f7fcf75b983540872e000caa6864c71362b369c71107f463f29c43c361e54260cbc54d791b7385dbe76f29d12b9befbe4f98792d7046481afcf0c156408310192a93d8413e5380438f270153a22c5ce2b1894f0a141adc19de567077d2d268f4c7e1476e9558ecb4411d486cd0d7aa3e9c7716739a055a8a1a64a162d0362645d63d13791a9876ef8b5a88021d56997f0c9e21201c59e1b7b6be8d5a609908a4f65bf266144baf5e61e3f14bc2651f62187f4ffa17c8b010fcb0fba94a04df56bc25e5cb20935ec5fb7ad632");
Transaction actual = deserializer.deserialize();
assertTrue(actual.multiVerify(3, Collections.emptyList()));
});
assertEquals("The multi signature asset is invalid.", thrown.getMessage());
}

@Test
void checkMultiSignaturePassingInvalidMultiSignatureAssets() {
String key1 = PublicKey.fromPassphrase("this is a top secret passphrase 1");
String key2 = PublicKey.fromPassphrase("this is a top secret passphrase 2");
String key3 = PublicKey.fromPassphrase("this is a top secret passphrase 3");

Deserializer deserializer =
new Deserializer(
"ff011e0100000004000200000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d30065cd1d00000000000203029fab3cb2f5e248ae7cbb4de646741da4d73c493b2a03ab5c71507fb2c0dcca9203629f9dbf7f1e91cefa845126189816ceae357bdd1f41bd14787318a7d5b55d48027941d2059f89a26d89e87d3385e261a0ede1234aaeaa487012b69d6b67962dc52b33558cdc62933ff56feb646d1f47a98104bf34b894895d5e816f86e556f87fce8485e55aa32dfa1cd86456a66a58ef7a68dff4af51e2f7fcf75b983540872e000caa6864c71362b369c71107f463f29c43c361e54260cbc54d791b7385dbe76f29d12b9befbe4f98792d7046481afcf0c156408310192a93d8413e5380438f270153a22c5ce2b1894f0a141adc19de567077d2d268f4c7e1476e9558ecb4411d486cd0d7aa3e9c7716739a055a8a1a64a162d0362645d63d13791a9876ef8b5a88021d56997f0c9e21201c59e1b7b6be8d5a609908a4f65bf266144baf5e61e3f14bc2651f62187f4ffa17c8b010fcb0fba94a04df56bc25e5cb20935ec5fb7ad632");
Transaction actual = deserializer.deserialize();
assertFalse(actual.multiVerify(3, Arrays.asList(key1, key2, key3)));
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package org.arkecosystem.crypto.transactions.builder;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.HashMap;
import java.util.List;
import org.arkecosystem.crypto.enums.Fees;
import org.arkecosystem.crypto.transactions.types.Transaction;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -62,4 +64,31 @@ void buildSecondSignature() {
HashMap actualHashMap = actual.toHashMap();
assertEquals(actualHashMap.get("secondSignature"), actual.secondSignature);
}

@Test
void buildMultiSignature() {
Transaction actual =
new TransferBuilder()
.recipient("0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A")
.amount(133380000000L)
.expiration(100000)
.vendorField("This is a transaction from Java")
.nonce(3)
.fee(Fees.TRANSFER.getValue())
.multiSign("secret 1", 0)
.multiSign("secret 2", 1)
.multiSign("secret 3", 2)
.sign("secret 1")
.transaction;

assertTrue(actual.verify());

HashMap actualHashMap = actual.toHashMap();

assertNotNull(actualHashMap.get("signatures"));

List<String> actualSignatures = (List<String>) actualHashMap.get("signatures");

assertEquals(3, actualSignatures.size());
}
}
Loading
Loading