Skip to main content

miden_protocol/transaction/
fee.rs

1use crate::MAX_TX_EXECUTION_CYCLES;
2use crate::asset::AssetAmount;
3use crate::block::FeeParameters;
4use crate::errors::AssetError;
5
6// TRANSACTION FEE
7// ================================================================================================
8
9/// Errors from constructing [`TransactionFee`] inputs.
10#[derive(Debug, thiserror::Error)]
11#[non_exhaustive]
12pub enum TransactionFeeError {
13    /// The total cycle count was zero; every transaction executes at least the kernel prologue.
14    #[error("transaction fee inputs require a non-zero total cycle count")]
15    ZeroTotalCycles,
16    /// The total cycle count exceeds [`MAX_TX_EXECUTION_CYCLES`], the bound the kernel's
17    /// `compute_fee` enforces.
18    #[error("total cycle count {0} exceeds the maximum of {MAX_TX_EXECUTION_CYCLES} cycles")]
19    TotalCyclesExceedsMax(u32),
20    /// The computed fee exceeds the maximum representable asset amount.
21    #[error("computed fee exceeds the maximum asset amount")]
22    FeeExceedsMaxAssetAmount(#[source] AssetError),
23}
24
25/// The inputs from which a transaction's fee is computed, mirroring the transaction kernel's
26/// `compute_fee` procedure.
27///
28/// This is the single Rust implementation of the kernel fee formula: keep it in sync with
29/// `compute_fee` in `asm/kernels/transaction-core/src/tx.masm`. The kernel's output-notes fee
30/// term is currently hardcoded to zero and thus omitted here.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct TransactionFee {
33    log_verification_cycles: u32,
34}
35
36impl TransactionFee {
37    /// Creates the fee inputs for a transaction executing `total_cycles` VM cycles.
38    ///
39    /// Mirrors the kernel's `compute_fee`: the number of charged verification cycles is
40    /// `ilog2(total_cycles) + 1`, where the unconditional `+ 1` rounds the proof-verification
41    /// cost up to the next power of two.
42    ///
43    /// Returns an error if `total_cycles` is 0 (every transaction executes at least the kernel
44    /// prologue, so a zero cycle count cannot describe a transaction) or exceeds
45    /// [`MAX_TX_EXECUTION_CYCLES`], the bound the kernel's `compute_fee` enforces.
46    pub fn new(total_cycles: u32) -> Result<Self, TransactionFeeError> {
47        if total_cycles == 0 {
48            return Err(TransactionFeeError::ZeroTotalCycles);
49        }
50        if total_cycles > MAX_TX_EXECUTION_CYCLES {
51            return Err(TransactionFeeError::TotalCyclesExceedsMax(total_cycles));
52        }
53        Ok(Self {
54            log_verification_cycles: total_cycles.ilog2() + 1,
55        })
56    }
57
58    /// Returns the number of verification cycles the fee is charged for - a logarithmic
59    /// measure of the transaction's total cycle count, not an exact cycle count.
60    pub fn log_verification_cycles(&self) -> u32 {
61        self.log_verification_cycles
62    }
63
64    /// Returns fee inputs charging `extra_verification_cycles` on top of the formula's
65    /// verification cycles, e.g. as a safety margin when the fee is derived from an estimated
66    /// rather than a measured cycle count.
67    ///
68    /// The addition saturates at `u32::MAX` verification cycles; a fee that large is rejected
69    /// by [`Self::compute_fee`] for any base fee above `2^31`.
70    pub fn with_safety_margin(self, extra_verification_cycles: u32) -> Self {
71        Self {
72            log_verification_cycles: self
73                .log_verification_cycles
74                .saturating_add(extra_verification_cycles),
75        }
76    }
77
78    /// Computes the fee under the given fee parameters.
79    ///
80    /// Returns an error if the fee exceeds [`AssetAmount::MAX`]: the formula's own
81    /// verification cycles keep the fee far below it, but a large [`Self::with_safety_margin`]
82    /// can push it beyond.
83    pub fn compute_fee(
84        &self,
85        fee_parameters: &FeeParameters,
86    ) -> Result<AssetAmount, TransactionFeeError> {
87        // Multiply in u64: the kernel multiplies in the field, so a u32 product would wrap
88        // where the kernel does not. A product of two u32 values cannot wrap a u64.
89        let fee_amount = u64::from(fee_parameters.verification_base_fee())
90            * u64::from(self.log_verification_cycles);
91
92        AssetAmount::new(fee_amount).map_err(TransactionFeeError::FeeExceedsMaxAssetAmount)
93    }
94}
95
96// TESTS
97// ================================================================================================
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::account::AccountId;
103    use crate::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
104
105    fn fee_parameters(verification_base_fee: u32) -> FeeParameters {
106        let fee_faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)
107            .expect("testing faucet ID should be valid");
108        FeeParameters::new(fee_faucet_id, verification_base_fee)
109    }
110
111    #[test]
112    fn log_verification_cycles_formula() {
113        let log_verification_cycles = |total_cycles: u32| {
114            TransactionFee::new(total_cycles).unwrap().log_verification_cycles()
115        };
116        assert_eq!(log_verification_cycles(1), 1);
117        assert_eq!(log_verification_cycles(2), 2);
118        assert_eq!(log_verification_cycles(3), 2);
119        assert_eq!(log_verification_cycles(4), 3);
120        assert_eq!(log_verification_cycles(65_536), 17);
121        assert_eq!(log_verification_cycles(MAX_TX_EXECUTION_CYCLES), 30);
122    }
123
124    #[test]
125    fn zero_cycles_are_rejected() {
126        assert!(matches!(TransactionFee::new(0), Err(TransactionFeeError::ZeroTotalCycles)));
127    }
128
129    #[test]
130    fn cycles_above_the_kernel_maximum_are_rejected() {
131        assert!(matches!(
132            TransactionFee::new(MAX_TX_EXECUTION_CYCLES + 1),
133            Err(TransactionFeeError::TotalCyclesExceedsMax(_))
134        ));
135    }
136
137    /// The maximal margin-free fee (`u32::MAX` base fee, 30 verification cycles) must neither
138    /// wrap nor exceed `AssetAmount::MAX`.
139    #[test]
140    fn compute_fee_does_not_wrap_at_the_maximal_base_fee() {
141        let fee = TransactionFee::new(MAX_TX_EXECUTION_CYCLES)
142            .unwrap()
143            .compute_fee(&fee_parameters(u32::MAX))
144            .unwrap();
145        assert_eq!(fee.as_u64(), u64::from(u32::MAX) * 30);
146    }
147
148    /// The safety margin adds verification cycles before the base-fee multiplication.
149    #[test]
150    fn safety_margin_adds_verification_cycles() {
151        let fee = TransactionFee::new(1 << 16)
152            .unwrap()
153            .with_safety_margin(3)
154            .compute_fee(&fee_parameters(500))
155            .unwrap();
156        assert_eq!(fee.as_u64(), 500 * (17 + 3));
157    }
158
159    /// An oversized margin pushes the fee beyond `AssetAmount::MAX`, which `compute_fee`
160    /// rejects.
161    #[test]
162    fn fee_exceeding_max_asset_amount_is_rejected() {
163        let result = TransactionFee::new(1)
164            .unwrap()
165            .with_safety_margin(u32::MAX - 1)
166            .compute_fee(&fee_parameters(u32::MAX));
167        assert!(matches!(result, Err(TransactionFeeError::FeeExceedsMaxAssetAmount(_))));
168    }
169}