Skip to main content

miden_standards/account/fees/policies/
basic_constant_fee.rs

1use alloc::collections::BTreeMap;
2
3use miden_protocol::account::component::{
4    AccountComponentCode,
5    AccountComponentMetadata,
6    SchemaType,
7    StorageSchema,
8    StorageSlotSchema,
9};
10use miden_protocol::account::{
11    AccountComponent,
12    AccountComponentName,
13    AccountProcedureRoot,
14    StorageMap,
15    StorageMapKey,
16    StorageSlot,
17    StorageSlotName,
18};
19use miden_protocol::asset::AssetAmount;
20use miden_protocol::note::NoteScriptRoot;
21use miden_protocol::utils::sync::LazyLock;
22use miden_protocol::{Felt, Word};
23
24use crate::account::account_component_code;
25use crate::procedure_root;
26
27// BASIC CONSTANT FEE POLICY
28// ================================================================================================
29
30account_component_code!(
31    BASIC_CONSTANT_FEE_POLICY_CODE,
32    "miden-standards-fees-policies-basic-constant-fee.masp"
33);
34
35// PROCEDURE ROOTS
36// ================================================================================================
37
38/// MASL library namespace used for procedure-root lookups. Distinct from
39/// [`BasicConstantFeePolicy::NAME`], which mirrors the standards-side MASM module path.
40const BASIC_CONSTANT_FEE_LIBRARY_PATH: &str =
41    "miden::standards::components::fees::policies::basic_constant_fee";
42
43procedure_root!(
44    BASIC_CONSTANT_FEE_POLICY_ROOT,
45    BASIC_CONSTANT_FEE_LIBRARY_PATH,
46    BasicConstantFeePolicy::PROC_NAME,
47    BasicConstantFeePolicy::code()
48);
49
50static FEE_SCHEDULE_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
51    StorageSlotName::new("miden::standards::fees::policies::basic_constant_fee::fee_schedule")
52        .expect("storage slot name should be valid")
53});
54
55/// Set-marker element of a fee schedule entry, distinguishing scheduled entries (including
56/// explicit 0 fees) from unset keys: storage maps prune zero-word values and return the zero
57/// word for unset keys, so a scheduled entry must be a non-zero word. The MASM
58/// `compute_note_fee` counterpart asserts this element equals 1 (unset keys read as the zero
59/// word, whose marker element is 0) and strips it before returning the fee asset value.
60const FEE_SCHEDULE_ENTRY_MARKER: Felt = Felt::ONE;
61
62/// Encodes a fee as a fee schedule map entry: the asset value word with the set-marker as the
63/// last element, i.e. `[fee_amount, 0, 0, 1]`.
64fn fee_schedule_entry(fee: AssetAmount) -> Word {
65    let mut entry = fee.to_word();
66    entry[3] = FEE_SCHEDULE_ENTRY_MARKER;
67    entry
68}
69
70/// The `basic_constant_fee` fee policy account component.
71///
72/// This is a simple baseline constant fee policy: the fee depends only on the note's script root.
73/// More sophisticated constant fee policies can be derived from it by swapping the lookup-key
74/// computation of its `compute_note_fee` procedure, which yields a policy with a new root. A
75/// derived policy must also rename its `fee_schedule` storage slot, since two components
76/// installed on the same account cannot share a slot name.
77///
78/// Register with a [`crate::account::fees::FeePolicyManager`], whose allowed fee-policies map then
79/// includes [`BasicConstantFeePolicy::root`]. When active, `estimate_note_fee` dispatches to this
80/// policy's `compute_note_fee` procedure, which returns the fee as a fee asset (asset ID and
81/// value words): the amount is looked up in the fee schedule under the note's script root
82/// (recovered from the note's recipient via the advice provider), and note scripts without a
83/// schedule entry abort fee estimation. To make a note script free, schedule an explicit 0 fee
84/// for it via [`BasicConstantFeePolicy::with_fee`]. The remaining note parameters, including the
85/// timeframe and priority, are ignored by this policy. The fee asset ID is read from the
86/// fee-policy storage, so the fee is always charged in the configured asset and the policy
87/// requires an [`AuthNetworkAccount`][crate::account::auth::AuthNetworkAccount] component on the
88/// same account.
89///
90/// ## Storage layout
91///
92/// - [`Self::fee_schedule_slot_name`] map slot: `NOTE_SCRIPT_ROOT => [fee_amount, 0, 0, 1]`, where
93///   the last element is a set-marker distinguishing scheduled entries from unset keys.
94#[derive(Debug, Clone, Default)]
95pub struct BasicConstantFeePolicy {
96    /// The fee charged per note script root.
97    fee_schedule: BTreeMap<NoteScriptRoot, AssetAmount>,
98}
99
100impl BasicConstantFeePolicy {
101    // CONSTANTS
102    // --------------------------------------------------------------------------------------------
103
104    /// The name of the component.
105    pub const NAME: &'static str = "miden::standards::fees::policies::basic_constant_fee";
106
107    pub(crate) const PROC_NAME: &str = "compute_note_fee";
108
109    /// Returns the canonical [`AccountComponentName`] of this component.
110    pub const fn name() -> AccountComponentName {
111        AccountComponentName::from_static_str(Self::NAME)
112    }
113
114    // CONSTRUCTORS
115    // --------------------------------------------------------------------------------------------
116
117    /// Creates a new `basic_constant_fee` fee policy with an empty fee schedule, charging fees in
118    /// the fungible asset its [`crate::account::fees::FeePolicyManager`] is configured with.
119    pub fn new() -> Self {
120        Self { fee_schedule: BTreeMap::new() }
121    }
122
123    /// Sets the fee for notes with the given script root, replacing any previous entry.
124    ///
125    /// The schedule stores bare amounts: the fee must be denominated in the fee asset of the
126    /// [`FeePolicyManager`](crate::account::fees::FeePolicyManager) this policy is registered
127    /// with, which is the asset the policy charges it in.
128    ///
129    /// Scheduling an explicit fee of 0 makes notes with this script root free; script roots
130    /// without a schedule entry abort fee estimation.
131    #[must_use]
132    pub fn with_fee(mut self, script_root: NoteScriptRoot, fee: AssetAmount) -> Self {
133        self.fee_schedule.insert(script_root, fee);
134        self
135    }
136
137    /// Extends the fee schedule with the given `(script_root, fee)` entries, replacing any
138    /// previous entries. See [`Self::with_fee`] for the fee denomination requirement.
139    #[must_use]
140    pub fn with_fees(
141        mut self,
142        entries: impl IntoIterator<Item = (NoteScriptRoot, AssetAmount)>,
143    ) -> Self {
144        for (script_root, fee) in entries {
145            self = self.with_fee(script_root, fee);
146        }
147        self
148    }
149
150    // PUBLIC ACCESSORS
151    // --------------------------------------------------------------------------------------------
152
153    /// Returns the [`AccountComponentCode`] of this component.
154    pub fn code() -> &'static AccountComponentCode {
155        &BASIC_CONSTANT_FEE_POLICY_CODE
156    }
157
158    /// Returns the procedure root of the `compute_note_fee` fee policy procedure.
159    pub fn root() -> AccountProcedureRoot {
160        *BASIC_CONSTANT_FEE_POLICY_ROOT
161    }
162
163    /// Returns the [`StorageSlotName`] of the slot holding the fee schedule map.
164    pub fn fee_schedule_slot_name() -> &'static StorageSlotName {
165        &FEE_SCHEDULE_SLOT_NAME
166    }
167
168    /// Returns the fee charged per note script root.
169    pub fn fee_schedule(&self) -> &BTreeMap<NoteScriptRoot, AssetAmount> {
170        &self.fee_schedule
171    }
172
173    /// Returns the [`AccountComponentMetadata`] for this component.
174    pub fn component_metadata() -> AccountComponentMetadata {
175        let storage_schema = StorageSchema::new([(
176            Self::fee_schedule_slot_name().clone(),
177            StorageSlotSchema::map(
178                "Fee charged per note script root",
179                SchemaType::native_word(),
180                SchemaType::native_word(),
181            ),
182        )])
183        .expect("storage schema should be valid");
184
185        AccountComponentMetadata::new(Self::NAME)
186            .with_description(
187                "`basic_constant_fee` fee policy charging a constant per-note-script fee",
188            )
189            .with_storage_schema(storage_schema)
190    }
191}
192
193impl From<BasicConstantFeePolicy> for AccountComponent {
194    fn from(policy: BasicConstantFeePolicy) -> Self {
195        let entries = policy
196            .fee_schedule
197            .into_iter()
198            .map(|(root, fee)| (StorageMapKey::new(root.as_word()), fee_schedule_entry(fee)));
199        let fee_schedule_map = StorageMap::with_entries(entries)
200            .expect("fee schedule entries should produce a valid storage map");
201        let fee_schedule_slot = StorageSlot::with_map(
202            BasicConstantFeePolicy::fee_schedule_slot_name().clone(),
203            fee_schedule_map,
204        );
205
206        AccountComponent::new(
207            BasicConstantFeePolicy::code().clone(),
208            vec![fee_schedule_slot],
209            BasicConstantFeePolicy::component_metadata(),
210        )
211        .expect(
212            "`basic_constant_fee` fee policy component should satisfy the requirements of a valid account component",
213        )
214    }
215}
216
217// TESTS
218// ================================================================================================
219
220#[cfg(test)]
221mod tests {
222    use miden_protocol::account::StorageSlotContent;
223
224    use super::*;
225
226    /// Check that the policy's storage slot contains the fee schedule entries.
227    #[test]
228    fn storage_slots_contain_expected_entries() -> anyhow::Result<()> {
229        let script_root = NoteScriptRoot::from_array([1, 2, 3, 4]);
230        let fee = AssetAmount::new(500)?;
231        let free_script_root = NoteScriptRoot::from_array([5, 6, 7, 8]);
232
233        // Seed an outdated entry and overwrite it via `with_fees`, so the storage assertions
234        // below also cover the batch method's extend-and-replace contract.
235        let policy = BasicConstantFeePolicy::new()
236            .with_fee(script_root, AssetAmount::new(100)?)
237            .with_fees([(script_root, fee), (free_script_root, AssetAmount::ZERO)]);
238
239        let component = AccountComponent::from(policy);
240        let slot = component
241            .storage_slots()
242            .iter()
243            .find(|slot| slot.name() == BasicConstantFeePolicy::fee_schedule_slot_name())
244            .expect("fee schedule slot should exist");
245
246        let StorageSlotContent::Map(map) = slot.content() else {
247            panic!("fee schedule slot must be a map");
248        };
249        assert_eq!(
250            map.get(&StorageMapKey::new(script_root.as_word())),
251            Word::new([Felt::new(500)?, Felt::ZERO, Felt::ZERO, Felt::ONE]),
252            "the fee entry should be stored as an asset value word with the set-marker"
253        );
254        assert_eq!(
255            map.get(&StorageMapKey::new(free_script_root.as_word())),
256            Word::new([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]),
257            "an explicit 0-fee entry should survive as a non-zero word"
258        );
259
260        Ok(())
261    }
262}