Skip to content

Commit baad60d

Browse files
feat!: fee calculation now takes into account reference script size
BREAKING CHANGE: minFee now takes resolvedInputs and complete protocol parameters as arguments - removed ProtocolParametersForInputSelection type - removed ProtocolParametersRequiredByInputSelection type
1 parent ea0b2c2 commit baad60d

File tree

6 files changed

+147
-74
lines changed

6 files changed

+147
-74
lines changed

packages/input-selection/src/types.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,3 @@ export interface InputSelector {
9999
*/
100100
select(params: InputSelectionParameters): Promise<SelectionResult>;
101101
}
102-
103-
export type ProtocolParametersForInputSelection = Pick<
104-
Cardano.ProtocolParameters,
105-
'coinsPerUtxoByte' | 'maxTxSize' | 'maxValueSize' | 'minFeeCoefficient' | 'minFeeConstant' | 'prices'
106-
>;
107-
108-
export type ProtocolParametersRequiredByInputSelection = Required<{
109-
[k in keyof ProtocolParametersForInputSelection]: ProtocolParametersForInputSelection[k];
110-
}>;

packages/input-selection/test/vectors.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Cardano, Serialization } from '@cardano-sdk/core';
22
import { HexBlob } from '@cardano-sdk/util';
3-
import { ProtocolParametersForInputSelection } from '../src';
43
import { SelectionSkeleton } from '../dist/cjs';
54
import { TxTestUtil } from '@cardano-sdk/util-dev';
65

@@ -28,7 +27,7 @@ export const babbageSelectionParameters = {
2827
minFeeCoefficient: 44,
2928
minFeeConstant: 155_381,
3029
prices: { memory: 0.0577, steps: 0.000_007_21 }
31-
} as ProtocolParametersForInputSelection;
30+
} as Cardano.ProtocolParameters;
3231

3332
export const cborUtxoSetWithManyAssets = [
3433
HexBlob(

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 { ProtocolParameters } from '@cardano-sdk/core/dist/cjs/Cardano';
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: ProtocolParameters) =>
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: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@ import {
33
ComputeMinimumCoinQuantity,
44
ComputeSelectionLimit,
55
EstimateTxCosts,
6-
ProtocolParametersForInputSelection,
7-
ProtocolParametersRequiredByInputSelection,
86
SelectionConstraints,
97
SelectionSkeleton,
108
TokenBundleSizeExceedsLimit,
119
sortTxIn
1210
} from '@cardano-sdk/input-selection';
13-
import { MinFeeCoefficient, MinFeeConstant, minAdaRequired, minFee } from '../fees';
1411
import { TxEvaluationResult, TxEvaluator, TxIdWithIndex } from '../tx-builder';
12+
import { minAdaRequired, minFee } from '../fees';
1513

1614
export const MAX_U64 = 18_446_744_073_709_551_615n;
1715

@@ -27,7 +25,7 @@ export interface RedeemersByType {
2725
}
2826

2927
export interface DefaultSelectionConstraintsProps {
30-
protocolParameters: ProtocolParametersForInputSelection;
28+
protocolParameters: Cardano.ProtocolParameters;
3129
buildTx: BuildTx;
3230
redeemersByType: RedeemersByType;
3331
txEvaluator: TxEvaluator;
@@ -105,11 +103,7 @@ const reorgRedeemers = (
105103

106104
export const computeMinimumCost =
107105
(
108-
{
109-
minFeeCoefficient,
110-
minFeeConstant,
111-
prices
112-
}: Pick<ProtocolParametersRequiredByInputSelection, 'minFeeCoefficient' | 'minFeeConstant' | 'prices'>,
106+
pparams: Cardano.ProtocolParameters,
113107
buildTx: BuildTx,
114108
txEvaluator: TxEvaluator,
115109
redeemersByType: RedeemersByType
@@ -126,18 +120,18 @@ export const computeMinimumCost =
126120
}
127121

128122
return {
129-
fee: minFee(tx, prices, MinFeeConstant(minFeeConstant), MinFeeCoefficient(minFeeCoefficient)),
123+
fee: minFee(tx, utxos, pparams),
130124
redeemers: tx.witness.redeemers
131125
};
132126
};
133127

134128
export const computeMinimumCoinQuantity =
135-
(coinsPerUtxoByte: ProtocolParametersRequiredByInputSelection['coinsPerUtxoByte']): ComputeMinimumCoinQuantity =>
129+
(coinsPerUtxoByte: Cardano.ProtocolParameters['coinsPerUtxoByte']): ComputeMinimumCoinQuantity =>
136130
(output) =>
137131
minAdaRequired(output, BigInt(coinsPerUtxoByte));
138132

139133
export const tokenBundleSizeExceedsLimit =
140-
(maxValueSize: ProtocolParametersRequiredByInputSelection['maxValueSize']): TokenBundleSizeExceedsLimit =>
134+
(maxValueSize: Cardano.ProtocolParameters['maxValueSize']): TokenBundleSizeExceedsLimit =>
141135
(tokenBundle) => {
142136
if (!tokenBundle) {
143137
return false;
@@ -159,7 +153,7 @@ const getTxSize = (tx: Serialization.Transaction) => Buffer.from(tx.toCbor(), 'h
159153
* @returns {ComputeSelectionLimit} constraint that returns txSize <= maxTxSize ? utxo[].length : utxo[].length-1
160154
*/
161155
export const computeSelectionLimit =
162-
(maxTxSize: ProtocolParametersRequiredByInputSelection['maxTxSize'], buildTx: BuildTx): ComputeSelectionLimit =>
156+
(maxTxSize: Cardano.ProtocolParameters['maxTxSize'], buildTx: BuildTx): ComputeSelectionLimit =>
163157
async (selectionSkeleton) => {
164158
const tx = await buildTx(selectionSkeleton);
165159
const txSize = getTxSize(Serialization.Transaction.fromCore(tx));
@@ -170,25 +164,27 @@ export const computeSelectionLimit =
170164
};
171165

172166
export const defaultSelectionConstraints = ({
173-
protocolParameters: { coinsPerUtxoByte, maxTxSize, maxValueSize, minFeeCoefficient, minFeeConstant, prices },
167+
protocolParameters,
174168
buildTx,
175169
redeemersByType,
176170
txEvaluator
177171
}: DefaultSelectionConstraintsProps): SelectionConstraints => {
178-
if (!coinsPerUtxoByte || !maxTxSize || !maxValueSize || !minFeeCoefficient || !minFeeConstant || !prices) {
172+
if (
173+
!protocolParameters.coinsPerUtxoByte ||
174+
!protocolParameters.maxTxSize ||
175+
!protocolParameters.maxValueSize ||
176+
!protocolParameters.minFeeCoefficient ||
177+
!protocolParameters.minFeeConstant ||
178+
!protocolParameters.prices
179+
) {
179180
throw new InvalidProtocolParametersError(
180181
'Missing one of: coinsPerUtxoByte, maxTxSize, maxValueSize, minFeeCoefficient, minFeeConstant, prices'
181182
);
182183
}
183184
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)
185+
computeMinimumCoinQuantity: computeMinimumCoinQuantity(protocolParameters.coinsPerUtxoByte),
186+
computeMinimumCost: computeMinimumCost(protocolParameters, buildTx, txEvaluator, redeemersByType),
187+
computeSelectionLimit: computeSelectionLimit(protocolParameters.maxTxSize, buildTx),
188+
tokenBundleSizeExceedsLimit: tokenBundleSizeExceedsLimit(protocolParameters.maxValueSize)
193189
};
194190
};

0 commit comments

Comments
 (0)