Skip to content

Commit

Permalink
Merge pull request #1559 from input-output-hk/feat/lw-12066-fee-calcu…
Browse files Browse the repository at this point in the history
…lation-for-spending-refer-script
  • Loading branch information
AngelCastilloB authored Jan 16, 2025
2 parents ea0b2c2 + 33527d5 commit 1dda3e5
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 56 deletions.
8 changes: 7 additions & 1 deletion packages/input-selection/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,13 @@ export interface InputSelector {

export type ProtocolParametersForInputSelection = Pick<
Cardano.ProtocolParameters,
'coinsPerUtxoByte' | 'maxTxSize' | 'maxValueSize' | 'minFeeCoefficient' | 'minFeeConstant' | 'prices'
| 'coinsPerUtxoByte'
| 'maxTxSize'
| 'maxValueSize'
| 'minFeeCoefficient'
| 'minFeeConstant'
| 'prices'
| 'minFeeRefScriptCostPerByte'
>;

export type ProtocolParametersRequiredByInputSelection = Required<{
Expand Down
96 changes: 66 additions & 30 deletions packages/tx-construction/src/fees/fees.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Cardano, Serialization } from '@cardano-sdk/core';
import { OpaqueNumber } from '@cardano-sdk/util';
import { ProtocolParametersForInputSelection } from '@cardano-sdk/input-selection';

/**
* The constant overhead of 160 bytes accounts for the transaction input and the entry in the UTxO map data
Expand Down Expand Up @@ -52,34 +52,69 @@ const getTotalExUnits = (redeemers: Cardano.Redeemer[]): Cardano.ExUnits => {
};

/**
* Gets the minimum fee incurred by the scripts on the transaction.
* Starting in the Conway era, the ref script min fee calculation is given by the total size (in bytes) of
* reference scripts priced according to a different, growing tiered pricing model.
* See https://github.com/CardanoSolutions/ogmios/releases/tag/v6.5.0
*
* @param tx The transaction to compute the min script fee from.
* @param exUnitsPrice The prices of the execution units.
* @param tx The transaction to compute the min ref script fee from.
* @param resolvedInputs The resolved inputs of the transaction.
* @param coinsPerRefScriptByte The price per byte of the reference script.
*/
const minScriptFee = (tx: Cardano.Tx, exUnitsPrice: Cardano.Prices): bigint => {
if (!tx.witness.redeemers) return BigInt(0);
const minRefScriptFee = (tx: Cardano.Tx, resolvedInputs: Cardano.Utxo[], coinsPerRefScriptByte: number): bigint => {
if (coinsPerRefScriptByte === 0) return BigInt(0);

const totalExUnits = getTotalExUnits(tx.witness.redeemers);
let base: number = coinsPerRefScriptByte;
const range = 25_600;
const multiplier = 1.2;

let totalRefScriptsSize = 0;

const totalInputs = [...tx.body.inputs, ...(tx.body.referenceInputs ?? [])];
for (const output of totalInputs) {
const resolvedInput = resolvedInputs.find(
(input) => input[0].txId === output.txId && input[0].index === output.index
);

return BigInt(Math.ceil(totalExUnits.steps * exUnitsPrice.steps + totalExUnits.memory * exUnitsPrice.memory));
if (resolvedInput && resolvedInput[1].scriptReference) {
totalRefScriptsSize += Serialization.Script.fromCore(resolvedInput[1].scriptReference).toCbor().length / 2;
}
}

let scriptRefFee = 0;
while (totalRefScriptsSize > 0) {
scriptRefFee += Math.ceil(Math.min(range, totalRefScriptsSize) * base);
totalRefScriptsSize = Math.max(totalRefScriptsSize - range, 0);
base *= multiplier;
}

return BigInt(scriptRefFee);
};

/**
* The value of the min fee constant is a payable fee, regardless of the size of the transaction. This parameter was
* primarily introduced to prevent Distributed-Denial-of-Service (DDoS) attacks. This constant makes such attacks
* prohibitively expensive, and eliminates the possibility of an attacker generating millions of small transactions
* to flood and crash the system.
* Gets the minimum fee incurred by the scripts on the transaction.
*
* @param tx The transaction to compute the min script fee from.
* @param exUnitsPrice The prices of the execution units.
* @param resolvedInputs The resolved inputs of the transaction.
* @param coinsPerRefScriptByte The price per byte of the reference script.
*/
export type MinFeeConstant = OpaqueNumber<'MinFeeConstant'>;
export const MinFeeConstant = (value: number): MinFeeConstant => value as unknown as MinFeeConstant;
const minScriptFee = (
tx: Cardano.Tx,
exUnitsPrice: Cardano.Prices,
resolvedInputs: Cardano.Utxo[],
coinsPerRefScriptByte: number
): bigint => {
const scriptRefFee = minRefScriptFee(tx, resolvedInputs, coinsPerRefScriptByte);

/**
* Min fee coefficient reflects the dependence of the transaction cost on the size of the transaction. The larger
* the transaction, the more resources are needed to store and process it.
*/
export type MinFeeCoefficient = OpaqueNumber<'MinFeeCoefficient'>;
export const MinFeeCoefficient = (value: number): MinFeeCoefficient => value as unknown as MinFeeCoefficient;
if (!tx.witness.redeemers) return BigInt(scriptRefFee);

const totalExUnits = getTotalExUnits(tx.witness.redeemers);

return (
BigInt(Math.ceil(totalExUnits.steps * exUnitsPrice.steps + totalExUnits.memory * exUnitsPrice.memory)) +
scriptRefFee
);
};

/**
* Gets the minimum fee incurred by the transaction size.
Expand All @@ -88,7 +123,7 @@ export const MinFeeCoefficient = (value: number): MinFeeCoefficient => value as
* @param minFeeConstant The prices of the execution units.
* @param minFeeCoefficient The prices of the execution units.
*/
const minNoScriptFee = (tx: Cardano.Tx, minFeeConstant: MinFeeConstant, minFeeCoefficient: MinFeeCoefficient) => {
const minNoScriptFee = (tx: Cardano.Tx, minFeeConstant: number, minFeeCoefficient: number) => {
const txSize = serializeTx(tx).length;
return BigInt(Math.ceil(txSize * minFeeCoefficient + minFeeConstant));
};
Expand Down Expand Up @@ -130,13 +165,14 @@ export const minAdaRequired = (output: Cardano.TxOut, coinsPerUtxoByte: bigint):
* Gets the minimum transaction fee for the given transaction given its size and its execution units budget.
*
* @param tx The transaction to compute the min fee from.
* @param exUnitsPrice The current (given by protocol parameters) execution unit prices.
* @param minFeeConstant The current (given by protocol parameters) constant fee that all transaction must pay.
* @param minFeeCoefficient The current (given by protocol parameters) transaction size fee coefficient.
* @param resolvedInputs The resolved inputs of the transaction.
* @param pparams The protocol parameters.
*/
export const minFee = (
tx: Cardano.Tx,
exUnitsPrice: Cardano.Prices,
minFeeConstant: MinFeeConstant,
minFeeCoefficient: MinFeeCoefficient
) => minNoScriptFee(tx, minFeeConstant, minFeeCoefficient) + minScriptFee(tx, exUnitsPrice);
export const minFee = (tx: Cardano.Tx, resolvedInputs: Cardano.Utxo[], pparams: ProtocolParametersForInputSelection) =>
minNoScriptFee(tx, pparams.minFeeConstant, pparams.minFeeCoefficient) +
minScriptFee(
tx,
pparams.prices,
resolvedInputs,
pparams.minFeeRefScriptCostPerByte ? Number(pparams.minFeeRefScriptCostPerByte) : 0
);
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
TokenBundleSizeExceedsLimit,
sortTxIn
} from '@cardano-sdk/input-selection';
import { MinFeeCoefficient, MinFeeConstant, minAdaRequired, minFee } from '../fees';
import { TxEvaluationResult, TxEvaluator, TxIdWithIndex } from '../tx-builder';
import { minAdaRequired, minFee } from '../fees';

export const MAX_U64 = 18_446_744_073_709_551_615n;

Expand Down Expand Up @@ -105,11 +105,7 @@ const reorgRedeemers = (

export const computeMinimumCost =
(
{
minFeeCoefficient,
minFeeConstant,
prices
}: Pick<ProtocolParametersRequiredByInputSelection, 'minFeeCoefficient' | 'minFeeConstant' | 'prices'>,
pparams: ProtocolParametersForInputSelection,
buildTx: BuildTx,
txEvaluator: TxEvaluator,
redeemersByType: RedeemersByType
Expand All @@ -126,7 +122,7 @@ export const computeMinimumCost =
}

return {
fee: minFee(tx, prices, MinFeeConstant(minFeeConstant), MinFeeCoefficient(minFeeCoefficient)),
fee: minFee(tx, utxos, pparams),
redeemers: tx.witness.redeemers
};
};
Expand Down Expand Up @@ -170,25 +166,27 @@ export const computeSelectionLimit =
};

export const defaultSelectionConstraints = ({
protocolParameters: { coinsPerUtxoByte, maxTxSize, maxValueSize, minFeeCoefficient, minFeeConstant, prices },
protocolParameters,
buildTx,
redeemersByType,
txEvaluator
}: DefaultSelectionConstraintsProps): SelectionConstraints => {
if (!coinsPerUtxoByte || !maxTxSize || !maxValueSize || !minFeeCoefficient || !minFeeConstant || !prices) {
if (
!protocolParameters.coinsPerUtxoByte ||
!protocolParameters.maxTxSize ||
!protocolParameters.maxValueSize ||
!protocolParameters.minFeeCoefficient ||
!protocolParameters.minFeeConstant ||
!protocolParameters.prices
) {
throw new InvalidProtocolParametersError(
'Missing one of: coinsPerUtxoByte, maxTxSize, maxValueSize, minFeeCoefficient, minFeeConstant, prices'
);
}
return {
computeMinimumCoinQuantity: computeMinimumCoinQuantity(coinsPerUtxoByte),
computeMinimumCost: computeMinimumCost(
{ minFeeCoefficient, minFeeConstant, prices },
buildTx,
txEvaluator,
redeemersByType
),
computeSelectionLimit: computeSelectionLimit(maxTxSize, buildTx),
tokenBundleSizeExceedsLimit: tokenBundleSizeExceedsLimit(maxValueSize)
computeMinimumCoinQuantity: computeMinimumCoinQuantity(protocolParameters.coinsPerUtxoByte),
computeMinimumCost: computeMinimumCost(protocolParameters, buildTx, txEvaluator, redeemersByType),
computeSelectionLimit: computeSelectionLimit(protocolParameters.maxTxSize, buildTx),
tokenBundleSizeExceedsLimit: tokenBundleSizeExceedsLimit(protocolParameters.maxValueSize)
};
};
Loading

0 comments on commit 1dda3e5

Please sign in to comment.