Skip to content

Commit

Permalink
Add era-specific serialization support for transactions (#441)
Browse files Browse the repository at this point in the history
* Add era-specific serialization support for transactions

Refactor transaction serialization logic to include era-specific behavior, ensuring compatibility with different Cardano eras. Update various test cases and integration tests to reflect these changes, and add logging for better traceability.

* Update era checks for null safety and consistency

Ensure that era checks account for null values before comparison, improving reliability. This change affects the `TransactionWitnessSet`, `SerializationUtil`, and `ScriptDataHashGenerator` classes, aligning all era validations with the updated approach.

* Set default era to Babbage if PlutusV1 exists

Updated ScriptDataHashGenerator to set the default era to Babbage when PlutusV1 scripts are detected and the era is not specified. This ensures compatibility with Babbage-era scripts while preserving existing behavior for other scenarios. Added tests to validate this behavior.
  • Loading branch information
satran004 authored Sep 15, 2024
1 parent 265ac93 commit 7d12d2f
Show file tree
Hide file tree
Showing 55 changed files with 724 additions and 334 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package com.bloxbean.cardano.client.spec;

/**
* List of Eras which can be used during transaction serialization
*/
public enum Era {
Byron(1),
Shelley(2),
Allegra(3),
Mary(4),
Alonzo(5),
Babbage(6),
Conway(7);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ public enum EraSerializationConfig {
this.era = Era.Conway;
}

public void setEra(Era era) {
this.era = era;
}

public Era getEra() {
return era;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
import com.bloxbean.cardano.client.exception.AddressExcepion;
import com.bloxbean.cardano.client.exception.AddressRuntimeException;
import com.bloxbean.cardano.client.exception.CborSerializationException;
import com.bloxbean.cardano.client.spec.Era;
import com.bloxbean.cardano.client.spec.EraSerializationConfig;
import com.bloxbean.cardano.client.spec.NetworkId;
import com.bloxbean.cardano.client.transaction.spec.*;
import com.bloxbean.cardano.client.util.HexUtil;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import com.bloxbean.cardano.client.metadata.cbor.CBORMetadataMap;
import com.bloxbean.cardano.client.plutus.spec.PlutusV1Script;
import com.bloxbean.cardano.client.spec.Era;
import com.bloxbean.cardano.client.spec.EraSerializationConfig;
import com.bloxbean.cardano.client.transaction.TransactionSigner;
import com.bloxbean.cardano.client.transaction.spec.script.ScriptAtLeast;
import com.bloxbean.cardano.client.transaction.spec.script.ScriptPubkey;
Expand Down Expand Up @@ -872,30 +871,22 @@ void testCanonicalOrderAssetNames_whenSerialize() throws CborSerializationExcept
class NamiCompatibility {
@Test
public void testCompatibilityWithNamiTxnHex_1() throws Exception {
var defaultEra = EraSerializationConfig.INSTANCE.getEra();
EraSerializationConfig.INSTANCE.setEra(Era.Babbage);

String namiTxnHex = "84a30082825820a149b3c9740b8f8b957415442336bec65bb393dfa93e88473c71a8fb7959c6d005825820297b1e742de3e7dd5f013424f5ff62d82fdf65f9d7194de1eff286a8232a196c00018282583900aff761fc1d70474cfeec6d86a2e428231949ebeb8b71464791256205167590ce95c329c8b4ba3e8e254bcc2d1e8db792dc66aa0117f94047821a0014851ea1581cac6d9e75ca58379c394378a64ae24eddf72b2e78d73f635bac32d03da143434144194e20825839003175d03902583e82037438cc86732f6e539f803f9a8b2d4ee164b9d0c77e617030631811f60a1f8a8be26d65a57ff71825b336cc6b76361d821a045c5f93a2581c0f39b76d79c90289b42af0a4759f04c2cb0adcc42ae19787ff8084eea14541444d494e01581cac6d9e75ca58379c394378a64ae24eddf72b2e78d73f635bac32d03da1434341441a23c19850021a0002aaeda0f5f6";
Transaction desTxn = Transaction.deserialize(HexUtil.decodeHexString(namiTxnHex));
desTxn.setEra(Era.Babbage);

assertThat(desTxn.serializeToHex()).isEqualTo(namiTxnHex);

EraSerializationConfig.INSTANCE.setEra(defaultEra);
}

@Test
public void testCompatibilityWithNamiTxnHex_2() throws Exception {
var defaultEra = EraSerializationConfig.INSTANCE.getEra();
EraSerializationConfig.INSTANCE.setEra(Era.Babbage);

String namiTxnHex = "84a300838258209f213609a073f632b0f38c8bf0a2b525f6899a0e5ee006117a5734d3bef2585c048258208a1df7d5c860bb63f3b344c03f4ddd8a34a7619ce223fdb8a73f6e8a92d3813b008258201c4859ad2027aa4d8e17f66e266ea83928d575c9dd9e72ca271db9ca1bf11b4f000182825839009cc9fc0f9d97e92ef5c40931bdc9a327be94f6c9d0390d4cbd8b62d16791a7c8e95c53ffb72600ea13051f03854eef6267298760265f5bbb821a1c528901a3581c0df4e527fb4ed572c6aca78a0e641701c70715261810fa6ee98db9efa1434f50441a0bebc214581c0f39b76d79c90289b42af0a4759f04c2cb0adcc42ae19787ff8084eea14541444d494e01581cac6d9e75ca58379c394378a64ae24eddf72b2e78d73f635bac32d03da143434144148258390015cabc8bf1382d2dae1f317c862c3cd3de6400c8db7de092483ce9c91bfb8f3e334b0d42cf6f28e536bcb3151d64484c5c78d47d849d4291821a0014851ea1581cac6d9e75ca58379c394378a64ae24eddf72b2e78d73f635bac32d03da143434144194e0c021a0002c851a0f5f6";

Transaction namiTxn = Transaction.deserialize(HexUtil.decodeHexString(namiTxnHex));
namiTxn.setEra(Era.Babbage);
String finalTxnHex = namiTxn.serializeToHex();

assertThat(finalTxnHex).isEqualTo(namiTxnHex);

EraSerializationConfig.INSTANCE.setEra(defaultEra);
}

@Test
Expand Down Expand Up @@ -942,22 +933,19 @@ public void testMultiAssetsPolicyIdOrdering() throws Exception {

@Test
void testCompatibilityWithNamiWitness() throws Exception {
var defaultEra = EraSerializationConfig.INSTANCE.getEra();
EraSerializationConfig.INSTANCE.setEra(Era.Babbage);
String namiTxnHex = "84a300838258206deb8993fa4b541892b5b698fef4fbb9ea73265395b5837fe45581b37efb3b7a01825820297b1e742de3e7dd5f013424f5ff62d82fdf65f9d7194de1eff286a8232a196c00825820538586beedfbac419ca3a5983c0efa3f486ffddbebf053824abc778a55c06078000182825839005a512f32ebdd33cbb6525d9fe041b0044ecbb24dec402b5c2c8bf98a1bfb8f3e334b0d42cf6f28e536bcb3151d64484c5c78d47d849d4291821a0014851ea1581cac6d9e75ca58379c394378a64ae24eddf72b2e78d73f635bac32d03da143434144194da8825839003175d03902583e82037438cc86732f6e539f803f9a8b2d4ee164b9d0c77e617030631811f60a1f8a8be26d65a57ff71825b336cc6b76361d821a003040c9a3581c0f39b76d79c90289b42af0a4759f04c2cb0adcc42ae19787ff8084eea14541444d494e01581c869708e97e418f1422e22f8b026559a565aab320184fdd98efff4f4ca14a2142466426544d6d704001581cac6d9e75ca58379c394378a64ae24eddf72b2e78d73f635bac32d03da14343414414021a0002c8d5a0f5f6";
String namiWitness = "a10081825820a5f73966e73d0bb9eadc75c5857eafd054a0202d716ac6dde00303ee9c0019e358402bfb7ba45583f5826e02d1cee544ab37398a72c0db5e01048f88ebd7c97deff7b49492be0f0edabebe67b14cb8b12a3e68c42cbb0b4970ec2e60919c3a5b6b02";
String namiMnemonic = "round stomach concert dizzy pluck express inject seminar satoshi vote essence artist pink awful bubble frog bullet horror spoil risk false dolphin limit sock";

Transaction transaction = Transaction.deserialize(HexUtil.decodeHexString(namiTxnHex));
transaction.setEra(Era.Babbage);
Account namiAcc = new Account(Networks.testnet(), namiMnemonic);
Transaction signedTxn = namiAcc.sign(transaction);

String finalWitnessHex = HexUtil.encodeHexString(CborSerializationUtil.serialize(signedTxn.getWitnessSet().serialize()));
String finalWitnessHex = HexUtil.encodeHexString(CborSerializationUtil.serialize(signedTxn.getWitnessSet().serialize(Era.Babbage)));

assertThat(finalWitnessHex).isEqualTo(namiWitness);
assertThat(transaction.serializeToHex()).isEqualTo(namiTxnHex);

EraSerializationConfig.INSTANCE.setEra(defaultEra);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
import com.bloxbean.cardano.client.exception.CborSerializationException;
import com.bloxbean.cardano.client.plutus.spec.*;
import com.bloxbean.cardano.client.plutus.util.ScriptDataHashGenerator;
import com.bloxbean.cardano.client.spec.Era;
import com.bloxbean.cardano.client.util.HexUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.math.BigInteger;
import java.util.Arrays;

import static com.bloxbean.cardano.client.api.util.CostModelUtil.PlutusV1CostModel;
import static com.bloxbean.cardano.client.api.util.CostModelUtil.PlutusV2CostModel;
import static org.assertj.core.api.Assertions.assertThat;

//TODO -- Move this test to spec module later
Expand Down Expand Up @@ -46,6 +49,29 @@ void generate() throws CborException, CborSerializationException {
PlutusData plutusData = new BigIntPlutusData(new BigInteger("1000"));
listPlutusData.add(plutusData);

Redeemer redeemer = Redeemer.builder()
.tag(RedeemerTag.Spend)
.index(BigInteger.valueOf(1))
.data(new BigIntPlutusData(new BigInteger("2000")))
.exUnits(ExUnits.builder()
.mem(BigInteger.valueOf(0))
.steps(BigInteger.valueOf(0))
.build()
).build();

byte[] hashBytes = ScriptDataHashGenerator.generate(Era.Babbage, Arrays.asList(redeemer), Arrays.asList(plutusData), costMdls);
String hash = HexUtil.encodeHexString(hashBytes);
System.out.println(hash);

assertThat(hash).isEqualTo("57240d358f8ab6128c4a66340271e4fec39b4971232add308f01a5809313adcf");
}

@Test
void generate_plutusV1_noEraSet() throws CborException, CborSerializationException {
ListPlutusData listPlutusData = new ListPlutusData();
PlutusData plutusData = new BigIntPlutusData(new BigInteger("1000"));
listPlutusData.add(plutusData);

Redeemer redeemer = Redeemer.builder()
.tag(RedeemerTag.Spend)
.index(BigInteger.valueOf(1))
Expand All @@ -63,11 +89,144 @@ void generate() throws CborException, CborSerializationException {
assertThat(hash).isEqualTo("57240d358f8ab6128c4a66340271e4fec39b4971232add308f01a5809313adcf");
}

@Test
void generate_plutusV1_eraSet_shouldNotUseBabbageEra() throws CborException, CborSerializationException { //negative test
ListPlutusData listPlutusData = new ListPlutusData();
PlutusData plutusData = new BigIntPlutusData(new BigInteger("1000"));
listPlutusData.add(plutusData);

Redeemer redeemer = Redeemer.builder()
.tag(RedeemerTag.Spend)
.index(BigInteger.valueOf(1))
.data(new BigIntPlutusData(new BigInteger("2000")))
.exUnits(ExUnits.builder()
.mem(BigInteger.valueOf(0))
.steps(BigInteger.valueOf(0))
.build()
).build();

byte[] hashBytes = ScriptDataHashGenerator.generate(Era.Conway, Arrays.asList(redeemer), Arrays.asList(plutusData), costMdls);
String hash = HexUtil.encodeHexString(hashBytes);
System.out.println(hash);

assertThat(hash).isNotEqualTo("57240d358f8ab6128c4a66340271e4fec39b4971232add308f01a5809313adcf");
}

@Test
void generate_plutusV2() throws CborException, CborSerializationException {
ListPlutusData listPlutusData = new ListPlutusData();
PlutusData plutusData = new BigIntPlutusData(new BigInteger("1000"));
listPlutusData.add(plutusData);

Redeemer redeemer = Redeemer.builder()
.tag(RedeemerTag.Spend)
.index(BigInteger.valueOf(1))
.data(new BigIntPlutusData(new BigInteger("2000")))
.exUnits(ExUnits.builder()
.mem(BigInteger.valueOf(0))
.steps(BigInteger.valueOf(0))
.build()
).build();

var _costMdls = new CostMdls();
_costMdls.add(PlutusV2CostModel);

byte[] hashBytes = ScriptDataHashGenerator.generate(Arrays.asList(redeemer), Arrays.asList(plutusData), _costMdls);
String hash = HexUtil.encodeHexString(hashBytes);
System.out.println(hash);

assertThat(hash).isEqualTo("83d39add124e06e9cf8c4fee28c8b8063932c4bdfc3d4900fb08f49beffef601");
}

@Test
void generate_plutusV2_eraSetToBabbage() throws CborException, CborSerializationException { //negative test
ListPlutusData listPlutusData = new ListPlutusData();
PlutusData plutusData = new BigIntPlutusData(new BigInteger("1000"));
listPlutusData.add(plutusData);

Redeemer redeemer = Redeemer.builder()
.tag(RedeemerTag.Spend)
.index(BigInteger.valueOf(1))
.data(new BigIntPlutusData(new BigInteger("2000")))
.exUnits(ExUnits.builder()
.mem(BigInteger.valueOf(0))
.steps(BigInteger.valueOf(0))
.build()
).build();

var _costMdls = new CostMdls();
_costMdls.add(PlutusV2CostModel);

byte[] hashBytes = ScriptDataHashGenerator.generate(Era.Babbage, Arrays.asList(redeemer), Arrays.asList(plutusData), _costMdls);
String hash = HexUtil.encodeHexString(hashBytes);
System.out.println(hash);

assertThat(hash).isNotEqualTo("83d39add124e06e9cf8c4fee28c8b8063932c4bdfc3d4900fb08f49beffef601");
}

@Test
void generate_plutusV2_eraSetToConway() throws CborException, CborSerializationException {
ListPlutusData listPlutusData = new ListPlutusData();
PlutusData plutusData = new BigIntPlutusData(new BigInteger("1000"));
listPlutusData.add(plutusData);

Redeemer redeemer = Redeemer.builder()
.tag(RedeemerTag.Spend)
.index(BigInteger.valueOf(1))
.data(new BigIntPlutusData(new BigInteger("2000")))
.exUnits(ExUnits.builder()
.mem(BigInteger.valueOf(0))
.steps(BigInteger.valueOf(0))
.build()
).build();

var _costMdls = new CostMdls();
_costMdls.add(PlutusV2CostModel);

byte[] hashBytes = ScriptDataHashGenerator.generate(Era.Conway, Arrays.asList(redeemer), Arrays.asList(plutusData), _costMdls);
String hash = HexUtil.encodeHexString(hashBytes);
System.out.println(hash);

assertThat(hash).isEqualTo("83d39add124e06e9cf8c4fee28c8b8063932c4bdfc3d4900fb08f49beffef601");
}

@Test
void generate_plutusV2_emptyRedeemer() throws CborException, CborSerializationException {
ListPlutusData listPlutusData = new ListPlutusData();
PlutusData plutusData = new BigIntPlutusData(new BigInteger("1000"));
listPlutusData.add(plutusData);

var _costMdls = new CostMdls();
_costMdls.add(PlutusV2CostModel);

byte[] hashBytes = ScriptDataHashGenerator.generate(Era.Conway, Arrays.asList(), Arrays.asList(plutusData), _costMdls);
String hash = HexUtil.encodeHexString(hashBytes);
System.out.println(hash);

assertThat(hash).isEqualTo("62d142288edfbd6b5bceec2bd476a22e6856c0904fcf26116fc4dd54440c038e");
}

@Test
void generate_plutusV1_emptyRedeemer() throws CborException, CborSerializationException {
ListPlutusData listPlutusData = new ListPlutusData();
PlutusData plutusData = new BigIntPlutusData(new BigInteger("1000"));
listPlutusData.add(plutusData);

var _costMdls = new CostMdls();
_costMdls.add(PlutusV1CostModel);

byte[] hashBytes = ScriptDataHashGenerator.generate(Arrays.asList(), Arrays.asList(plutusData), _costMdls);
String hash = HexUtil.encodeHexString(hashBytes);
System.out.println(hash);

assertThat(hash).isEqualTo("d6e05995f657a2bf1a3dd246756733f9e680487df08c5d958617e5eec70b9fc2");
}

@Test
void generate_emptyRedemeers_nonEmptyDatum() throws Exception {
PlutusData plutusData = PlutusData.deserialize(HexUtil.decodeHexString("d8799f4114d8799fd8799fd8799fd8799f581c3050f6f4d5981748bc3a2b84d8165b20c100a75057b6593befd9323cffd8799fd8799fd8799f581cc5cdc99429b4ce659f2542994c48b6c801f0b8e21ca7fb586326a545ffffffffd87a80ffd87a80ff1a002625a0d8799fd879801a0025a559d8799f01ffffff"));

byte[] scriptDataHash = ScriptDataHashGenerator.generate(Arrays.asList(), Arrays.asList(plutusData),
byte[] scriptDataHash = ScriptDataHashGenerator.generate(Era.Babbage, Arrays.asList(), Arrays.asList(plutusData),
costMdls);

byte[] expected = new byte[]{71, -22, -92, 74, -39, 124, 55, -108, 120, -127, -125, 119, 41, -77, 48, -72, 121, 0, -10, -77, -29, 103, -99, -9, -111, -118, 11, -126, -52, -29, -81, 105};
Expand All @@ -78,7 +237,7 @@ void generate_emptyRedemeers_nonEmptyDatum() throws Exception {
void generate_nullRedemeers_nonEmptyDatum() throws Exception {
PlutusData plutusData = PlutusData.deserialize(HexUtil.decodeHexString("d8799f4114d8799fd8799fd8799fd8799f581c3050f6f4d5981748bc3a2b84d8165b20c100a75057b6593befd9323cffd8799fd8799fd8799f581cc5cdc99429b4ce659f2542994c48b6c801f0b8e21ca7fb586326a545ffffffffd87a80ffd87a80ff1a002625a0d8799fd879801a0025a559d8799f01ffffff"));

byte[] scriptDataHash = ScriptDataHashGenerator.generate(null, Arrays.asList(plutusData),
byte[] scriptDataHash = ScriptDataHashGenerator.generate(Era.Babbage, null, Arrays.asList(plutusData),
costMdls);

byte[] expected = new byte[]{71, -22, -92, 74, -39, 124, 55, -108, 120, -127, -125, 119, 41, -77, 48, -72, 121, 0, -10, -77, -29, 103, -99, -9, -111, -118, 11, -126, -52, -29, -81, 105};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,12 @@
import com.bloxbean.cardano.client.plutus.spec.CostMdls;
import com.bloxbean.cardano.client.plutus.spec.Language;
import com.bloxbean.cardano.client.plutus.spec.PlutusScript;
import com.bloxbean.cardano.client.spec.Era;
import com.bloxbean.cardano.client.transaction.spec.MultiAsset;
import com.bloxbean.cardano.client.transaction.spec.Transaction;
import com.bloxbean.cardano.client.util.HexUtil;
import com.bloxbean.cardano.client.util.Tuple;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.*;

import java.util.*;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -56,6 +54,10 @@ public class TxBuilderContext {
@Setter(AccessLevel.NONE)
private boolean mergeOutputs = true;

@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private Era serializationEra;

public TxBuilderContext(UtxoSupplier utxoSupplier, ProtocolParamsSupplier protocolParamsSupplier) {
this(utxoSupplier, protocolParamsSupplier.getProtocolParams());
}
Expand Down Expand Up @@ -163,6 +165,24 @@ public TxBuilderContext mergeOutputs(boolean mergeOutputs) {
return this;
}

/**
* Set the serialization era for the transaction
* @param era
* @return TxBuilderContext
*/
public TxBuilderContext withSerializationEra(Era era) {
this.serializationEra = era;
return this;
}

/**
* Get the serialization era for the transaction
* @return Era or null if not set
*/
public Era getSerializationEra() {
return serializationEra;
}

/**
* @deprecated
* Use {@link #withCostMdls(CostMdls)} instead
Expand Down Expand Up @@ -235,6 +255,7 @@ public static TxBuilderContext init(UtxoSupplier utxoSupplier, ProtocolParamsSup
*/
public Transaction build(TxBuilder txBuilder) {
Transaction transaction = new Transaction();
transaction.setEra(getSerializationEra());
txBuilder.apply(this, transaction);
clearTempStates();
return transaction;
Expand Down
Loading

0 comments on commit 7d12d2f

Please sign in to comment.