Skip to content

Commit 621583e

Browse files
authored
feat: add transaction encoder utility and key sign/recover helpers (#205)
1 parent 3684273 commit 621583e

8 files changed

Lines changed: 314 additions & 79 deletions

File tree

src/main/java/org/arkecosystem/crypto/enums/AbiFunction.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ public enum AbiFunction {
55
UNVOTE("unvote"),
66
VALIDATOR_REGISTRATION("registerValidator"),
77
VALIDATOR_RESIGNATION("resignValidator"),
8+
UPDATE_VALIDATOR("updateValidator"),
89
USERNAME_REGISTRATION("registerUsername"),
910
USERNAME_RESIGNATION("resignUsername"),
1011
MULTIPAYMENT("pay"),

src/main/java/org/arkecosystem/crypto/identities/PrivateKey.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.arkecosystem.crypto.identities;
22

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

3637
return ECKey.fromPrivate(privateKeyBytes, true);
3738
}
39+
40+
public static byte[] sign(byte[] message, String passphrase) {
41+
return sign(message, fromPassphrase(passphrase));
42+
}
43+
44+
public static byte[] sign(byte[] message, ECKey privateKey) {
45+
ECKey.ECDSASignature signature = privateKey.sign(Sha256Hash.wrap(message));
46+
47+
int recId = -1;
48+
for (int i = 0; i < 4; i++) {
49+
ECKey k = ECKey.recoverFromSignature(i, signature, Sha256Hash.wrap(message), true);
50+
if (k != null && k.getPubKeyPoint().equals(privateKey.getPubKeyPoint())) {
51+
recId = i;
52+
break;
53+
}
54+
}
55+
if (recId == -1) {
56+
throw new RuntimeException("Could not find recId");
57+
}
58+
59+
byte[] rBytes = bigIntegerToBytes(signature.r, 32);
60+
byte[] sBytes = bigIntegerToBytes(signature.s, 32);
61+
62+
byte[] signatureWithRecId = new byte[65];
63+
System.arraycopy(rBytes, 0, signatureWithRecId, 0, 32);
64+
System.arraycopy(sBytes, 0, signatureWithRecId, 32, 32);
65+
signatureWithRecId[64] = (byte) recId;
66+
67+
return signatureWithRecId;
68+
}
69+
70+
private static byte[] bigIntegerToBytes(BigInteger b, int numBytes) {
71+
byte[] src = b.toByteArray();
72+
byte[] dest = new byte[numBytes];
73+
int srcPos = Math.max(0, src.length - numBytes);
74+
int destPos = Math.max(0, numBytes - src.length);
75+
int length = Math.min(src.length, numBytes);
76+
System.arraycopy(src, srcPos, dest, destPos, length);
77+
return dest;
78+
}
3879
}

src/main/java/org/arkecosystem/crypto/identities/PublicKey.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package org.arkecosystem.crypto.identities;
22

3+
import java.math.BigInteger;
4+
import java.util.Arrays;
35
import org.arkecosystem.crypto.encoding.Hex;
46
import org.bitcoinj.core.ECKey;
7+
import org.bitcoinj.core.Sha256Hash;
58

69
public class PublicKey {
710
public static String fromPassphrase(String passphrase) {
@@ -12,4 +15,23 @@ public static ECKey fromHex(String publicKey) {
1215
ECKey key = ECKey.fromPublicOnly(Hex.decode(publicKey));
1316
return ECKey.fromPublicOnly(key.getPubKeyPoint().getEncoded(true));
1417
}
18+
19+
public static ECKey recover(byte[] message, byte[] signature) {
20+
if (signature == null || signature.length != 65) {
21+
throw new IllegalArgumentException(
22+
"Signature must be a 65-byte compact signature (R || S || recId).");
23+
}
24+
25+
byte recId = signature[64];
26+
BigInteger r = new BigInteger(1, Arrays.copyOfRange(signature, 0, 32));
27+
BigInteger s = new BigInteger(1, Arrays.copyOfRange(signature, 32, 64));
28+
29+
ECKey.ECDSASignature ecdsa = new ECKey.ECDSASignature(r, s);
30+
ECKey recovered = ECKey.recoverFromSignature(recId, ecdsa, Sha256Hash.wrap(message), true);
31+
if (recovered == null) {
32+
throw new RuntimeException("Could not recover public key from signature.");
33+
}
34+
35+
return recovered;
36+
}
1537
}

src/main/java/org/arkecosystem/crypto/transactions/types/AbstractTransaction.java

Lines changed: 4 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@
22

33
import com.google.gson.GsonBuilder;
44
import java.math.BigInteger;
5-
import java.util.Arrays;
65
import java.util.HashMap;
76
import java.util.List;
87
import java.util.Map;
98
import org.arkecosystem.crypto.encoding.Hex;
109
import org.arkecosystem.crypto.identities.Address;
1110
import org.arkecosystem.crypto.identities.PrivateKey;
11+
import org.arkecosystem.crypto.identities.PublicKey;
1212
import org.arkecosystem.crypto.transactions.Serializer;
1313
import org.arkecosystem.crypto.utils.AbiDecoder;
1414
import org.arkecosystem.crypto.utils.TransactionUtils;
1515
import org.bitcoinj.core.ECKey;
16-
import org.bitcoinj.core.Sha256Hash;
1716

1817
public abstract class AbstractTransaction {
1918
public int network;
@@ -123,69 +122,15 @@ public AbstractTransaction legacySecondSign(String secondPassphrase) {
123122
}
124123

125124
private static String signHash(byte[] hash, ECKey privateKey) {
126-
ECKey.ECDSASignature signature = privateKey.sign(Sha256Hash.wrap(hash));
127-
128-
int recId = -1;
129-
for (int i = 0; i < 4; i++) {
130-
ECKey k = ECKey.recoverFromSignature(i, signature, Sha256Hash.wrap(hash), true);
131-
if (k != null && k.getPubKeyPoint().equals(privateKey.getPubKeyPoint())) {
132-
recId = i;
133-
break;
134-
}
135-
}
136-
if (recId == -1) {
137-
throw new RuntimeException("Could not find recId");
138-
}
139-
140-
byte[] rBytes = bigIntegerToBytes(signature.r, 32);
141-
byte[] sBytes = bigIntegerToBytes(signature.s, 32);
142-
143-
byte[] signatureBytes = new byte[64];
144-
System.arraycopy(rBytes, 0, signatureBytes, 0, 32);
145-
System.arraycopy(sBytes, 0, signatureBytes, 32, 32);
146-
147-
byte[] signatureWithRecId = new byte[65];
148-
System.arraycopy(signatureBytes, 0, signatureWithRecId, 0, 64);
149-
signatureWithRecId[64] = (byte) recId;
150-
151-
return Hex.encode(signatureWithRecId);
152-
}
153-
154-
private static byte[] bigIntegerToBytes(BigInteger b, int numBytes) {
155-
byte[] src = b.toByteArray();
156-
byte[] dest = new byte[numBytes];
157-
int srcPos = Math.max(0, src.length - numBytes);
158-
int destPos = Math.max(0, numBytes - src.length);
159-
int length = Math.min(src.length, numBytes);
160-
System.arraycopy(src, srcPos, dest, destPos, length);
161-
return dest;
125+
return Hex.encode(PrivateKey.sign(hash, privateKey));
162126
}
163127

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

169-
byte[] signatureWithRecId = Hex.decode(this.signature);
170-
if (signatureWithRecId.length != 65) {
171-
throw new RuntimeException("Invalid signature length");
172-
}
173-
174-
byte recId = signatureWithRecId[64];
175-
byte[] signatureBytes = Arrays.copyOfRange(signatureWithRecId, 0, 64);
176-
177-
BigInteger r = new BigInteger(1, Arrays.copyOfRange(signatureBytes, 0, 32));
178-
BigInteger s = new BigInteger(1, Arrays.copyOfRange(signatureBytes, 32, 64));
179-
180-
ECKey.ECDSASignature signature = new ECKey.ECDSASignature(r, s);
181-
182-
byte[] hash = this.hash(true);
183-
184-
ECKey recoveredKey =
185-
ECKey.recoverFromSignature(recId, signature, Sha256Hash.wrap(hash), true);
186-
if (recoveredKey == null) {
187-
throw new RuntimeException("Could not recover public key from signature");
188-
}
133+
ECKey recoveredKey = PublicKey.recover(this.hash(true), Hex.decode(this.signature));
189134

190135
this.senderPublicKey = recoveredKey.getPublicKeyAsHex();
191136

@@ -199,27 +144,7 @@ public boolean verify() {
199144
}
200145

201146
ECKey keys = ECKey.fromPublicOnly(Hex.decode(this.senderPublicKey));
202-
203-
byte[] signatureWithRecId = Hex.decode(this.signature);
204-
if (signatureWithRecId.length != 65) {
205-
return false;
206-
}
207-
208-
byte recId = signatureWithRecId[64];
209-
byte[] signatureBytes = Arrays.copyOfRange(signatureWithRecId, 0, 64);
210-
211-
BigInteger r = new BigInteger(1, Arrays.copyOfRange(signatureBytes, 0, 32));
212-
BigInteger s = new BigInteger(1, Arrays.copyOfRange(signatureBytes, 32, 64));
213-
214-
ECKey.ECDSASignature signature = new ECKey.ECDSASignature(r, s);
215-
216-
byte[] hash = this.hash(true);
217-
218-
ECKey recoveredKey =
219-
ECKey.recoverFromSignature(recId, signature, Sha256Hash.wrap(hash), true);
220-
if (recoveredKey == null) {
221-
return false;
222-
}
147+
ECKey recoveredKey = PublicKey.recover(this.hash(true), Hex.decode(this.signature));
223148

224149
return recoveredKey.getPubKeyPoint().equals(keys.getPubKeyPoint());
225150
} catch (Exception e) {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package org.arkecosystem.crypto.utils;
2+
3+
import java.math.BigInteger;
4+
import java.util.Arrays;
5+
import java.util.Collections;
6+
import java.util.List;
7+
import org.arkecosystem.crypto.enums.AbiFunction;
8+
import org.arkecosystem.crypto.enums.ContractAbiType;
9+
10+
public final class TransactionEncoder {
11+
12+
private TransactionEncoder() {}
13+
14+
public static String multiPayment(List<String> recipients, List<BigInteger> amounts) {
15+
return encode(
16+
ContractAbiType.MULTIPAYMENT,
17+
AbiFunction.MULTIPAYMENT,
18+
Arrays.asList(recipients, amounts));
19+
}
20+
21+
public static String tokenTransfer(String recipientAddress, BigInteger amount) {
22+
return encode(
23+
ContractAbiType.TOKEN,
24+
AbiFunction.TRANSFER,
25+
Arrays.asList(recipientAddress, amount));
26+
}
27+
28+
public static String usernameRegistration(String username) {
29+
return encode(
30+
ContractAbiType.USERNAMES,
31+
AbiFunction.USERNAME_REGISTRATION,
32+
Collections.singletonList(username));
33+
}
34+
35+
public static String usernameResignation() {
36+
return encode(
37+
ContractAbiType.USERNAMES,
38+
AbiFunction.USERNAME_RESIGNATION,
39+
Collections.emptyList());
40+
}
41+
42+
public static String validatorRegistration(String validatorPublicKey) {
43+
return encode(
44+
ContractAbiType.CONSENSUS,
45+
AbiFunction.VALIDATOR_REGISTRATION,
46+
Collections.singletonList(addHexPrefix(validatorPublicKey)));
47+
}
48+
49+
public static String validatorResignation() {
50+
return encode(
51+
ContractAbiType.CONSENSUS,
52+
AbiFunction.VALIDATOR_RESIGNATION,
53+
Collections.emptyList());
54+
}
55+
56+
public static String updateValidator(String validatorPublicKey) {
57+
return encode(
58+
ContractAbiType.CONSENSUS,
59+
AbiFunction.UPDATE_VALIDATOR,
60+
Collections.singletonList(addHexPrefix(validatorPublicKey)));
61+
}
62+
63+
public static String vote(String voteAddress) {
64+
return encode(
65+
ContractAbiType.CONSENSUS,
66+
AbiFunction.VOTE,
67+
Collections.singletonList(voteAddress));
68+
}
69+
70+
public static String unvote() {
71+
return encode(ContractAbiType.CONSENSUS, AbiFunction.UNVOTE, Collections.emptyList());
72+
}
73+
74+
private static String encode(ContractAbiType type, AbiFunction function, List<Object> args) {
75+
try {
76+
return new AbiEncoder(type).encodeFunctionCall(function.toString(), args);
77+
} catch (Exception e) {
78+
throw new RuntimeException(
79+
"Error encoding " + function + " against " + type + " ABI", e);
80+
}
81+
}
82+
83+
private static String addHexPrefix(String value) {
84+
return value.startsWith("0x") ? value : "0x" + value;
85+
}
86+
}

src/test/java/org/arkecosystem/crypto/identities/PrivateKeyTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,31 @@ public void fromWif_round_trips_with_fromPassphrase() throws IOException {
5050
assertEquals(fromPassphrase, fromWif);
5151
}
5252

53+
@Test
54+
public void sign_produces_a_65_byte_signature_recoverable_to_the_signer_public_key() {
55+
byte[] message = org.bitcoinj.core.Sha256Hash.hash("payload".getBytes());
56+
57+
byte[] signature = PrivateKey.sign(message, "this is a top secret passphrase");
58+
59+
assertEquals(65, signature.length);
60+
assertEquals(
61+
"034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192",
62+
PublicKey.recover(message, signature).getPublicKeyAsHex());
63+
}
64+
65+
@Test
66+
public void sign_with_eckey_overload_matches_passphrase_overload() {
67+
byte[] message = org.bitcoinj.core.Sha256Hash.hash("payload".getBytes());
68+
org.bitcoinj.core.ECKey key = PrivateKey.fromPassphrase("this is a top secret passphrase");
69+
70+
byte[] viaPassphrase = PrivateKey.sign(message, "this is a top secret passphrase");
71+
byte[] viaEcKey = PrivateKey.sign(message, key);
72+
73+
assertEquals(
74+
org.arkecosystem.crypto.encoding.Hex.encode(viaPassphrase),
75+
org.arkecosystem.crypto.encoding.Hex.encode(viaEcKey));
76+
}
77+
5378
@Test
5479
public void fromWif_rejects_when_version_byte_belongs_to_another_network() throws IOException {
5580
String mainnetWif;

src/test/java/org/arkecosystem/crypto/identities/PublicKeyTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,23 @@ public void fromHex_accepts_uncompressed_public_key() {
3131

3232
Assertions.assertEquals(compressed, actual);
3333
}
34+
35+
@Test
36+
public void recover_round_trips_with_PrivateKey_sign() {
37+
byte[] message = org.bitcoinj.core.Sha256Hash.hash("Hello World".getBytes());
38+
byte[] signature = PrivateKey.sign(message, "this is a top secret passphrase");
39+
40+
org.bitcoinj.core.ECKey recovered = PublicKey.recover(message, signature);
41+
42+
Assertions.assertEquals(
43+
"034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192",
44+
recovered.getPublicKeyAsHex());
45+
}
46+
47+
@Test
48+
public void recover_throws_on_invalid_signature_length() {
49+
byte[] message = new byte[32];
50+
Assertions.assertThrows(
51+
IllegalArgumentException.class, () -> PublicKey.recover(message, new byte[64]));
52+
}
3453
}

0 commit comments

Comments
 (0)