Skip to content

feat!: fee calculation now takes into account reference script size #1559

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

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