cml_chain/
fees.rs

1use crate::plutus::utils::compute_total_ex_units;
2use crate::plutus::ExUnitPrices;
3use crate::transaction::Transaction;
4use crate::Coin;
5use cml_core::{serialization::Serialize, ArithmeticError};
6use num::{rational::BigRational, CheckedAdd, CheckedMul};
7use std::convert::TryFrom;
8
9/// Careful: although the linear fee is the same for Byron & Shelley
10/// The value of the parameters and how fees are computed is not the same
11#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
12pub struct LinearFee {
13    /// minfee_a
14    pub coefficient: Coin,
15    /// minfee_b
16    pub constant: Coin,
17    /// min_fee_ref_script_cost_per_byte
18    pub ref_script_cost_per_byte: Coin,
19}
20
21impl LinearFee {
22    /**
23     * * `coefficient` - minfee_a from protocol params
24     * * `constant` - minfee_b from protocol params
25     * * `ref_script_cost_per_bytes` - min_fee_ref_script_cost_per_byte from protocol params. New in Conway
26     */
27    pub fn new(coefficient: Coin, constant: Coin, ref_script_cost_per_byte: Coin) -> Self {
28        Self {
29            constant,
30            coefficient,
31            ref_script_cost_per_byte,
32        }
33    }
34}
35
36/**
37 * Min fee for JUST the script, NOT including ref inputs
38 */
39pub fn min_script_fee(
40    tx: &Transaction,
41    ex_unit_prices: &ExUnitPrices,
42) -> Result<Coin, ArithmeticError> {
43    if let Some(redeemers) = &tx.witness_set.redeemers {
44        let total_ex_units = compute_total_ex_units(&redeemers.clone().to_flat_format())?;
45        let script_fee = ((BigRational::new(total_ex_units.mem.into(), 1u64.into())
46            * BigRational::new(
47                ex_unit_prices.mem_price.numerator.into(),
48                ex_unit_prices.mem_price.denominator.into(),
49            ))
50            + (BigRational::new(total_ex_units.steps.into(), 1u64.into())
51                * BigRational::new(
52                    ex_unit_prices.step_price.numerator.into(),
53                    ex_unit_prices.step_price.denominator.into(),
54                )))
55        .ceil()
56        .to_integer();
57        u64::try_from(script_fee).map_err(|_| ArithmeticError::IntegerOverflow)
58    } else {
59        Ok(0)
60    }
61}
62
63/**
64 * Calculates the cost of all ref scripts
65 * * `total_ref_script_size` - Total size (original, not hashes) of all ref scripts. Duplicate scripts are counted as many times as they occur
66 */
67pub fn min_ref_script_fee(
68    linear_fee: &LinearFee,
69    total_ref_script_size: u64,
70) -> Result<Coin, ArithmeticError> {
71    // based on:
72    // https://github.com/IntersectMBO/cardano-ledger/blob/7e65f0365eef647b9415e3fe9b3c35561761a3d5/eras/conway/impl/src/Cardano/Ledger/Conway/Tx.hs#L84
73    // https://github.com/IntersectMBO/cardano-ledger/blob/a34f878c56763d138d2203d8ba84b3af64d94fce/eras/conway/impl/src/Cardano/Ledger/Conway/UTxO.hs#L152
74
75    if total_ref_script_size > 0 {
76        let multiplier = BigRational::new(12u64.into(), 10u64.into());
77        let size_increment = 25_600u64; // 25KiB
78        let mut fee: BigRational = BigRational::from_integer(0.into());
79        let mut fee_tier: BigRational =
80            BigRational::from_integer(linear_fee.ref_script_cost_per_byte.into());
81        let mut ref_scripts_size_left = total_ref_script_size;
82
83        loop {
84            fee = BigRational::from_integer(
85                std::cmp::min(size_increment, ref_scripts_size_left).into(),
86            )
87            .checked_mul(&fee_tier)
88            .and_then(|x| x.checked_add(&fee))
89            .ok_or(ArithmeticError::IntegerOverflow)?;
90            if ref_scripts_size_left <= size_increment {
91                break;
92            }
93            ref_scripts_size_left -= size_increment;
94            fee_tier = fee_tier
95                .checked_mul(&multiplier)
96                .ok_or(ArithmeticError::IntegerOverflow)?;
97        }
98        u64::try_from(fee.ceil().to_integer()).map_err(|_e| ArithmeticError::IntegerOverflow)
99    } else {
100        Ok(0)
101    }
102}
103
104pub fn min_no_script_fee(
105    tx: &Transaction,
106    linear_fee: &LinearFee,
107) -> Result<Coin, ArithmeticError> {
108    (tx.to_cbor_bytes().len() as u64)
109        .checked_mul(linear_fee.coefficient)
110        .and_then(|x| x.checked_add(linear_fee.constant))
111        .ok_or(ArithmeticError::IntegerOverflow)
112}
113
114pub fn min_fee(
115    tx: &Transaction,
116    linear_fee: &LinearFee,
117    ex_unit_prices: &ExUnitPrices,
118    total_ref_script_size: u64,
119) -> Result<Coin, ArithmeticError> {
120    // TODO: the fee should be 0 if all inputs are genesis redeem addresses
121    let base_fee = min_no_script_fee(tx, linear_fee)?;
122    let script_fee = min_script_fee(tx, ex_unit_prices)?;
123    let ref_scripts_fee = min_ref_script_fee(linear_fee, total_ref_script_size)?;
124    base_fee
125        .checked_add(script_fee)
126        .and_then(|x| x.checked_add(ref_scripts_fee))
127        .ok_or(ArithmeticError::IntegerOverflow)
128}