Skip to main content

miden_standards/account/auth/
fee.rs

1use alloc::vec::Vec;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::errors::NoteError;
5use miden_protocol::{Felt, Hasher, Word};
6
7// FEE PAYMENT INFO
8// ================================================================================================
9
10/// Conversion info instructing `miden::standards::fee::pay_fee` which asset to pay
11/// the transaction fee in.
12///
13/// `pay_fee` accepts only the native fee asset at rate 1/1, so the paid amount is the fee the
14/// kernel computed. Build it with [`Self::one_to_one`] and the reference block's fee faucet.
15///
16/// For signature-based authentication components the conversion info is committed to via the
17/// transaction's auth args (see [`commit_fee_conversion_info`]).
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct FeeConversionInfo {
20    faucet_id: AccountId,
21    rate_num: Felt,
22    rate_den: Felt,
23}
24
25impl FeeConversionInfo {
26    /// Creates new fee conversion info paying the fee in the asset issued by `faucet_id` at the
27    /// rate `rate_num / rate_den`.
28    ///
29    /// Any faucet or rate other than the native fee asset at 1/1 aborts until multi-asset
30    /// fee payments are supported.
31    ///
32    /// # Errors
33    ///
34    /// Returns an error if `rate_num` or `rate_den` is zero or does not fit into a field
35    /// element.
36    pub fn new(faucet_id: AccountId, rate_num: u64, rate_den: u64) -> Result<Self, NoteError> {
37        if rate_num == 0 {
38            return Err(NoteError::other("fee conversion rate numerator must be non-zero"));
39        }
40        if rate_den == 0 {
41            return Err(NoteError::other("fee conversion rate denominator must be non-zero"));
42        }
43        let rate_num = Felt::try_from(rate_num).map_err(|err| {
44            NoteError::other_with_source("fee conversion rate numerator is not a valid felt", err)
45        })?;
46        let rate_den = Felt::try_from(rate_den).map_err(|err| {
47            NoteError::other_with_source("fee conversion rate denominator is not a valid felt", err)
48        })?;
49
50        Ok(Self { faucet_id, rate_num, rate_den })
51    }
52
53    /// Creates fee conversion info paying the fee in the asset issued by `faucet_id` at the
54    /// rate 1/1.
55    pub fn one_to_one(faucet_id: AccountId) -> Self {
56        Self {
57            faucet_id,
58            rate_num: Felt::ONE,
59            rate_den: Felt::ONE,
60        }
61    }
62
63    // PUBLIC ACCESSORS
64    // --------------------------------------------------------------------------------------------
65
66    /// Returns the ID of the faucet issuing the fee payment asset.
67    pub fn faucet_id(&self) -> AccountId {
68        self.faucet_id
69    }
70
71    /// Returns the numerator of the conversion rate.
72    pub fn rate_num(&self) -> Felt {
73        self.rate_num
74    }
75
76    /// Returns the denominator of the conversion rate.
77    pub fn rate_den(&self) -> Felt {
78        self.rate_den
79    }
80
81    // CONVERSIONS
82    // --------------------------------------------------------------------------------------------
83
84    /// Returns the conversion info encoded as a word.
85    ///
86    /// The layout must be kept in sync with `load_conversion_info` in the
87    /// `miden::standards::fee` MASM module.
88    pub fn to_word(&self) -> Word {
89        Word::from([
90            self.faucet_id.suffix(),
91            self.faucet_id.prefix().as_felt(),
92            self.rate_num,
93            self.rate_den,
94        ])
95    }
96}
97
98// AUTH ARGS COMMITMENT
99// ================================================================================================
100
101/// Commits to the given conversion info under `salt` for passing to the authentication
102/// procedure via the transaction's auth args.
103///
104/// Returns the auth args together with the advice map value holding their preimage: the auth
105/// args are the commitment `hash(CONVERSION_INFO || SALT)` and the advice map must map them to
106/// `[SALT, CONVERSION_INFO]`, which `miden::standards::fee::load_conversion_info` reads and
107/// verifies in-VM.
108///
109/// Committing via the auth args means the signature over the transaction summary authorizes the
110/// payment asset and rate, while the salt slot keeps the auth args usable as a unique salt for
111/// replay protection.
112pub fn commit_fee_conversion_info(
113    conversion_info: FeeConversionInfo,
114    salt: Word,
115) -> (Word, Vec<Felt>) {
116    let info_word = conversion_info.to_word();
117
118    let mut value = Vec::with_capacity(8);
119    value.extend(salt.iter());
120    value.extend(info_word.iter());
121
122    (Hasher::merge(&[info_word, salt]), value)
123}
124
125// TESTS
126// ================================================================================================
127
128#[cfg(test)]
129mod tests {
130    use miden_protocol::account::AccountType;
131
132    use super::*;
133
134    fn faucet() -> AccountId {
135        AccountId::builder()
136            .account_type(AccountType::Public)
137            .build_with_seed([3u8; 32])
138    }
139
140    /// A zero rate numerator or denominator is rejected by construction.
141    #[test]
142    fn zero_rates_are_rejected() {
143        assert!(FeeConversionInfo::new(faucet(), 0, 1).is_err());
144        assert!(FeeConversionInfo::new(faucet(), 1, 0).is_err());
145        assert!(FeeConversionInfo::new(faucet(), 1, 1).is_ok());
146    }
147
148    /// A rate numerator or denominator at or above the field modulus is rejected by
149    /// construction, while large rates below it are accepted.
150    #[test]
151    fn rates_exceeding_field_modulus_are_rejected() {
152        assert!(FeeConversionInfo::new(faucet(), u64::MAX, 1).is_err());
153        assert!(FeeConversionInfo::new(faucet(), 1, u64::MAX).is_err());
154        assert!(FeeConversionInfo::new(faucet(), 10u64.pow(16), 10u64.pow(4)).is_ok());
155    }
156
157    /// The advice map value is the preimage of the auth args commitment.
158    #[test]
159    fn advice_map_value_is_commitment_preimage() {
160        let payment_info = FeeConversionInfo::new(faucet(), 2, 3).unwrap();
161        let salt = Word::from([1u32, 2, 3, 4]);
162
163        let (key, value) = commit_fee_conversion_info(payment_info, salt);
164
165        assert_eq!(key, Hasher::merge(&[payment_info.to_word(), salt]));
166        assert_eq!(value.len(), 8);
167        assert_eq!(&value[..4], salt.as_elements());
168        assert_eq!(&value[4..], payment_info.to_word().as_elements());
169    }
170}