Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: #337 Added a separate method to sign transaction bytes #342

Merged
merged 3 commits into from
Jan 26, 2024
Merged
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
@@ -1,6 +1,7 @@
package com.bloxbean.cardano.client.transaction;

import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.*;
import com.bloxbean.cardano.client.common.cbor.CborSerializationUtil;
import com.bloxbean.cardano.client.crypto.Blake2bUtil;
import com.bloxbean.cardano.client.crypto.KeyGenUtil;
Expand All @@ -10,17 +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.util.ArrayList;

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 @@ -29,62 +25,72 @@ public enum TransactionSigner {

}

public Transaction sign(Transaction transaction, HdKeyPair hdKeyPair) {
Transaction cloneTxn = createCopy(transaction);
TransactionBody transactionBody = cloneTxn.getBody();
/**
* 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 {
byte[] signedTxBytes = sign(transaction.serialize(), hdKeyPair);
return Transaction.deserialize(signedTxBytes);
} catch (CborSerializationException | CborDeserializationException e) {
throw new CborRuntimeException(e);
}
}

byte[] txnBody = null;
/**
* 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) {
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();
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());
byte[] signedTransaction = addWitnessToTransaction(transactionBytes, hdKeyPair.getPublicKey().getKeyData(), signature);

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

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

return cloneTxn;
return signedTransaction;
}

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[] txnBodyHash = Blake2bUtil.blake2bHash256(txnBody);
/**
* Sign transaction bytes with a secret key. Use this method to sign transaction bytes from another
* transaction builder.
*
* @param transactionBytes
* @param secretKey
* @return Signed transaction bytes
*/
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 = null;
byte[] signature = null;
VerificationKey verificationKey;
byte[] signature;

if (secretKey.getBytes().length == 64) { //extended pvt key (most prob for regular account)
//check for public key
Expand All @@ -105,20 +111,38 @@ public Transaction sign(Transaction transaction, SecretKey secretKey) {
}
}

VkeyWitness vkeyWitness = VkeyWitness.builder()
.vkey(verificationKey.getBytes())
.signature(signature)
.build();
byte[] signedTransaction = addWitnessToTransaction(txBytes, verificationKey.getBytes(), signature);
return signedTransaction;
}

if (cloneTxn.getWitnessSet() == null)
cloneTxn.setWitnessSet(new TransactionWitnessSet());
private byte[] addWitnessToTransaction(TransactionBytes transactionBytes, byte[] vkey, byte[] signature) {
try {
DataItem witnessSetDI = CborSerializationUtil.deserialize(transactionBytes.getTxWitnessBytes());
Map witnessSetMap = (Map) witnessSetDI;

DataItem vkWitnessArrayDI = witnessSetMap.get(new UnsignedInteger(0));
Array vkWitnessArray;
if (vkWitnessArrayDI != null) {
vkWitnessArray = (Array) vkWitnessArrayDI;
} else {
vkWitnessArray = new Array();
witnessSetMap.put(new UnsignedInteger(0), vkWitnessArray);
}

//Add witness
Array vkeyWitness = new Array();
vkeyWitness.add(new ByteString(vkey));
vkeyWitness.add(new ByteString(signature));

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

cloneTxn.getWitnessSet().getVkeyWitnesses().add(vkeyWitness);
byte[] txWitnessBytes = CborSerializationUtil.serialize(witnessSetMap, false);

return cloneTxn;
return transactionBytes.withNewWitnessSetBytes(txWitnessBytes)
.getTxBytes();
} catch (CborException e) {
throw new CborRuntimeException(e);
}
}

}
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
Loading