Skip to content

Commit a362b93

Browse files
authored
fix: use RLP encoding for transaction serialization (#193)
1 parent b60a6f0 commit a362b93

22 files changed

Lines changed: 419 additions & 406 deletions
Lines changed: 65 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,83 @@
11
package org.arkecosystem.crypto.transactions;
22

33
import java.math.BigInteger;
4-
import java.nio.ByteBuffer;
5-
import java.nio.ByteOrder;
4+
import java.util.List;
65
import java.util.Map;
6+
import org.arkecosystem.crypto.configuration.Network;
77
import org.arkecosystem.crypto.encoding.Hex;
88
import org.arkecosystem.crypto.enums.AbiFunction;
99
import org.arkecosystem.crypto.transactions.types.*;
1010
import org.arkecosystem.crypto.utils.AbiDecoder;
11+
import org.arkecosystem.crypto.utils.RlpDecoder;
1112

1213
public class Deserializer {
13-
private static final int SIGNATURE_SIZE = 64;
14-
private static final int RECOVERY_SIZE = 1;
1514

16-
private final ByteBuffer buffer;
15+
private final byte[] rawBytes;
1716

1817
public Deserializer(String serialized) {
19-
byte[] bytes = serialized.contains("\0") ? serialized.getBytes() : Hex.decode(serialized);
20-
this.buffer = ByteBuffer.wrap(bytes);
21-
this.buffer.order(ByteOrder.LITTLE_ENDIAN);
18+
this.rawBytes = Hex.decode(serialized);
2219
}
2320

2421
public static Deserializer newDeserializer(String serialized) {
2522
return new Deserializer(serialized);
2623
}
2724

2825
public AbstractTransaction deserialize() {
29-
int startPosition = buffer.position();
26+
List<byte[]> fields = RlpDecoder.decode(rawBytes);
27+
28+
// Fields: [nonce, gasPrice, gasLimit, to, value, data, v, r, s]
29+
long nonce = bytesToLong(fields.get(0));
30+
long gasPrice = bytesToLong(fields.get(1));
31+
long gasLimit = bytesToLong(fields.get(2));
32+
String recipientAddress = fields.get(3).length > 0 ? "0x" + Hex.encode(fields.get(3)) : "";
33+
String value = fields.get(4).length > 0 ? new BigInteger(1, fields.get(4)).toString() : "0";
34+
String data = fields.get(5).length > 0 ? Hex.encode(fields.get(5)) : "";
35+
36+
// Recover signature
37+
String signature = null;
38+
if (fields.size() >= 9) {
39+
int vEncoded =
40+
fields.get(6).length > 0 ? new BigInteger(1, fields.get(6)).intValue() : 0;
41+
byte[] r = fields.get(7);
42+
byte[] s = fields.get(8);
43+
44+
int chainId = Network.get().chainId();
45+
int v = vEncoded - (chainId * 2 + 35);
46+
47+
if (r.length > 0 || s.length > 0) {
48+
byte[] rPadded = padTo32(r);
49+
byte[] sPadded = padTo32(s);
50+
byte[] sigBytes = new byte[65];
51+
System.arraycopy(rPadded, 0, sigBytes, 0, 32);
52+
System.arraycopy(sPadded, 0, sigBytes, 32, 32);
53+
sigBytes[64] = (byte) v;
54+
signature = Hex.encode(sigBytes);
55+
}
56+
}
3057

58+
// Create temp transaction to guess type
3159
AbstractTransaction tempTransaction = new EvmCall();
32-
deserializeCommon(tempTransaction);
33-
deserializeData(tempTransaction);
60+
tempTransaction.nonce = nonce;
61+
tempTransaction.gasPrice = gasPrice;
62+
tempTransaction.gasLimit = gasLimit;
63+
tempTransaction.recipientAddress = recipientAddress;
64+
tempTransaction.value = value;
65+
tempTransaction.data = data;
66+
tempTransaction.network = Network.get().version();
3467

3568
AbstractTransaction transaction = guessTransactionFromTransactionData(tempTransaction);
36-
37-
buffer.position(startPosition);
38-
39-
deserializeCommon(transaction);
40-
deserializeData(transaction);
41-
deserializeSignatures(transaction);
42-
43-
transaction.recoverSender();
69+
transaction.nonce = nonce;
70+
transaction.gasPrice = gasPrice;
71+
transaction.gasLimit = gasLimit;
72+
transaction.recipientAddress = recipientAddress;
73+
transaction.value = value;
74+
transaction.data = data;
75+
transaction.network = Network.get().version();
76+
transaction.signature = signature;
77+
78+
if (signature != null) {
79+
transaction.recoverSender();
80+
}
4481

4582
transaction.computeId();
4683

@@ -49,7 +86,7 @@ public AbstractTransaction deserialize() {
4986

5087
private AbstractTransaction guessTransactionFromTransactionData(
5188
AbstractTransaction transactionData) {
52-
if (!"0".equals(transactionData.value)) {
89+
if (!"0".equals(transactionData.value) && !"".equals(transactionData.value)) {
5390
return new Transfer();
5491
}
5592

@@ -87,41 +124,15 @@ private Map<String, Object> decodePayload(AbstractTransaction transaction) {
87124
}
88125
}
89126

90-
private void deserializeCommon(AbstractTransaction transaction) {
91-
transaction.network = Byte.toUnsignedInt(buffer.get());
92-
transaction.nonce = buffer.getLong();
93-
transaction.gasPrice = buffer.getInt();
94-
transaction.gasLimit = buffer.getInt();
95-
}
96-
97-
private void deserializeData(AbstractTransaction transaction) {
98-
byte[] valueBytes = new byte[32];
99-
buffer.get(valueBytes);
100-
transaction.value = new BigInteger(1, valueBytes).toString();
101-
102-
int recipientMarker = Byte.toUnsignedInt(buffer.get());
103-
if (recipientMarker == 1) {
104-
byte[] recipientBytes = new byte[20];
105-
buffer.get(recipientBytes);
106-
transaction.recipientAddress = "0x" + Hex.encode(recipientBytes);
107-
}
108-
109-
int payloadLength = buffer.getInt();
110-
if (payloadLength > 0) {
111-
byte[] payloadBytes = new byte[payloadLength];
112-
buffer.get(payloadBytes);
113-
transaction.data = Hex.encode(payloadBytes);
114-
} else {
115-
transaction.data = "";
116-
}
127+
private static long bytesToLong(byte[] bytes) {
128+
if (bytes.length == 0) return 0;
129+
return new BigInteger(1, bytes).longValue();
117130
}
118131

119-
private void deserializeSignatures(AbstractTransaction transaction) {
120-
int signatureLength = SIGNATURE_SIZE + RECOVERY_SIZE;
121-
if (buffer.remaining() >= signatureLength) {
122-
byte[] signatureBytes = new byte[signatureLength];
123-
buffer.get(signatureBytes);
124-
transaction.signature = Hex.encode(signatureBytes);
125-
}
132+
private static byte[] padTo32(byte[] input) {
133+
if (input.length >= 32) return input;
134+
byte[] padded = new byte[32];
135+
System.arraycopy(input, 0, padded, 32 - input.length, input.length);
136+
return padded;
126137
}
127138
}
Lines changed: 2 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package org.arkecosystem.crypto.transactions;
22

3-
import java.math.BigInteger;
4-
import java.nio.ByteBuffer;
5-
import java.nio.ByteOrder;
6-
import org.arkecosystem.crypto.encoding.Hex;
73
import org.arkecosystem.crypto.transactions.types.AbstractTransaction;
4+
import org.arkecosystem.crypto.utils.TransactionUtils;
85

96
public class Serializer {
107
private final AbstractTransaction transaction;
@@ -22,61 +19,6 @@ public static byte[] getBytes(AbstractTransaction transaction, boolean skipSigna
2219
}
2320

2421
public byte[] serialize(boolean skipSignature) {
25-
ByteBuffer buffer = ByteBuffer.allocate(1024);
26-
buffer.order(ByteOrder.LITTLE_ENDIAN);
27-
28-
serializeCommon(buffer);
29-
30-
serializeData(buffer);
31-
32-
serializeSignatures(buffer, skipSignature);
33-
34-
byte[] result = new byte[buffer.position()];
35-
buffer.flip();
36-
buffer.get(result);
37-
return result;
38-
}
39-
40-
private void serializeCommon(ByteBuffer buffer) {
41-
buffer.put((byte) transaction.network);
42-
buffer.putLong(transaction.nonce);
43-
buffer.putInt((int) transaction.gasPrice);
44-
buffer.putInt((int) transaction.gasLimit);
45-
}
46-
47-
private void serializeData(ByteBuffer buffer) {
48-
// Convert 'value' from String to BigInteger and write as Uint256 (32 bytes)
49-
byte[] valueBytes = new BigInteger(transaction.value).toByteArray();
50-
byte[] valueBytesPadded = new byte[32];
51-
int srcPos = Math.max(0, valueBytes.length - 32);
52-
int destPos = 32 - (valueBytes.length - srcPos);
53-
System.arraycopy(valueBytes, srcPos, valueBytesPadded, destPos, valueBytes.length - srcPos);
54-
buffer.put(valueBytesPadded);
55-
56-
// Write recipient marker and address
57-
if (transaction.recipientAddress != null && !transaction.recipientAddress.isEmpty()) {
58-
buffer.put((byte) 1);
59-
byte[] recipientBytes =
60-
Hex.decode(transaction.recipientAddress.replaceFirst("^0x", "").toLowerCase());
61-
62-
buffer.put(recipientBytes);
63-
} else {
64-
buffer.put((byte) 0);
65-
}
66-
67-
// Write payload length as UInt32 and the payload itself if present
68-
String payloadHex =
69-
transaction.data != null ? transaction.data.replaceFirst("^0x", "") : "";
70-
int payloadLength = payloadHex.length() / 2;
71-
buffer.putInt(payloadLength);
72-
if (payloadLength > 0) {
73-
buffer.put(Hex.decode(payloadHex));
74-
}
75-
}
76-
77-
private void serializeSignatures(ByteBuffer buffer, boolean skipSignature) {
78-
if (!skipSignature && transaction.signature != null) {
79-
buffer.put(Hex.decode(transaction.signature));
80-
}
22+
return TransactionUtils.toBuffer(transaction.toHashMap(), skipSignature);
8123
}
8224
}

src/main/java/org/arkecosystem/crypto/transactions/builder/AbstractTransactionBuilder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ private void initializeTransactionDefaults() {
2424
this.transaction.refreshPayloadData();
2525
}
2626

27-
public TBuilder gasLimit(int gasLimit) {
27+
public TBuilder gasLimit(long gasLimit) {
2828
this.transaction.gasLimit = gasLimit;
2929
return this.instance();
3030
}
@@ -34,7 +34,7 @@ public TBuilder recipientAddress(String recipientAddressId) {
3434
return this.instance();
3535
}
3636

37-
public TBuilder gasPrice(int gasPrice) {
37+
public TBuilder gasPrice(long gasPrice) {
3838
this.transaction.gasPrice = gasPrice;
3939
return this.instance();
4040
}

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

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import org.arkecosystem.crypto.identities.PrivateKey;
1212
import org.arkecosystem.crypto.transactions.Serializer;
1313
import org.arkecosystem.crypto.utils.AbiDecoder;
14-
import org.arkecosystem.crypto.utils.TransactionHasher;
14+
import org.arkecosystem.crypto.utils.TransactionUtils;
1515
import org.bitcoinj.core.ECKey;
1616
import org.bitcoinj.core.Sha256Hash;
1717

@@ -26,8 +26,8 @@ public abstract class AbstractTransaction {
2626
public String value = "0";
2727
public String recipientAddress;
2828
public String id;
29-
public int gasLimit;
30-
public int gasPrice;
29+
public long gasLimit;
30+
public long gasPrice;
3131
public String validatorPublicKey;
3232
public String vote;
3333

@@ -38,13 +38,18 @@ public AbstractTransaction(Map<String, Object> data) {
3838
this.network = ((Number) data.get("network")).intValue();
3939
}
4040
if (data.containsKey("nonce")) {
41-
this.nonce = Long.parseLong(data.get("nonce").toString());
41+
Object nonceVal = data.get("nonce");
42+
if (nonceVal instanceof Number) {
43+
this.nonce = ((Number) nonceVal).longValue();
44+
} else {
45+
this.nonce = Long.parseLong(nonceVal.toString());
46+
}
4247
}
4348
if (data.containsKey("gasPrice")) {
44-
this.gasPrice = ((Number) data.get("gasPrice")).intValue();
49+
this.gasPrice = ((Number) data.get("gasPrice")).longValue();
4550
}
4651
if (data.containsKey("gasLimit")) {
47-
this.gasLimit = ((Number) data.get("gasLimit")).intValue();
52+
this.gasLimit = ((Number) data.get("gasLimit")).longValue();
4853
}
4954
if (data.containsKey("recipientAddress")) {
5055
this.recipientAddress = (String) data.get("recipientAddress");
@@ -93,18 +98,7 @@ public String getId() {
9398
}
9499

95100
public byte[] hash(boolean skipSignature) {
96-
HashMap<String, Object> map = new HashMap<>();
97-
map.put("gasPrice", this.gasPrice);
98-
map.put("network", this.network);
99-
map.put("nonce", this.nonce);
100-
map.put("value", this.value);
101-
map.put("gasLimit", this.gasLimit);
102-
map.put("data", this.data);
103-
map.put("recipientAddress", this.recipientAddress);
104-
if (!skipSignature && this.signature != null) {
105-
map.put("signature", this.signature);
106-
}
107-
return TransactionHasher.toHash(map, skipSignature);
101+
return TransactionUtils.toHash(toHashMap(), skipSignature);
108102
}
109103

110104
public AbstractTransaction sign(String passphrase) {
@@ -181,7 +175,6 @@ public void recoverSender() {
181175

182176
this.senderPublicKey = recoveredKey.getPublicKeyAsHex();
183177

184-
// Compute the sender's address (EVM address)
185178
this.senderAddress = Address.fromPublicKey(this.senderPublicKey);
186179
}
187180

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package org.arkecosystem.crypto.utils;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
public class RlpDecoder {
7+
8+
public static List<byte[]> decode(byte[] input) {
9+
List<byte[]> result = new ArrayList<>();
10+
decodeList(input, 0, input.length, result);
11+
return result;
12+
}
13+
14+
private static void decodeList(byte[] input, int offset, int end, List<byte[]> result) {
15+
if (offset >= end) return;
16+
17+
int prefix = input[offset] & 0xFF;
18+
19+
if (prefix <= 0x7F) {
20+
result.add(new byte[] {input[offset]});
21+
decodeList(input, offset + 1, end, result);
22+
} else if (prefix <= 0xB7) {
23+
int len = prefix - 0x80;
24+
byte[] data = new byte[len];
25+
System.arraycopy(input, offset + 1, data, 0, len);
26+
result.add(data);
27+
decodeList(input, offset + 1 + len, end, result);
28+
} else if (prefix <= 0xBF) {
29+
int lenOfLen = prefix - 0xB7;
30+
int len = readLength(input, offset + 1, lenOfLen);
31+
byte[] data = new byte[len];
32+
System.arraycopy(input, offset + 1 + lenOfLen, data, 0, len);
33+
result.add(data);
34+
decodeList(input, offset + 1 + lenOfLen + len, end, result);
35+
} else if (prefix <= 0xF7) {
36+
int len = prefix - 0xC0;
37+
List<byte[]> inner = new ArrayList<>();
38+
decodeList(input, offset + 1, offset + 1 + len, inner);
39+
result.addAll(inner);
40+
decodeList(input, offset + 1 + len, end, result);
41+
} else {
42+
int lenOfLen = prefix - 0xF7;
43+
int len = readLength(input, offset + 1, lenOfLen);
44+
List<byte[]> inner = new ArrayList<>();
45+
decodeList(input, offset + 1 + lenOfLen, offset + 1 + lenOfLen + len, inner);
46+
result.addAll(inner);
47+
decodeList(input, offset + 1 + lenOfLen + len, end, result);
48+
}
49+
}
50+
51+
private static int readLength(byte[] input, int offset, int lenOfLen) {
52+
int len = 0;
53+
for (int i = 0; i < lenOfLen; i++) {
54+
len = (len << 8) | (input[offset + i] & 0xFF);
55+
}
56+
return len;
57+
}
58+
}

0 commit comments

Comments
 (0)