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
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
Expand Up @@ -2,14 +2,18 @@

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.plutus.spec.PlutusScript;
import com.bloxbean.cardano.client.transaction.spec.TransactionInput;
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 +22,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 +54,35 @@ 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());
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,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 +77,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 +125,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 +157,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,7 +180,7 @@ 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,19 @@ public List<PlutusScript> getRefPlutusScripts() {
.collect(Collectors.toList());
}

public Set<String> getRefScriptHashes() {
var refScriptInUtxos = utxos.stream()
.filter(utxo -> utxo.getReferenceScriptHash() != null)
.map(utxo -> utxo.getReferenceScriptHash())
.collect(Collectors.toSet());

var hashes = new HashSet<String>();
hashes.addAll(refScripts.keySet());
hashes.addAll(refScriptInUtxos);

return hashes;
}

public List<Language> getRefScriptLanguages() {
if (refScripts.size() == 0)
return Collections.emptyList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ private static void adjust(TxBuilderContext context, Transaction transaction, Tr
transaction.getBody().getInputs()
.add(new TransactionInput(utxo.getTxHash(), utxo.getOutputIndex()));
UtxoUtil.copyUtxoValuesToOutput(outputToAdjust, utxo);
context.addUtxo(utxo);
});

//As transaction is changed now, fee calculation is required.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.bloxbean.cardano.client.function.helper;

import com.bloxbean.cardano.client.function.TxBuilder;
import com.bloxbean.cardano.client.plutus.spec.PlutusScript;
import com.bloxbean.cardano.client.util.HexUtil;
import com.bloxbean.cardano.client.util.Try;

import java.util.List;
import java.util.Set;

/**
* Helper class to provide a {@link TxBuilder} to remove duplicate script witnesses from the transaction.
* This is useful when you have the same script in witness set and as reference script in transaction input
* or transaction reference input.
* In case of such scenario or duplicate witness script, tx submission will fail with error. (ExtraneousScriptWitnessesUTXOW)
*/
public class DuplicateScriptWitnessChecker {

/**
* Returns a {@link TxBuilder} to remove duplicate script witnesses from the transaction.
* It checks if the script is present as reference script bytes in a transaction input or reference input.
* If yes, it removes the script from witness set.
*
* @return TxBuilder
*/
public static TxBuilder removeDuplicateScriptWitnesses() {
return (context, txn) -> {
Set<String> refScriptHashes = context.getRefScriptHashes();

//Remove duplicate script from witness set
if (refScriptHashes != null && !refScriptHashes.isEmpty()) {
removeDuplicateScripts(txn.getWitnessSet().getPlutusV1Scripts(), refScriptHashes);
removeDuplicateScripts(txn.getWitnessSet().getPlutusV2Scripts(), refScriptHashes);
removeDuplicateScripts(txn.getWitnessSet().getPlutusV3Scripts(), refScriptHashes);
}

};
}

private static void removeDuplicateScripts(List<? extends PlutusScript> scripts, Set<String> refScriptHashes) {
if (scripts == null || scripts.isEmpty())
return;

scripts.removeIf(plutusScript ->
Try.of(() -> refScriptHashes.contains(HexUtil.encodeHexString(plutusScript.getScriptHash())))
.getOrElse(false)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,47 @@ private static void execute(TxBuilderContext context, Transaction transaction, S
}
}

//Check if script transaction (i.e; script datashash is set) and any input utxos has reference script.
//If yes, we need to calculate reference script fee for input utxos as well
//https://github.com/bloxbean/cardano-client-lib/issues/450
long totalRefScriptBytesInInputs = 0;
if (transaction.getBody().getScriptDataHash() != null && context.getUtxos() != null) {
//Find the inputs with reference script hash, but reference script is not there in the context
var inputWithScriptRefToBeFetched = context.getUtxos().stream()
.filter(utxo -> utxo.getReferenceScriptHash() != null)
.filter(utxo -> context.getRefScript(utxo.getReferenceScriptHash()).isEmpty())
.map(utxo -> new TransactionInput(utxo.getTxHash(), utxo.getOutputIndex()))
.collect(Collectors.toSet());

//Find the size of all available reference script bytes in the context for the inputs
var inputRefScriptSize = context.getUtxos().stream()
.filter(utxo -> utxo.getReferenceScriptHash() != null)
.flatMap(utxo -> context.getRefScript(utxo.getReferenceScriptHash()).stream())
.mapToLong(bytes -> bytes.length)
.sum();

//Fetch the missing reference scripts and calculate the size
if (inputWithScriptRefToBeFetched != null && inputWithScriptRefToBeFetched.size() > 0) {
totalRefScriptBytesInInputs = inputRefScriptSize + ReferenceScriptUtil.totalRefScriptsSizeInInputs(
context.getUtxoSupplier(),
context.getScriptSupplier(),
inputWithScriptRefToBeFetched);
} else {
totalRefScriptBytesInInputs = inputRefScriptSize;
}
}

BigInteger refScriptFee = BigInteger.ZERO;
if (transaction.getBody().getReferenceInputs() != null && transaction.getBody().getReferenceInputs().size() > 0) {
var refScripts = context.getRefScripts();
if (refScripts == null || refScripts.size() == 0) {
if (context.getScriptSupplier() != null) {
long totalRefScriptsBytes =
ReferenceScriptUtil.fetchAndCalculateReferenceScriptsSize(
ReferenceScriptUtil.totalRefScriptsSizeInRefInputs(
context.getUtxoSupplier(),
context.getScriptSupplier(),
transaction);
refScriptFee = feeCalculationService.tierRefScriptFee(totalRefScriptsBytes);
refScriptFee = feeCalculationService.tierRefScriptFee(totalRefScriptsBytes + totalRefScriptBytesInInputs);
} else {
log.debug("Script supplier is required to calculate reference script fee. " +
"Alternatively, you can set reference scripts during building the transaction.");
Expand All @@ -110,8 +140,11 @@ private static void execute(TxBuilderContext context, Transaction transaction, S
int totalRefScriptBytes = refScripts.stream()
.mapToInt(byteArray -> byteArray.length)
.sum();
refScriptFee = feeCalculationService.tierRefScriptFee(totalRefScriptBytes);
refScriptFee = feeCalculationService.tierRefScriptFee(totalRefScriptBytes + totalRefScriptBytesInInputs);
}
} else {
if (totalRefScriptBytesInInputs > 0)
refScriptFee = feeCalculationService.tierRefScriptFee(totalRefScriptBytesInInputs);
}

BigInteger totalFee = baseFee.add(scriptFee).add(refScriptFee);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@
import com.bloxbean.cardano.client.function.TxBuilder;

/**
* Helper class to return a {@link TxBuilder} to resolve reference scripts in tx's reference inputs
* Helper class to return a {@link TxBuilder} to resolve reference scripts in tx's reference inputs and inputs
*/
public class ReferenceScriptResolver {

public static TxBuilder resolveReferenceScript() {
return (context, txn) -> {
var refInputs = txn.getBody().getReferenceInputs();
if (refInputs == null || refInputs.isEmpty()) {
return;
}
var inputUtxos = context.getUtxos();

ReferenceScriptUtil.resolveReferenceScripts(context.getUtxoSupplier(), context.getScriptSupplier(), txn)
ReferenceScriptUtil.resolveReferenceScripts(context.getUtxoSupplier(), context.getScriptSupplier(), txn, inputUtxos)
.forEach(script -> {
if (script != null) {
context.addRefScripts(script);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,11 @@ public static void calculateScriptDataHash(TxBuilderContext ctx, Transaction tra
costMdls.add(costModel.orElse(PlutusV3CostModel));
}

if (transaction.getBody().getReferenceInputs() != null
&& transaction.getBody().getReferenceInputs().size() > 0) { //If reference input is there, the check the language from that
if (!ctx.getRefScriptLanguages().isEmpty()) {
for (Language language : ctx.getRefScriptLanguages()) {
Optional<CostModel> costModel =
CostModelUtil.getCostModelFromProtocolParams(ctx.getProtocolParams(), language);
costMdls.add(costModel.orElseThrow(() -> new IllegalArgumentException("Cost model not found for language : " + language)));
}
if (!ctx.getRefScriptLanguages().isEmpty()) {
for (Language language : ctx.getRefScriptLanguages()) {
Optional<CostModel> costModel =
CostModelUtil.getCostModelFromProtocolParams(ctx.getProtocolParams(), language);
costMdls.add(costModel.orElseThrow(() -> new IllegalArgumentException("Cost model not found for language : " + language)));
}
}
}
Expand Down
Loading
Loading