miden_protocol/transaction/
fee.rs1use crate::MAX_TX_EXECUTION_CYCLES;
2use crate::asset::AssetAmount;
3use crate::block::FeeParameters;
4use crate::errors::AssetError;
5
6#[derive(Debug, thiserror::Error)]
11#[non_exhaustive]
12pub enum TransactionFeeError {
13 #[error("transaction fee inputs require a non-zero total cycle count")]
15 ZeroTotalCycles,
16 #[error("total cycle count {0} exceeds the maximum of {MAX_TX_EXECUTION_CYCLES} cycles")]
19 TotalCyclesExceedsMax(u32),
20 #[error("computed fee exceeds the maximum asset amount")]
22 FeeExceedsMaxAssetAmount(#[source] AssetError),
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct TransactionFee {
33 log_verification_cycles: u32,
34}
35
36impl TransactionFee {
37 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 pub fn log_verification_cycles(&self) -> u32 {
61 self.log_verification_cycles
62 }
63
64 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 pub fn compute_fee(
84 &self,
85 fee_parameters: &FeeParameters,
86 ) -> Result<AssetAmount, TransactionFeeError> {
87 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#[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 #[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 #[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 #[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}