Skip to content
Draft
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 @@ -5,6 +5,7 @@ public enum AbiFunction {
UNVOTE("unvote"),
VALIDATOR_REGISTRATION("registerValidator"),
VALIDATOR_RESIGNATION("resignValidator"),
UPDATE_VALIDATOR("updateValidator"),
USERNAME_REGISTRATION("registerUsername"),
USERNAME_RESIGNATION("resignUsername"),
MULTIPAYMENT("pay"),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.arkecosystem.crypto.identities;

import java.math.BigInteger;
import java.util.Arrays;
import org.arkecosystem.crypto.configuration.Network;
import org.arkecosystem.crypto.encoding.Base58;
Expand Down Expand Up @@ -35,4 +36,44 @@ public static ECKey fromWif(String wif) {

return ECKey.fromPrivate(privateKeyBytes, true);
}

public static byte[] sign(byte[] message, String passphrase) {
return sign(message, fromPassphrase(passphrase));
}

public static byte[] sign(byte[] message, ECKey privateKey) {
ECKey.ECDSASignature signature = privateKey.sign(Sha256Hash.wrap(message));

int recId = -1;
for (int i = 0; i < 4; i++) {
ECKey k = ECKey.recoverFromSignature(i, signature, Sha256Hash.wrap(message), true);
if (k != null && k.getPubKeyPoint().equals(privateKey.getPubKeyPoint())) {
recId = i;
break;
}
}
if (recId == -1) {
throw new RuntimeException("Could not find recId");
}

byte[] rBytes = bigIntegerToBytes(signature.r, 32);
byte[] sBytes = bigIntegerToBytes(signature.s, 32);

byte[] signatureWithRecId = new byte[65];
System.arraycopy(rBytes, 0, signatureWithRecId, 0, 32);
System.arraycopy(sBytes, 0, signatureWithRecId, 32, 32);
signatureWithRecId[64] = (byte) recId;

return signatureWithRecId;
}

private static byte[] bigIntegerToBytes(BigInteger b, int numBytes) {
byte[] src = b.toByteArray();
byte[] dest = new byte[numBytes];
int srcPos = Math.max(0, src.length - numBytes);
int destPos = Math.max(0, numBytes - src.length);
int length = Math.min(src.length, numBytes);
System.arraycopy(src, srcPos, dest, destPos, length);
return dest;
}
}
22 changes: 22 additions & 0 deletions src/main/java/org/arkecosystem/crypto/identities/PublicKey.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package org.arkecosystem.crypto.identities;

import java.math.BigInteger;
import java.util.Arrays;
import org.arkecosystem.crypto.encoding.Hex;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Sha256Hash;

public class PublicKey {
public static String fromPassphrase(String passphrase) {
Expand All @@ -12,4 +15,23 @@ public static ECKey fromHex(String publicKey) {
ECKey key = ECKey.fromPublicOnly(Hex.decode(publicKey));
return ECKey.fromPublicOnly(key.getPubKeyPoint().getEncoded(true));
}

public static ECKey recover(byte[] message, byte[] signature) {
if (signature == null || signature.length != 65) {
throw new IllegalArgumentException(
"Signature must be a 65-byte compact signature (R || S || recId).");
}

byte recId = signature[64];
BigInteger r = new BigInteger(1, Arrays.copyOfRange(signature, 0, 32));
BigInteger s = new BigInteger(1, Arrays.copyOfRange(signature, 32, 64));

ECKey.ECDSASignature ecdsa = new ECKey.ECDSASignature(r, s);
ECKey recovered = ECKey.recoverFromSignature(recId, ecdsa, Sha256Hash.wrap(message), true);
if (recovered == null) {
throw new RuntimeException("Could not recover public key from signature.");
}

return recovered;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@

import com.google.gson.GsonBuilder;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.arkecosystem.crypto.encoding.Hex;
import org.arkecosystem.crypto.identities.Address;
import org.arkecosystem.crypto.identities.PrivateKey;
import org.arkecosystem.crypto.identities.PublicKey;
import org.arkecosystem.crypto.transactions.Serializer;
import org.arkecosystem.crypto.utils.AbiDecoder;
import org.arkecosystem.crypto.utils.TransactionUtils;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Sha256Hash;

public abstract class AbstractTransaction {
public int network;
Expand Down Expand Up @@ -123,69 +122,15 @@ public AbstractTransaction legacySecondSign(String secondPassphrase) {
}

private static String signHash(byte[] hash, ECKey privateKey) {
ECKey.ECDSASignature signature = privateKey.sign(Sha256Hash.wrap(hash));

int recId = -1;
for (int i = 0; i < 4; i++) {
ECKey k = ECKey.recoverFromSignature(i, signature, Sha256Hash.wrap(hash), true);
if (k != null && k.getPubKeyPoint().equals(privateKey.getPubKeyPoint())) {
recId = i;
break;
}
}
if (recId == -1) {
throw new RuntimeException("Could not find recId");
}

byte[] rBytes = bigIntegerToBytes(signature.r, 32);
byte[] sBytes = bigIntegerToBytes(signature.s, 32);

byte[] signatureBytes = new byte[64];
System.arraycopy(rBytes, 0, signatureBytes, 0, 32);
System.arraycopy(sBytes, 0, signatureBytes, 32, 32);

byte[] signatureWithRecId = new byte[65];
System.arraycopy(signatureBytes, 0, signatureWithRecId, 0, 64);
signatureWithRecId[64] = (byte) recId;

return Hex.encode(signatureWithRecId);
}

private static byte[] bigIntegerToBytes(BigInteger b, int numBytes) {
byte[] src = b.toByteArray();
byte[] dest = new byte[numBytes];
int srcPos = Math.max(0, src.length - numBytes);
int destPos = Math.max(0, numBytes - src.length);
int length = Math.min(src.length, numBytes);
System.arraycopy(src, srcPos, dest, destPos, length);
return dest;
return Hex.encode(PrivateKey.sign(hash, privateKey));
}

public void recoverSender() {
if (this.signature == null || this.signature.length() != 130) {
throw new RuntimeException("Invalid signature");
}

byte[] signatureWithRecId = Hex.decode(this.signature);
if (signatureWithRecId.length != 65) {
throw new RuntimeException("Invalid signature length");
}

byte recId = signatureWithRecId[64];
byte[] signatureBytes = Arrays.copyOfRange(signatureWithRecId, 0, 64);

BigInteger r = new BigInteger(1, Arrays.copyOfRange(signatureBytes, 0, 32));
BigInteger s = new BigInteger(1, Arrays.copyOfRange(signatureBytes, 32, 64));

ECKey.ECDSASignature signature = new ECKey.ECDSASignature(r, s);

byte[] hash = this.hash(true);

ECKey recoveredKey =
ECKey.recoverFromSignature(recId, signature, Sha256Hash.wrap(hash), true);
if (recoveredKey == null) {
throw new RuntimeException("Could not recover public key from signature");
}
ECKey recoveredKey = PublicKey.recover(this.hash(true), Hex.decode(this.signature));

this.senderPublicKey = recoveredKey.getPublicKeyAsHex();

Expand All @@ -199,27 +144,7 @@ public boolean verify() {
}

ECKey keys = ECKey.fromPublicOnly(Hex.decode(this.senderPublicKey));

byte[] signatureWithRecId = Hex.decode(this.signature);
if (signatureWithRecId.length != 65) {
return false;
}

byte recId = signatureWithRecId[64];
byte[] signatureBytes = Arrays.copyOfRange(signatureWithRecId, 0, 64);

BigInteger r = new BigInteger(1, Arrays.copyOfRange(signatureBytes, 0, 32));
BigInteger s = new BigInteger(1, Arrays.copyOfRange(signatureBytes, 32, 64));

ECKey.ECDSASignature signature = new ECKey.ECDSASignature(r, s);

byte[] hash = this.hash(true);

ECKey recoveredKey =
ECKey.recoverFromSignature(recId, signature, Sha256Hash.wrap(hash), true);
if (recoveredKey == null) {
return false;
}
ECKey recoveredKey = PublicKey.recover(this.hash(true), Hex.decode(this.signature));

return recoveredKey.getPubKeyPoint().equals(keys.getPubKeyPoint());
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.arkecosystem.crypto.utils;

import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.arkecosystem.crypto.enums.AbiFunction;
import org.arkecosystem.crypto.enums.ContractAbiType;

public final class TransactionEncoder {

private TransactionEncoder() {}

public static String multiPayment(List<String> recipients, List<BigInteger> amounts) {
return encode(
ContractAbiType.MULTIPAYMENT,
AbiFunction.MULTIPAYMENT,
Arrays.asList(recipients, amounts));
}

public static String tokenTransfer(String recipientAddress, BigInteger amount) {
return encode(
ContractAbiType.TOKEN,
AbiFunction.TRANSFER,
Arrays.asList(recipientAddress, amount));
}

public static String usernameRegistration(String username) {
return encode(
ContractAbiType.USERNAMES,
AbiFunction.USERNAME_REGISTRATION,
Collections.singletonList(username));
}

public static String usernameResignation() {
return encode(
ContractAbiType.USERNAMES,
AbiFunction.USERNAME_RESIGNATION,
Collections.emptyList());
}

public static String validatorRegistration(String validatorPublicKey) {
return encode(
ContractAbiType.CONSENSUS,
AbiFunction.VALIDATOR_REGISTRATION,
Collections.singletonList(addHexPrefix(validatorPublicKey)));
}

public static String validatorResignation() {
return encode(
ContractAbiType.CONSENSUS,
AbiFunction.VALIDATOR_RESIGNATION,
Collections.emptyList());
}

public static String updateValidator(String validatorPublicKey) {
return encode(
ContractAbiType.CONSENSUS,
AbiFunction.UPDATE_VALIDATOR,
Collections.singletonList(addHexPrefix(validatorPublicKey)));
}

public static String vote(String voteAddress) {
return encode(
ContractAbiType.CONSENSUS,
AbiFunction.VOTE,
Collections.singletonList(voteAddress));
}

public static String unvote() {
return encode(ContractAbiType.CONSENSUS, AbiFunction.UNVOTE, Collections.emptyList());
}

private static String encode(ContractAbiType type, AbiFunction function, List<Object> args) {
try {
return new AbiEncoder(type).encodeFunctionCall(function.toString(), args);
} catch (Exception e) {
throw new RuntimeException(
"Error encoding " + function + " against " + type + " ABI", e);
}
}

private static String addHexPrefix(String value) {
return value.startsWith("0x") ? value : "0x" + value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,31 @@ public void fromWif_round_trips_with_fromPassphrase() throws IOException {
assertEquals(fromPassphrase, fromWif);
}

@Test
public void sign_produces_a_65_byte_signature_recoverable_to_the_signer_public_key() {
byte[] message = org.bitcoinj.core.Sha256Hash.hash("payload".getBytes());

byte[] signature = PrivateKey.sign(message, "this is a top secret passphrase");

assertEquals(65, signature.length);
assertEquals(
"034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192",
PublicKey.recover(message, signature).getPublicKeyAsHex());
}

@Test
public void sign_with_eckey_overload_matches_passphrase_overload() {
byte[] message = org.bitcoinj.core.Sha256Hash.hash("payload".getBytes());
org.bitcoinj.core.ECKey key = PrivateKey.fromPassphrase("this is a top secret passphrase");

byte[] viaPassphrase = PrivateKey.sign(message, "this is a top secret passphrase");
byte[] viaEcKey = PrivateKey.sign(message, key);

assertEquals(
org.arkecosystem.crypto.encoding.Hex.encode(viaPassphrase),
org.arkecosystem.crypto.encoding.Hex.encode(viaEcKey));
}

@Test
public void fromWif_rejects_when_version_byte_belongs_to_another_network() throws IOException {
String mainnetWif;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,23 @@ public void fromHex_accepts_uncompressed_public_key() {

Assertions.assertEquals(compressed, actual);
}

@Test
public void recover_round_trips_with_PrivateKey_sign() {
byte[] message = org.bitcoinj.core.Sha256Hash.hash("Hello World".getBytes());
byte[] signature = PrivateKey.sign(message, "this is a top secret passphrase");

org.bitcoinj.core.ECKey recovered = PublicKey.recover(message, signature);

Assertions.assertEquals(
"034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192",
recovered.getPublicKeyAsHex());
}

@Test
public void recover_throws_on_invalid_signature_length() {
byte[] message = new byte[32];
Assertions.assertThrows(
IllegalArgumentException.class, () -> PublicKey.recover(message, new byte[64]));
}
}
Loading
Loading