Skip to content
Open
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 @@ -23,7 +23,7 @@ public static Deserializer newDeserializer(String serialized) {
public AbstractTransaction deserialize() {
List<byte[]> fields = RlpDecoder.decode(rawBytes);

// Fields: [nonce, gasPrice, gasLimit, to, value, data, v, r, s]
// Fields: [nonce, gasPrice, gasLimit, to, value, data, v, r, s, legacySecondSignature?]
long nonce = bytesToLong(fields.get(0));
long gasPrice = bytesToLong(fields.get(1));
long gasLimit = bytesToLong(fields.get(2));
Expand Down Expand Up @@ -53,6 +53,8 @@ public AbstractTransaction deserialize() {
}
}

String legacySecondSignature = fields.size() == 10 ? Hex.encode(fields.get(9)) : null;

// Create temp transaction to guess type
AbstractTransaction tempTransaction = new EvmCall();
tempTransaction.nonce = nonce;
Expand All @@ -72,6 +74,7 @@ public AbstractTransaction deserialize() {
transaction.data = data;
transaction.network = Network.get().version();
transaction.signature = signature;
transaction.legacySecondSignature = legacySecondSignature;

if (signature != null) {
transaction.recoverSender();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@
import org.arkecosystem.crypto.utils.AbiDecoder;
import org.arkecosystem.crypto.utils.TransactionUtils;
import org.bitcoinj.core.ECKey;
import org.web3j.crypto.Hash;

public abstract class AbstractTransaction {
// 0x8f ‖ "MAINSAIL_LSS_V1"
private static final byte[] LEGACY_SECOND_SIGNATURE_DOMAIN =
Hex.decode("8f4d41494e5341494c5f4c53535f5631");

public int network;
public long nonce;
public String senderPublicKey;
Expand Down Expand Up @@ -85,6 +90,9 @@ public AbstractTransaction(Map<String, Object> data) {
if (data.containsKey("senderAddress")) {
this.senderAddress = (String) data.get("senderAddress");
}
if (data.containsKey("legacySecondSignature")) {
this.legacySecondSignature = (String) data.get("legacySecondSignature");
}
}

public String getPayload() {
Expand All @@ -109,7 +117,10 @@ public byte[] hash(boolean skipSignature) {
}

public AbstractTransaction sign(String passphrase) {
ECKey privateKey = PrivateKey.fromPassphrase(passphrase);
return sign(PrivateKey.fromPassphrase(passphrase));
}

public AbstractTransaction sign(ECKey privateKey) {
this.senderPublicKey = privateKey.getPublicKeyAsHex();

this.signature = signHash(this.hash(true), privateKey);
Expand All @@ -118,9 +129,31 @@ public AbstractTransaction sign(String passphrase) {
}

public AbstractTransaction legacySecondSign(String secondPassphrase) {
ECKey privateKey = PrivateKey.fromPassphrase(secondPassphrase);
return legacySecondSign(PrivateKey.fromPassphrase(secondPassphrase));
}

public AbstractTransaction legacySecondSign(ECKey secondPrivateKey) {
if (this.signature == null) {
throw new IllegalStateException(
"Transaction must be signed before adding a legacy second signature");
}

byte[] transactionHash = this.hash(false);
byte[] message = new byte[LEGACY_SECOND_SIGNATURE_DOMAIN.length + transactionHash.length];
System.arraycopy(
LEGACY_SECOND_SIGNATURE_DOMAIN,
0,
message,
0,
LEGACY_SECOND_SIGNATURE_DOMAIN.length);
System.arraycopy(
transactionHash,
0,
message,
LEGACY_SECOND_SIGNATURE_DOMAIN.length,
transactionHash.length);

this.legacySecondSignature = signHash(this.hash(true), privateKey);
this.legacySecondSignature = signHash(Hash.sha3(message), secondPrivateKey);

return this;
}
Expand Down Expand Up @@ -181,6 +214,7 @@ public HashMap<String, Object> toHashMap() {
map.put("recipientAddress", this.recipientAddress);
map.put("value", this.value);
map.put("data", this.data);
map.put("legacySecondSignature", this.legacySecondSignature);
return map;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
public class TransactionUtils {

public static byte[] toBuffer(Map<String, Object> transaction, boolean skipSignature) {
return toBuffer(transaction, skipSignature, false);
}

public static byte[] toBuffer(
Map<String, Object> transaction,
boolean skipSignature,
boolean skipLegacySecondSignature) {
String toHex = parseHexFromStr((String) transaction.get("recipientAddress"));
if (toHex.length() % 2 != 0) {
toHex = "0" + toHex;
Expand Down Expand Up @@ -42,8 +49,12 @@ public static byte[] toBuffer(Map<String, Object> transaction, boolean skipSigna

int chainId = Network.get().chainId();
fields.add(RlpEncoder.toBeArray(BigInteger.valueOf(v + chainId * 2 + 35)));
fields.add(r);
fields.add(s);
fields.add(stripLeadingZeroBytes(r));
fields.add(stripLeadingZeroBytes(s));

if (!skipLegacySecondSignature && transaction.get("legacySecondSignature") != null) {
fields.add(Hex.decode((String) transaction.get("legacySecondSignature")));
}
} else {
int chainId = Network.get().chainId();
fields.add(RlpEncoder.toBeArray(BigInteger.valueOf(chainId)));
Expand All @@ -55,7 +66,8 @@ public static byte[] toBuffer(Map<String, Object> transaction, boolean skipSigna
}

public static byte[] toHash(Map<String, Object> transaction, boolean skipSignature) {
byte[] buffer = toBuffer(transaction, skipSignature);
// The transaction hash never covers the legacy second signature
byte[] buffer = toBuffer(transaction, skipSignature, true);
return Hash.sha3(buffer);
}

Expand All @@ -64,6 +76,14 @@ public static String getId(Map<String, Object> transaction) {
return Hex.encode(hash);
}

private static byte[] stripLeadingZeroBytes(byte[] value) {
int start = 0;
while (start < value.length && value[start] == 0) {
start++;
}
return Arrays.copyOfRange(value, start, value.length);
}

private static String parseHexFromStr(String value) {
if (value == null) return "";
return value.replaceFirst("^0x", "");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.arkecosystem.crypto.transactions;

import static org.junit.jupiter.api.Assertions.*;

import java.util.HashMap;
import java.util.Map;
import org.arkecosystem.crypto.AbstractTest;
import org.arkecosystem.crypto.configuration.Network;
import org.arkecosystem.crypto.encoding.Hex;
import org.arkecosystem.crypto.identities.PrivateKey;
import org.arkecosystem.crypto.networks.Testnet;
import org.arkecosystem.crypto.transactions.types.AbstractTransaction;
import org.arkecosystem.crypto.transactions.types.Transfer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

// Test vector from the Mainsail LSS v1 spec, built for chain 10_000
public class LegacySecondSignatureTest extends AbstractTest {
private Map<String, Object> fixture;
private Map<String, Object> data;

@BeforeEach
public void setUpVector() throws Exception {
this.fixture = loadFixture("transfer-legacy-second-signature-vector");
this.data = (Map<String, Object>) fixture.get("data");

int chainId = ((Number) fixture.get("chainId")).intValue();
Network.set(
new Testnet() {
@Override
public int chainId() {
return chainId;
}
});
}

private Transfer unsignedTransfer() {
Map<String, Object> unsignedData = new HashMap<>(this.data);
unsignedData.remove("legacySecondSignature");
unsignedData.remove("id");

return new Transfer(unsignedData);
}

@Test
public void it_should_sign_the_hash_of_the_signed_transaction_with_the_second_key() {
Transfer transaction = unsignedTransfer();

transaction.sign(PrivateKey.fromHex((String) fixture.get("privateKey")));

assertEquals(fixture.get("signed"), Hex.encode(transaction.serialize()));
assertEquals(data.get("id"), transaction.getId());

transaction.legacySecondSign(PrivateKey.fromHex((String) fixture.get("secondPrivateKey")));

assertEquals(data.get("legacySecondSignature"), transaction.legacySecondSignature);
assertEquals(fixture.get("serialized"), Hex.encode(transaction.serialize()));
assertEquals(data.get("id"), transaction.getId());
}

@Test
public void it_should_use_a_0_or_1_recovery_id() {
Transfer transaction = unsignedTransfer();

transaction.sign(PrivateKey.fromHex((String) fixture.get("privateKey")));
transaction.legacySecondSign(PrivateKey.fromHex((String) fixture.get("secondPrivateKey")));

String recoveryId = transaction.legacySecondSignature.substring(128);
assertTrue(recoveryId.equals("00") || recoveryId.equals("01"));
}

@Test
public void it_should_require_the_primary_signature_first() {
Transfer transaction = unsignedTransfer();

IllegalStateException exception =
assertThrows(
IllegalStateException.class,
() ->
transaction.legacySecondSign(
PrivateKey.fromHex(
(String) fixture.get("secondPrivateKey"))));

assertEquals(
"Transaction must be signed before adding a legacy second signature",
exception.getMessage());
}

@Test
public void it_should_deserialize_and_hash_only_the_first_9_fields() {
AbstractTransaction transaction =
Deserializer.newDeserializer((String) fixture.get("serialized")).deserialize();

assertEquals(data.get("legacySecondSignature"), transaction.legacySecondSignature);
assertEquals(data.get("id"), transaction.id);
assertEquals(fixture.get("serialized"), Hex.encode(transaction.serialize()));
assertTrue(transaction.verify());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"chainId": 10000,
"privateKey": "314f59dbc54460b18fc036d67dac36ad6e1fa1c3c0e0b3427a84e43faabfcecb",
"secondPrivateKey": "4427335cab240f6cc651b1df5ce5ff489c5a0bcb952507b542afe8f6c90e98e7",
"data": {
"nonce": "0",
"gasPrice": 5000000000,
"gasLimit": 21000,
"recipientAddress": "0x0DFeDD974c2e58Dc8E292ec159d1DC9AdeaFb1C0",
"value": "1000000000000000000",
"data": "",
"legacySecondSignature": "ea77adb968255ec1570c0e48cede8cceec3a9aff29e9dac1c53c2a6de4f2a18740df289edfcdfe97a189975cba8945334e5f230d66232751e468c5500dbe7dec01",
"id": "27af7a389f34f32569c90815ced4e23e8770eb2d77ace6f29dabec1a10bef205"
},
"signed": "f86e8085012a05f200825208940dfedd974c2e58dc8e292ec159d1dc9adeafb1c0880de0b6b3a764000080824e44a00f4f4ac7f1effa669ea1a8970ed37f367e4aa0b549636619feead2d4a2408ffca04492af59379f74385bd951d229456b3b23ead33ce1d10106535c6830a688733d",
"serialized": "f8b18085012a05f200825208940dfedd974c2e58dc8e292ec159d1dc9adeafb1c0880de0b6b3a764000080824e44a00f4f4ac7f1effa669ea1a8970ed37f367e4aa0b549636619feead2d4a2408ffca04492af59379f74385bd951d229456b3b23ead33ce1d10106535c6830a688733db841ea77adb968255ec1570c0e48cede8cceec3a9aff29e9dac1c53c2a6de4f2a18740df289edfcdfe97a189975cba8945334e5f230d66232751e468c5500dbe7dec01"
}
Loading