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

Adjust the fee calculation when reference script bytes are present in the input UTXOs of a transaction #455

Merged
merged 9 commits into from
Oct 15, 2024
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
package com.bloxbean.cardano.client.spec;

import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.Array;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.UnsignedInteger;
import com.bloxbean.cardano.client.common.cbor.CborSerializationUtil;
import com.bloxbean.cardano.client.exception.CborRuntimeException;
import com.bloxbean.cardano.client.exception.CborSerializationException;
import com.bloxbean.cardano.client.util.HexUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
Expand All @@ -33,26 +27,6 @@ default byte[] serialize() throws CborSerializationException {
return finalBytes;
}

/**
* Get serialized bytes for script reference. This is used in TransactionOutput's script_ref
* @return
* @throws CborSerializationException
*/
default byte[] scriptRefBytes() throws CborSerializationException {
int type = getScriptType();
byte[] serializedBytes = serializeScriptBody();

Array array = new Array();
array.add(new UnsignedInteger(type));
array.add(new ByteString(serializedBytes));

try {
return CborSerializationUtil.serialize(array);
} catch (CborException e) {
throw new CborRuntimeException(e);
}
}

@JsonIgnore
default byte[] getScriptHash() throws CborSerializationException {
return blake2bHash224(serialize());
Expand All @@ -63,6 +37,13 @@ default String getPolicyId() throws CborSerializationException {
return HexUtil.encodeHexString(getScriptHash());
}

/**
* Get serialized bytes for script reference. This is used in TransactionOutput's script_ref
* @return byte[]
* @throws CborSerializationException
*/
byte[] scriptRefBytes() throws CborSerializationException;

DataItem serializeAsDataItem() throws CborSerializationException;
byte[] serializeScriptBody() throws CborSerializationException;
@JsonIgnore
Expand Down
33 changes: 33 additions & 0 deletions common/src/main/java/com/bloxbean/cardano/client/util/Try.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.bloxbean.cardano.client.util;

import java.util.function.Supplier;

/**
* A utility class that encapsulates a computation that may either result in a successful value
* or throw an exception. This class provides a way to handle checked exceptions in a functional
Expand Down Expand Up @@ -59,6 +61,37 @@ public T get() {
return result;
}

/**
* Returns the result of the computation if successful, or the specified default value
* if an exception occurred during the computation.
*
* @param defaultValue The value to return if the computation resulted in an exception.
* @return The result of the computation, or the provided default value if an exception occurred.
*/
public T getOrElse(T defaultValue) {
if (exception != null) {
return defaultValue;
}
return result;
}

/**
* Returns the result of the computation if successful, or throws the specified exception
* if an exception was thrown during the computation.
*
* @param exceptionSupplier A Supplier that provides the exception to be thrown if
* the computation resulted in an exception.
* @return The result of the computation.
* @throws RuntimeException The exception provided by the exceptionSupplier if the
* computation resulted in an exception.
*/
public T orElseThrow(Supplier<? extends RuntimeException> exceptionSupplier) {
if (exception != null) {
throw exceptionSupplier.get();
}
return result;
}

/**
* Returns the exception that was thrown during the computation, if any.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
package com.bloxbean.cardano.client.api.util;

import co.nstant.in.cbor.model.Array;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.UnsignedInteger;
import com.bloxbean.cardano.client.api.ScriptSupplier;
import com.bloxbean.cardano.client.api.UtxoSupplier;
import com.bloxbean.cardano.client.api.model.Utxo;
import com.bloxbean.cardano.client.common.cbor.CborSerializationUtil;
import com.bloxbean.cardano.client.exception.CborRuntimeException;
import com.bloxbean.cardano.client.plutus.spec.PlutusScript;
import com.bloxbean.cardano.client.spec.Script;
import com.bloxbean.cardano.client.transaction.spec.TransactionInput;
import com.bloxbean.cardano.client.transaction.spec.script.NativeScript;
import com.bloxbean.cardano.client.util.Try;
import com.bloxbean.cardano.client.transaction.spec.Transaction;

import java.util.List;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;

/**
* Utility class for handling reference scripts in transactions.
*/
public class ReferenceScriptUtil {

/**
Expand All @@ -18,19 +29,29 @@ public class ReferenceScriptUtil {
* @param utxoSupplier utxo supplier
* @param scriptSupplier script supplier
* @param transaction transaction
* @return size of the reference scripts
* @return size of the reference scripts in transaction's reference inputs
*/
public static long fetchAndCalculateReferenceScriptsSize(UtxoSupplier utxoSupplier, ScriptSupplier scriptSupplier, Transaction transaction) {
return transaction.getBody().getReferenceInputs()
.stream()
public static long totalRefScriptsSizeInRefInputs(UtxoSupplier utxoSupplier, ScriptSupplier scriptSupplier, Transaction transaction) {
return totalRefScriptsSizeInInputs(utxoSupplier, scriptSupplier, new HashSet<>(transaction.getBody().getReferenceInputs()));
}

/**
* Fetches and calculates the size of reference scripts in given inputs using UTXO supplier and script supplier.
*
* @param utxoSupplier - utxo supplier
* @param scriptSupplier - script supplier
* @param inputs the set of transaction inputs for which to fetch and calculate reference script sizes
* @return the total size of the reference scripts in bytes
*/
public static long totalRefScriptsSizeInInputs(UtxoSupplier utxoSupplier, ScriptSupplier scriptSupplier, Set<TransactionInput> inputs) {
return inputs.stream()
.flatMap(refInput -> utxoSupplier.getTxOutput(refInput.getTransactionId(), refInput.getIndex()).stream()
.map(utxo -> scriptSupplier.getScript(utxo.getReferenceScriptHash()))
.flatMap(Optional::stream))
.map(script -> Try.of(() -> script.scriptRefBytes().length))
.filter(Try::isSuccess)
.mapToLong(Try::get)
.sum();

}

/**
Expand All @@ -40,13 +61,61 @@ public static long fetchAndCalculateReferenceScriptsSize(UtxoSupplier utxoSuppli
* @param transaction transaction
* @return list of {@link PlutusScript}
*/
public static List<PlutusScript> resolveReferenceScripts(UtxoSupplier utxoSupplier, ScriptSupplier scriptSupplier, Transaction transaction) {
return transaction.getBody().getReferenceInputs()
public static List<PlutusScript> resolveReferenceScripts(UtxoSupplier utxoSupplier, ScriptSupplier scriptSupplier, Transaction transaction, Set<Utxo> inputUtxos) {
var refInputPlutusScripts = transaction.getBody().getReferenceInputs()
.stream()
.flatMap(refInput -> utxoSupplier.getTxOutput(refInput.getTransactionId(), refInput.getIndex()).stream()
.map(utxo -> scriptSupplier.getScript(utxo.getReferenceScriptHash()))
.flatMap(Optional::stream))
.collect(Collectors.toList());

var inputPlutusScripts = transaction.getBody().getInputs()
.stream()
.flatMap(input -> inputUtxos.stream()
.filter(utxo -> utxo.getTxHash().equals(input.getTransactionId())
&& utxo.getOutputIndex() == input.getIndex()
&& utxo.getReferenceScriptHash() != null)
.findFirst()
.map(utxo -> scriptSupplier.getScript(utxo.getReferenceScriptHash())).stream()
)
.flatMap(Optional::stream)
.collect(Collectors.toList());

if (refInputPlutusScripts.isEmpty() && inputPlutusScripts.isEmpty())
return Collections.emptyList();
else {
Set<PlutusScript> allPlutusScripts = new HashSet<>();
allPlutusScripts.addAll(refInputPlutusScripts);
allPlutusScripts.addAll(inputPlutusScripts);

return allPlutusScripts.stream().collect(Collectors.toList());
}
}

public static Script deserializeScriptRef(byte[] scriptRefBytes) {
Array scriptArray = (Array) CborSerializationUtil.deserialize(scriptRefBytes);

List<DataItem> dataItemList = scriptArray.getDataItems();
if (dataItemList == null || dataItemList.size() == 0 || dataItemList.size() < 2) {
throw new CborRuntimeException("Reference Script deserialization failed. Invalid no of DataItem : " + dataItemList.size());
}

int type = ((UnsignedInteger) dataItemList.get(0)).getValue().intValue();

try {
switch (type) {
case 0:
return NativeScript.deserializeScriptRef(scriptRefBytes);
case 1:
case 2:
case 3:
return PlutusScript.deserializeScriptRef(scriptRefBytes);
default:
throw new CborRuntimeException("Invalid script type : " + type);
}
} catch (Exception e) {
throw new CborRuntimeException("Reference Script deserialization failed.", e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
import com.bloxbean.cardano.client.api.UtxoSupplier;
import com.bloxbean.cardano.client.api.model.Utxo;
import com.bloxbean.cardano.client.exception.CborSerializationException;
import com.bloxbean.cardano.client.plutus.spec.PlutusV1Script;
import com.bloxbean.cardano.client.plutus.spec.PlutusV2Script;
import com.bloxbean.cardano.client.plutus.spec.PlutusV3Script;
import com.bloxbean.cardano.client.transaction.spec.Transaction;
import com.bloxbean.cardano.client.transaction.spec.TransactionBody;
import com.bloxbean.cardano.client.transaction.spec.TransactionInput;
import com.bloxbean.cardano.client.transaction.spec.script.NativeScript;
import com.bloxbean.cardano.client.transaction.spec.script.ScriptAtLeast;
import com.bloxbean.cardano.client.util.HexUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
Expand All @@ -29,7 +34,7 @@ class ReferenceScriptUtilTest {
private ScriptSupplier scriptSupplier;

@Test
void fetchAndCalculateReferenceScriptsSize() throws CborSerializationException {
void totalRefScriptsSizeInRefInputs() throws CborSerializationException {

var transactionBody = TransactionBody.builder()
.referenceInputs(List.of(
Expand Down Expand Up @@ -77,15 +82,15 @@ void fetchAndCalculateReferenceScriptsSize() throws CborSerializationException {
given(scriptSupplier.getScript("script3")).willReturn(
Optional.of(script3));

var refScriptsSize = ReferenceScriptUtil.fetchAndCalculateReferenceScriptsSize(utxoSupplier, scriptSupplier, transaction);
var refScriptsSize = ReferenceScriptUtil.totalRefScriptsSizeInRefInputs(utxoSupplier, scriptSupplier, transaction);

long expectedSize = script1.scriptRefBytes().length + script2.scriptRefBytes().length + script3.scriptRefBytes().length;

assertThat(refScriptsSize).isEqualTo(expectedSize);
}

@Test
void fetchAndCalculateReferenceScriptsSize_includesNoScriptInTx() throws CborSerializationException {
void totalRefScriptsSizeInRefInputs_includesNoScriptInTx() throws CborSerializationException {

var transactionBody = TransactionBody.builder()
.referenceInputs(List.of(
Expand Down Expand Up @@ -125,15 +130,15 @@ void fetchAndCalculateReferenceScriptsSize_includesNoScriptInTx() throws CborSer
given(scriptSupplier.getScript("script3")).willReturn(
Optional.of(script3));

var refScriptsSize = ReferenceScriptUtil.fetchAndCalculateReferenceScriptsSize(utxoSupplier, scriptSupplier, transaction);
var refScriptsSize = ReferenceScriptUtil.totalRefScriptsSizeInRefInputs(utxoSupplier, scriptSupplier, transaction);

long expectedSize = script1.scriptRefBytes().length + script3.scriptRefBytes().length;

assertThat(refScriptsSize).isEqualTo(expectedSize);
}

@Test
void fetchAndCalculateReferenceScriptsSize_singleScript() throws CborSerializationException {
void totalRefScriptsSizeInRefInputs_singleScript() throws CborSerializationException {

var transactionBody = TransactionBody.builder()
.referenceInputs(List.of(
Expand All @@ -157,15 +162,15 @@ void fetchAndCalculateReferenceScriptsSize_singleScript() throws CborSerializati
given(scriptSupplier.getScript("script1")).willReturn(
Optional.of(script1));

var refScriptsSize = ReferenceScriptUtil.fetchAndCalculateReferenceScriptsSize(utxoSupplier, scriptSupplier, transaction);
var refScriptsSize = ReferenceScriptUtil.totalRefScriptsSizeInRefInputs(utxoSupplier, scriptSupplier, transaction);

long expectedSize = script1.scriptRefBytes().length;

assertThat(refScriptsSize).isEqualTo(expectedSize);
}

@Test
void fetchAndCalculateReferenceScriptsSize_noScript() throws CborSerializationException {
void totalRefScriptsSizeInRefInputs_noScript() throws CborSerializationException {

var transactionBody = TransactionBody.builder()
.referenceInputs(List.of(
Expand All @@ -180,8 +185,75 @@ void fetchAndCalculateReferenceScriptsSize_noScript() throws CborSerializationEx
.willReturn(Optional.of(Utxo.builder()
.build()));

var refScriptsSize = ReferenceScriptUtil.fetchAndCalculateReferenceScriptsSize(utxoSupplier, scriptSupplier, transaction);
var refScriptsSize = ReferenceScriptUtil.totalRefScriptsSizeInRefInputs(utxoSupplier, scriptSupplier, transaction);

assertThat(refScriptsSize).isEqualTo(0);
}

@Test
void deserializeScriptRef_plutusV1() throws CborSerializationException {
PlutusV1Script plutusScript = PlutusV1Script.builder()
.type("PlutusScriptV1")
.cborHex("4e4d01000033222220051200120011")
.build();

var scriptRefBytes = plutusScript.scriptRefBytes();

var script = ReferenceScriptUtil.deserializeScriptRef(scriptRefBytes);

assertThat(script).isInstanceOf(PlutusV1Script.class);
assertThat(script).isEqualTo(plutusScript);
}

@Test
void deserializeScriptRef_plutusV2() throws CborSerializationException {
PlutusV2Script plutusScript = PlutusV2Script.builder()
.type("PlutusScriptV2")
.cborHex("49480100002221200101")
.build();

var scriptRefBytes = plutusScript.scriptRefBytes();

var script = ReferenceScriptUtil.deserializeScriptRef(scriptRefBytes);

assertThat(script).isInstanceOf(PlutusV2Script.class);
assertThat(script).isEqualTo(plutusScript);
}

@Test
void deserializeScriptRef_plutusV3() throws CborSerializationException {
PlutusV3Script plutusScript = PlutusV3Script.builder()
.type("PlutusScriptV3")
.cborHex("46450101002499")
.build();

var scriptRefBytes = plutusScript.scriptRefBytes();

var script = ReferenceScriptUtil.deserializeScriptRef(scriptRefBytes);

assertThat(script).isInstanceOf(PlutusV3Script.class);
assertThat(script).isEqualTo(plutusScript);
}

@Test
void deserializeScriptRef_nativeScript() throws CborSerializationException {
var policy = PolicyUtil.createMultiSigScriptAtLeastPolicy("test", 2, 1);

var scriptRefBytes = policy.getPolicyScript().scriptRefBytes();

var script = ReferenceScriptUtil.deserializeScriptRef(scriptRefBytes);

assertThat(script).isInstanceOf(NativeScript.class);
assertThat(script).isInstanceOf(ScriptAtLeast.class);
assertThat(script).isEqualTo(policy.getPolicyScript());
}

@Test
void deserializeScriptRef_nativeScriptBytes() {
String scriptRefBytesHex = "82008200581ca0dfc5656b946dea62a1fc23ff8881eb6468fdc14c295e2839c3ece3";

var script = ReferenceScriptUtil.deserializeScriptRef(HexUtil.decodeHexString(scriptRefBytesHex));

assertThat(script).isInstanceOf(NativeScript.class);
}
}
Loading
Loading