Skip to content

Commit

Permalink
refactor: #337 Extract transaction body bytes directly from transacti…
Browse files Browse the repository at this point in the history
…on bytes
  • Loading branch information
satran004 committed Jan 24, 2024
1 parent dbfad4c commit ee67dd1
Show file tree
Hide file tree
Showing 4 changed files with 295 additions and 123 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.bloxbean.cardano.client.transaction;

import co.nstant.in.cbor.CborDecoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.*;
import com.bloxbean.cardano.client.common.cbor.CborSerializationUtil;
Expand All @@ -12,19 +11,12 @@
import com.bloxbean.cardano.client.crypto.bip32.HdKeyGenerator;
import com.bloxbean.cardano.client.crypto.bip32.HdKeyPair;
import com.bloxbean.cardano.client.crypto.config.CryptoConfiguration;
import com.bloxbean.cardano.client.exception.AddressExcepion;
import com.bloxbean.cardano.client.exception.CborDeserializationException;
import com.bloxbean.cardano.client.exception.CborRuntimeException;
import com.bloxbean.cardano.client.exception.CborSerializationException;
import com.bloxbean.cardano.client.transaction.spec.Transaction;
import com.bloxbean.cardano.client.transaction.spec.TransactionBody;
import com.bloxbean.cardano.client.transaction.spec.TransactionWitnessSet;
import com.bloxbean.cardano.client.transaction.spec.VkeyWitness;

import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.List;

import static com.bloxbean.cardano.client.transaction.util.TransactionUtil.createCopy;
import com.bloxbean.cardano.client.transaction.util.TransactionBytes;
import lombok.NonNull;

public enum TransactionSigner {
INSTANCE();
Expand All @@ -33,96 +25,55 @@ public enum TransactionSigner {

}

public Transaction sign(Transaction transaction, HdKeyPair hdKeyPair) {
Transaction cloneTxn = createCopy(transaction);
TransactionBody transactionBody = cloneTxn.getBody();

byte[] txnBody = null;
/**
* Sign transaction with a HD key pair
*
* @param transaction - Transaction to sign
* @param hdKeyPair - HD key pair
* @return Signed transaction
*/
public Transaction sign(@NonNull Transaction transaction, @NonNull HdKeyPair hdKeyPair) {
try {
txnBody = CborSerializationUtil.serialize(transactionBody.serialize());
} catch (CborException e) {
throw new CborRuntimeException("Error in Cbor serialization", e);
} catch (AddressExcepion e) {
throw new CborRuntimeException("Error in Cbor serialization", e);
} catch (CborSerializationException e) {
throw new CborRuntimeException("Error in Cbor serialization", e);
byte[] signedTxBytes = sign(transaction.serialize(), hdKeyPair);
return Transaction.deserialize(signedTxBytes);
} catch (CborSerializationException | CborDeserializationException e) {
throw new CborRuntimeException(e);
}

byte[] txnBodyHash = Blake2bUtil.blake2bHash256(txnBody);

SigningProvider signingProvider = CryptoConfiguration.INSTANCE.getSigningProvider();
byte[] signature = signingProvider.signExtended(txnBodyHash, hdKeyPair.getPrivateKey().getKeyData(), hdKeyPair.getPublicKey().getKeyData());

VkeyWitness vkeyWitness = VkeyWitness.builder()
.vkey(hdKeyPair.getPublicKey().getKeyData())
.signature(signature)
.build();

if (cloneTxn.getWitnessSet() == null)
cloneTxn.setWitnessSet(new TransactionWitnessSet());

if (cloneTxn.getWitnessSet().getVkeyWitnesses() == null)
cloneTxn.getWitnessSet().setVkeyWitnesses(new ArrayList<>());

cloneTxn.getWitnessSet().getVkeyWitnesses().add(vkeyWitness);

return cloneTxn;
}

/**
* Sign transaction with a secret key
*
* @param transaction - Transaction to sign
* @param secretKey - Secret key
* @return Signed transaction
*/
public Transaction sign(Transaction transaction, SecretKey secretKey) {
Transaction cloneTxn = createCopy(transaction);
TransactionBody transactionBody = cloneTxn.getBody();

byte[] txnBody = null;
try {
txnBody = CborSerializationUtil.serialize(transactionBody.serialize());
} catch (CborException e) {
throw new CborRuntimeException("Error in Cbor serialization", e);
} catch (AddressExcepion e) {
throw new CborRuntimeException("Error in Cbor serialization", e);
} catch (CborSerializationException e) {
throw new CborRuntimeException("Error in Cbor serialization", e);
byte[] signedTxBytes = sign(transaction.serialize(), secretKey);
return Transaction.deserialize(signedTxBytes);
} catch (CborSerializationException | CborDeserializationException e) {
throw new CborRuntimeException(e);
}
}

byte[] txnBodyHash = Blake2bUtil.blake2bHash256(txnBody);
/**
* Sign transaction bytes with a HD key pair. Use this method to sign transaction bytes from another transaction builder.
*
* @param txBytes - Transaction bytes
* @param hdKeyPair - HD key pair
* @return Signed transaction bytes
*/
public byte[] sign(@NonNull byte[] txBytes, @NonNull HdKeyPair hdKeyPair) {
TransactionBytes transactionBytes = new TransactionBytes(txBytes);
byte[] txnBodyHash = Blake2bUtil.blake2bHash256(transactionBytes.getTxBodyBytes());

SigningProvider signingProvider = CryptoConfiguration.INSTANCE.getSigningProvider();
VerificationKey verificationKey = null;
byte[] signature = null;

if (secretKey.getBytes().length == 64) { //extended pvt key (most prob for regular account)
//check for public key
byte[] vBytes = HdKeyGenerator.getPublicKey(secretKey.getBytes());
signature = signingProvider.signExtended(txnBodyHash, secretKey.getBytes(), vBytes);

try {
verificationKey = VerificationKey.create(vBytes);
} catch (CborSerializationException e) {
throw new CborRuntimeException("Unable to get verification key from secret key", e);
}
} else {
signature = signingProvider.sign(txnBodyHash, secretKey.getBytes());
try {
verificationKey = KeyGenUtil.getPublicKeyFromPrivateKey(secretKey);
} catch (CborSerializationException e) {
throw new CborRuntimeException("Unable to get verification key from SecretKey", e);
}
}

VkeyWitness vkeyWitness = VkeyWitness.builder()
.vkey(verificationKey.getBytes())
.signature(signature)
.build();

if (cloneTxn.getWitnessSet() == null)
cloneTxn.setWitnessSet(new TransactionWitnessSet());

if (cloneTxn.getWitnessSet().getVkeyWitnesses() == null)
cloneTxn.getWitnessSet().setVkeyWitnesses(new ArrayList<>());
byte[] signature = signingProvider.signExtended(txnBodyHash, hdKeyPair.getPrivateKey().getKeyData(), hdKeyPair.getPublicKey().getKeyData());

cloneTxn.getWitnessSet().getVkeyWitnesses().add(vkeyWitness);
byte[] signedTransaction = addWitnessToTransaction(transactionBytes, hdKeyPair.getPublicKey().getKeyData(), signature);

return cloneTxn;
return signedTransaction;
}

/**
Expand All @@ -133,9 +84,9 @@ public Transaction sign(Transaction transaction, SecretKey secretKey) {
* @param secretKey
* @return Signed transaction bytes
*/
public byte[] sign(byte[] transactionBytes, SecretKey secretKey) {
byte[] txnBody = extractTransactionBody(transactionBytes);
byte[] txnBodyHash = Blake2bUtil.blake2bHash256(txnBody);
public byte[] sign(@NonNull byte[] transactionBytes, @NonNull SecretKey secretKey) {
TransactionBytes txBytes = new TransactionBytes(transactionBytes);
byte[] txnBodyHash = Blake2bUtil.blake2bHash256(txBytes.getTxBodyBytes());

SigningProvider signingProvider = CryptoConfiguration.INSTANCE.getSigningProvider();
VerificationKey verificationKey;
Expand All @@ -160,39 +111,13 @@ public byte[] sign(byte[] transactionBytes, SecretKey secretKey) {
}
}

byte[] signedTransaction = addWitnessToTransaction(transactionBytes, verificationKey.getBytes(), signature);
byte[] signedTransaction = addWitnessToTransaction(txBytes, verificationKey.getBytes(), signature);
return signedTransaction;
}

/**
* Extract transaction body from transaction bytes
*
* @param txBytes
* @return transaction body bytes
*/
private byte[] extractTransactionBody(byte[] txBytes) {
ByteArrayInputStream bais = new ByteArrayInputStream(txBytes);
CborDecoder decoder = new CborDecoder(bais);
try {
Array txArray = (Array) decoder.decodeNext();
DataItem txBodyDI = txArray.getDataItems().get(0);
return CborSerializationUtil.serialize(txBodyDI, false);
} catch (CborException e) {
throw new CborRuntimeException(e);
} finally {
try {
bais.close();
} catch (Exception e) {
}
}
}

private byte[] addWitnessToTransaction(byte[] txBytes, byte[] vkey, byte[] signature) {
Array txDIArray = (Array) CborSerializationUtil.deserialize(txBytes);
List<DataItem> txDIList = txDIArray.getDataItems();

private byte[] addWitnessToTransaction(TransactionBytes transactionBytes, byte[] vkey, byte[] signature) {
try {
DataItem witnessSetDI = txDIList.get(1);
DataItem witnessSetDI = CborSerializationUtil.deserialize(transactionBytes.getTxWitnessBytes());
Map witnessSetMap = (Map) witnessSetDI;

DataItem vkWitnessArrayDI = witnessSetMap.get(new UnsignedInteger(0));
Expand All @@ -211,7 +136,10 @@ private byte[] addWitnessToTransaction(byte[] txBytes, byte[] vkey, byte[] signa

vkWitnessArray.add(vkeyWitness);

return CborSerializationUtil.serialize(txDIArray, false);
byte[] txWitnessBytes = CborSerializationUtil.serialize(witnessSetMap, false);

return transactionBytes.withNewWitnessSetBytes(txWitnessBytes)
.getTxBytes();
} catch (CborException e) {
throw new CborRuntimeException(e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.bloxbean.cardano.client.transaction.util;

import co.nstant.in.cbor.CborDecoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.SimpleValue;
import com.bloxbean.cardano.client.crypto.bip32.util.BytesUtil;
import com.bloxbean.cardano.client.exception.CborRuntimeException;
import com.bloxbean.cardano.client.util.Tuple;
import lombok.Data;

import java.io.ByteArrayInputStream;

/**
* Utility class to extract different parts of a transaction as raw bytes, without deserializing the entire transaction.
* This is useful for ensuring that the correct transaction body bytes are signed when signing a transaction.
* This is important because the deserialization and serialization of a transaction may alter the transaction body bytes.
*
*/
@Data
public class TransactionBytes {
private byte[] initialBytes;
private byte[] txBodyBytes;
private byte[] txWitnessBytes;
private byte[] validBytes;
private byte[] auxiliaryDataBytes;

/**
* Extract and create TransactionBytes from transaction bytes
* @param txBytes
*/
public TransactionBytes(byte[] txBytes) {
extractTransactionBytesFromTx(txBytes);
}

private TransactionBytes(byte[] initialBytes, byte[] txBodyBytes, byte[] txWitnessBytes, byte[] validBytes,
byte[] auxiliaryDataBytes) {
this.initialBytes = initialBytes;
this.txBodyBytes = txBodyBytes;
this.txWitnessBytes = txWitnessBytes;
this.validBytes = validBytes;
this.auxiliaryDataBytes = auxiliaryDataBytes;
}

/**
* Returns the final transaction bytes. This method merges all parts of the transaction to final bytes.
* @return transaction bytes
*/
public byte[] getTxBytes() {
if (validBytes == null) //Pre Babbage era tx
return BytesUtil.merge(initialBytes, txBodyBytes, txWitnessBytes, auxiliaryDataBytes);
else //Post Babbage era tx
return BytesUtil.merge(initialBytes, txBodyBytes, txWitnessBytes, validBytes, auxiliaryDataBytes);
}

/**
* Returns a new TransactionBytes object with new witnessSet bytes and the rest of the bytes as it is.
* @param witnessBytes
* @return a new TransactionBytes object
*/
public TransactionBytes withNewWitnessSetBytes(byte[] witnessBytes) {
return new TransactionBytes(initialBytes, txBodyBytes, witnessBytes, validBytes, auxiliaryDataBytes);
}

private TransactionBytes extractTransactionBytesFromTx(byte[] txBytes) {
if (txBytes == null || txBytes.length == 0)
throw new IllegalArgumentException("Transaction bytes can't be null or empty");

ByteArrayInputStream bais = new ByteArrayInputStream(txBytes);
CborDecoder decoder = new CborDecoder(bais);

//Extract transaction body
byte tag = (byte) bais.read(); //Skip the first byte as it is a tag
initialBytes = new byte[]{tag};

txBodyBytes = nextElementBytes(txBytes, 1, decoder, bais)._1;
int nextPos = 1 + txBodyBytes.length;
txWitnessBytes = nextElementBytes(txBytes, nextPos, decoder, bais)._1;
nextPos = nextPos + txWitnessBytes.length;
var nextElmTupel = nextElementBytes(txBytes, nextPos, decoder, bais);
validBytes = null;
auxiliaryDataBytes = null;
//Babbage era tx
if (nextElmTupel._2 == SimpleValue.TRUE || nextElmTupel._2 == SimpleValue.FALSE) {
validBytes = nextElmTupel._1;
nextPos = nextPos + validBytes.length;
auxiliaryDataBytes = nextElementBytes(txBytes, nextPos, decoder, bais)._1;
} else {
auxiliaryDataBytes = nextElmTupel._1; //Pre Babbage Era Tx
}

return this;
}

private static Tuple<byte[], DataItem> nextElementBytes(byte[] txBytes, int startPos, CborDecoder decoder, ByteArrayInputStream bais) {
DataItem dataItem;
try {
dataItem = decoder.decodeNext();
} catch (CborException e) {
throw new CborRuntimeException(e);
}

int available = bais.available();
byte[] txBodyRaw = new byte[txBytes.length - available - startPos]; // -1 for the first byte

//Copy tx body bytes to txBodyRaw
System.arraycopy(txBytes,startPos,txBodyRaw,0,txBodyRaw.length);
return new Tuple<>(txBodyRaw, dataItem);
}

}
Loading

0 comments on commit ee67dd1

Please sign in to comment.