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