Skip to content

Commit 1dda3e5

Browse files
Merge pull request #1559 from input-output-hk/feat/lw-12066-fee-calculation-for-spending-refer-script
2 parents ea0b2c2 + 33527d5 commit 1dda3e5

File tree

4 files changed

+148
-56
lines changed

4 files changed

+148
-56
lines changed

packages/input-selection/src/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,13 @@ export interface InputSelector {
102102

103103
export type ProtocolParametersForInputSelection = Pick<
104104
Cardano.ProtocolParameters,
105-
'coinsPerUtxoByte' | 'maxTxSize' | 'maxValueSize' | 'minFeeCoefficient' | 'minFeeConstant' | 'prices'
105+
| 'coinsPerUtxoByte'
106+
| 'maxTxSize'
107+
| 'maxValueSize'
108+
| 'minFeeCoefficient'
109+
| 'minFeeConstant'
110+
| 'prices'
111+
| 'minFeeRefScriptCostPerByte'
106112
>;
107113

108114
export type ProtocolParametersRequiredByInputSelection = Required<{

packages/tx-construction/src/fees/fees.ts

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Cardano, Serialization } from '@cardano-sdk/core';
2-
import { OpaqueNumber } from '@cardano-sdk/util';
2+
import { ProtocolParametersForInputSelection } from '@cardano-sdk/input-selection';
33

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

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

63-
const totalExUnits = getTotalExUnits(tx.witness.redeemers);
66+
let base: number = coinsPerRefScriptByte;
67+
const range = 25_600;
68+
const multiplier = 1.2;
69+
70+
let totalRefScriptsSize = 0;
71+
72+
const totalInputs = [...tx.body.inputs, ...(tx.body.referenceInputs ?? [])];
73+
for (const output of totalInputs) {
74+
const resolvedInput = resolvedInputs.find(
75+
(input) => input[0].txId === output.txId && input[0].index === output.index
76+
);
6477

65-
return BigInt(Math.ceil(totalExUnits.steps * exUnitsPrice.steps + totalExUnits.memory * exUnitsPrice.memory));
78+
if (resolvedInput && resolvedInput[1].scriptReference) {
79+
totalRefScriptsSize += Serialization.Script.fromCore(resolvedInput[1].scriptReference).toCbor().length / 2;
80+
}
81+
}
82+
83+
let scriptRefFee = 0;
84+
while (totalRefScriptsSize > 0) {
85+
scriptRefFee += Math.ceil(Math.min(range, totalRefScriptsSize) * base);
86+
totalRefScriptsSize = Math.max(totalRefScriptsSize - range, 0);
87+
base *= multiplier;
88+
}
89+
90+
return BigInt(scriptRefFee);
6691
};
6792

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

77-
/**
78-
* Min fee coefficient reflects the dependence of the transaction cost on the size of the transaction. The larger
79-
* the transaction, the more resources are needed to store and process it.
80-
*/
81-
export type MinFeeCoefficient = OpaqueNumber<'MinFeeCoefficient'>;
82-
export const MinFeeCoefficient = (value: number): MinFeeCoefficient => value as unknown as MinFeeCoefficient;
109+
if (!tx.witness.redeemers) return BigInt(scriptRefFee);
110+
111+
const totalExUnits = getTotalExUnits(tx.witness.redeemers);
112+
113+
return (
114+
BigInt(Math.ceil(totalExUnits.steps * exUnitsPrice.steps + totalExUnits.memory * exUnitsPrice.memory)) +
115+
scriptRefFee
116+
);
117+
};
83118

84119
/**
85120
* Gets the minimum fee incurred by the transaction size.
@@ -88,7 +123,7 @@ export const MinFeeCoefficient = (value: number): MinFeeCoefficient => value as
88123
* @param minFeeConstant The prices of the execution units.
89124
* @param minFeeCoefficient The prices of the execution units.
90125
*/
91-
const minNoScriptFee = (tx: Cardano.Tx, minFeeConstant: MinFeeConstant, minFeeCoefficient: MinFeeCoefficient) => {
126+
const minNoScriptFee = (tx: Cardano.Tx, minFeeConstant: number, minFeeCoefficient: number) => {
92127
const txSize = serializeTx(tx).length;
93128
return BigInt(Math.ceil(txSize * minFeeCoefficient + minFeeConstant));
94129
};
@@ -130,13 +165,14 @@ export const minAdaRequired = (output: Cardano.TxOut, coinsPerUtxoByte: bigint):
130165
* Gets the minimum transaction fee for the given transaction given its size and its execution units budget.
131166
*
132167
* @param tx The transaction to compute the min fee from.
133-
* @param exUnitsPrice The current (given by protocol parameters) execution unit prices.
134-
* @param minFeeConstant The current (given by protocol parameters) constant fee that all transaction must pay.
135-
* @param minFeeCoefficient The current (given by protocol parameters) transaction size fee coefficient.
168+
* @param resolvedInputs The resolved inputs of the transaction.
169+
* @param pparams The protocol parameters.
136170
*/
137-
export const minFee = (
138-
tx: Cardano.Tx,
139-
exUnitsPrice: Cardano.Prices,
140-
minFeeConstant: MinFeeConstant,
141-
minFeeCoefficient: MinFeeCoefficient
142-
) => minNoScriptFee(tx, minFeeConstant, minFeeCoefficient) + minScriptFee(tx, exUnitsPrice);
171+
export const minFee = (tx: Cardano.Tx, resolvedInputs: Cardano.Utxo[], pparams: ProtocolParametersForInputSelection) =>
172+
minNoScriptFee(tx, pparams.minFeeConstant, pparams.minFeeCoefficient) +
173+
minScriptFee(
174+
tx,
175+
pparams.prices,
176+
resolvedInputs,
177+
pparams.minFeeRefScriptCostPerByte ? Number(pparams.minFeeRefScriptCostPerByte) : 0
178+
);

packages/tx-construction/src/input-selection/selectionConstraints.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import {
1010
TokenBundleSizeExceedsLimit,
1111
sortTxIn
1212
} from '@cardano-sdk/input-selection';
13-
import { MinFeeCoefficient, MinFeeConstant, minAdaRequired, minFee } from '../fees';
1413
import { TxEvaluationResult, TxEvaluator, TxIdWithIndex } from '../tx-builder';
14+
import { minAdaRequired, minFee } from '../fees';
1515

1616
export const MAX_U64 = 18_446_744_073_709_551_615n;
1717

@@ -105,11 +105,7 @@ const reorgRedeemers = (
105105

106106
export const computeMinimumCost =
107107
(
108-
{
109-
minFeeCoefficient,
110-
minFeeConstant,
111-
prices
112-
}: Pick<ProtocolParametersRequiredByInputSelection, 'minFeeCoefficient' | 'minFeeConstant' | 'prices'>,
108+
pparams: ProtocolParametersForInputSelection,
113109
buildTx: BuildTx,
114110
txEvaluator: TxEvaluator,
115111
redeemersByType: RedeemersByType
@@ -126,7 +122,7 @@ export const computeMinimumCost =
126122
}
127123

128124
return {
129-
fee: minFee(tx, prices, MinFeeConstant(minFeeConstant), MinFeeCoefficient(minFeeCoefficient)),
125+
fee: minFee(tx, utxos, pparams),
130126
redeemers: tx.witness.redeemers
131127
};
132128
};
@@ -170,25 +166,27 @@ export const computeSelectionLimit =
170166
};
171167

172168
export const defaultSelectionConstraints = ({
173-
protocolParameters: { coinsPerUtxoByte, maxTxSize, maxValueSize, minFeeCoefficient, minFeeConstant, prices },
169+
protocolParameters,
174170
buildTx,
175171
redeemersByType,
176172
txEvaluator
177173
}: DefaultSelectionConstraintsProps): SelectionConstraints => {
178-
if (!coinsPerUtxoByte || !maxTxSize || !maxValueSize || !minFeeCoefficient || !minFeeConstant || !prices) {
174+
if (
175+
!protocolParameters.coinsPerUtxoByte ||
176+
!protocolParameters.maxTxSize ||
177+
!protocolParameters.maxValueSize ||
178+
!protocolParameters.minFeeCoefficient ||
179+
!protocolParameters.minFeeConstant ||
180+
!protocolParameters.prices
181+
) {
179182
throw new InvalidProtocolParametersError(
180183
'Missing one of: coinsPerUtxoByte, maxTxSize, maxValueSize, minFeeCoefficient, minFeeConstant, prices'
181184
);
182185
}
183186
return {
184-
computeMinimumCoinQuantity: computeMinimumCoinQuantity(coinsPerUtxoByte),
185-
computeMinimumCost: computeMinimumCost(
186-
{ minFeeCoefficient, minFeeConstant, prices },
187-
buildTx,
188-
txEvaluator,
189-
redeemersByType
190-
),
191-
computeSelectionLimit: computeSelectionLimit(maxTxSize, buildTx),
192-
tokenBundleSizeExceedsLimit: tokenBundleSizeExceedsLimit(maxValueSize)
187+
computeMinimumCoinQuantity: computeMinimumCoinQuantity(protocolParameters.coinsPerUtxoByte),
188+
computeMinimumCost: computeMinimumCost(protocolParameters, buildTx, txEvaluator, redeemersByType),
189+
computeSelectionLimit: computeSelectionLimit(protocolParameters.maxTxSize, buildTx),
190+
tokenBundleSizeExceedsLimit: tokenBundleSizeExceedsLimit(protocolParameters.maxValueSize)
193191
};
194192
};

0 commit comments

Comments
 (0)