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
103 #[test]
104 fn log_verification_cycles_formula() {
105 let log_verification_cycles = |total_cycles: u32| {
106 TransactionFee::new(total_cycles).unwrap().log_verification_cycles()
107 };
108 assert_eq!(log_verification_cycles(1), 1);
109 assert_eq!(log_verification_cycles(2), 2);
110 assert_eq!(log_verification_cycles(3), 2);
111 assert_eq!(log_verification_cycles(4), 3);
112 assert_eq!(log_verification_cycles(65_536), 17);
113 assert_eq!(log_verification_cycles(MAX_TX_EXECUTION_CYCLES), 30);
114 }
115
116 #[test]
117 fn zero_cycles_are_rejected() {
118 assert!(matches!(TransactionFee::new(0), Err(TransactionFeeError::ZeroTotalCycles)));
119 }
120
121 #[test]
122 fn cycles_above_the_kernel_maximum_are_rejected() {
123 assert!(matches!(
124 TransactionFee::new(MAX_TX_EXECUTION_CYCLES + 1),
125 Err(TransactionFeeError::TotalCyclesExceedsMax(_))
126 ));
127 }
128
129 /// The maximal margin-free fee (`u32::MAX` base fee, 30 verification cycles) must neither
130 /// wrap nor exceed `AssetAmount::MAX`.
131 #[test]
132 fn compute_fee_does_not_wrap_at_the_maximal_base_fee() {
133 let fee = TransactionFee::new(MAX_TX_EXECUTION_CYCLES)
134 .unwrap()
135 .compute_fee(&FeeParameters::new(u32::MAX))
136 .unwrap();
137 assert_eq!(fee.as_u64(), u64::from(u32::MAX) * 30);
138 }
139
140 /// The safety margin adds verification cycles before the base-fee multiplication.
141 #[test]
142 fn safety_margin_adds_verification_cycles() {
143 let fee = TransactionFee::new(1 << 16)
144 .unwrap()
145 .with_safety_margin(3)
146 .compute_fee(&FeeParameters::new(500))
147 .unwrap();
148 assert_eq!(fee.as_u64(), 500 * (17 + 3));
149 }
150
151 /// An oversized margin pushes the fee beyond `AssetAmount::MAX`, which `compute_fee`
152 /// rejects.
153 #[test]
154 fn fee_exceeding_max_asset_amount_is_rejected() {
155 let result = TransactionFee::new(1)
156 .unwrap()
157 .with_safety_margin(u32::MAX - 1)
158 .compute_fee(&FeeParameters::new(u32::MAX));
159 assert!(matches!(result, Err(TransactionFeeError::FeeExceedsMaxAssetAmount(_))));
160 }
161}