Skip to main content

miden_standards/note/
min_burn_amount_config.rs

1use alloc::vec::Vec;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::assembly::Path;
5use miden_protocol::asset::AssetAmount;
6use miden_protocol::crypto::rand::FeltRng;
7use miden_protocol::errors::NoteError;
8use miden_protocol::note::{
9    Note,
10    NoteAssets,
11    NoteAttachment,
12    NoteAttachments,
13    NoteRecipient,
14    NoteScript,
15    NoteScriptRoot,
16    NoteStorage,
17    NoteTag,
18    NoteType,
19    PartialNoteMetadata,
20};
21use miden_protocol::utils::sync::LazyLock;
22use miden_protocol::{Felt, Word};
23
24use crate::StandardsLib;
25use crate::note::NetworkAccountTarget;
26use crate::note::costs::{MIN_BURN_AMOUNT_CONFIG_CONSUMPTION_CYCLES, NoteConsumptionCost};
27
28// NOTE SCRIPT
29// ================================================================================================
30
31/// Path to the MIN_BURN_AMOUNT_CONFIG note script procedure in the standards library.
32const MIN_BURN_AMOUNT_CONFIG_SCRIPT_PATH: &str =
33    "::miden::standards::notes::min_burn_amount_config::main";
34
35// Initialize the MIN_BURN_AMOUNT_CONFIG note script only once.
36static MIN_BURN_AMOUNT_CONFIG_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
37    let standards_lib = StandardsLib::default();
38    let path = Path::new(MIN_BURN_AMOUNT_CONFIG_SCRIPT_PATH);
39    NoteScript::from_package_reference(standards_lib.as_ref(), path)
40        .expect("Standards library contains MIN_BURN_AMOUNT_CONFIG note script procedure")
41});
42
43// MIN BURN AMOUNT CONFIG NOTE
44// ================================================================================================
45
46/// A MinBurnAmountConfig note: updates the minimum burn amount of a faucet's
47/// [`MinBurnAmount`](crate::account::policies::MinBurnAmount) burn policy by calling its
48/// `set_min_burn_amount` procedure on the faucet that consumes it.
49///
50/// The new threshold is carried in the note's storage as `[min_burn_amount]` (see the [`Note`]
51/// conversion below). Because the storage is fixed at note creation and bound into the note
52/// commitment, the authorized party is the note sender: the consuming faucet's
53/// `set_min_burn_amount` procedure authorizes the sender through the account-wide
54/// [`Authority`](crate::account::access::Authority) component.
55///
56/// The note is always public (for network execution) and tagged for `target` - the faucet carrying
57/// the `MinBurnAmount` component whose threshold is being updated. The `sender` is the account
58/// authorized for the update per the faucet's `Authority` configuration (the owner under
59/// [`OwnerControlled`](crate::account::access::Authority::OwnerControlled), or a role member under
60/// [`RbacControlled`](crate::account::access::Authority::RbacControlled)).
61///
62/// The note is bound to the target account by a
63/// [`NetworkAccountTarget`](crate::note::NetworkAccountTarget) attachment: the script asserts that
64/// the consuming account matches that target before calling `set_min_burn_amount`, so the note
65/// cannot be consumed by a third-party account that merely accepts its sender.
66///
67/// Note that the threshold only takes effect while `MinBurnAmount` is the faucet's active burn
68/// policy; it is stored on the component either way, so it can be configured before the policy is
69/// switched in.
70///
71/// Construct one with the [builder](MinBurnAmountConfigNote::builder); convert it into a protocol
72/// [`Note`] infallibly via `Note::from`.
73#[derive(Debug, Clone)]
74pub struct MinBurnAmountConfigNote {
75    sender: AccountId,
76    target: AccountId,
77    min_burn_amount: AssetAmount,
78    serial_number: Word,
79    attachments: NoteAttachments,
80}
81
82#[bon::bon]
83impl MinBurnAmountConfigNote {
84    /// Builds a new [`MinBurnAmountConfigNote`] setting `min_burn_amount` on `target`.
85    ///
86    /// # Errors
87    ///
88    /// Returns an error if:
89    /// - `target` is not a public account (the note is bound to it via a `NetworkAccountTarget`,
90    ///   which requires a public target).
91    /// - the attachments carry a `NetworkAccountTarget` for an account other than `target`.
92    /// - the attachments exceed their protocol limit (see [`NoteAttachments::new`]); the target
93    ///   attachment occupies one of the available slots when the caller does not supply it.
94    #[builder]
95    pub fn new(
96        #[builder(field)] mut attachments: Vec<NoteAttachment>,
97        sender: AccountId,
98        target: AccountId,
99        min_burn_amount: AssetAmount,
100        serial_number: Word,
101    ) -> Result<Self, NoteError> {
102        // Bind the note to `target`: the note script asserts, before calling
103        // `set_min_burn_amount`, that the consuming account matches this `NetworkAccountTarget`.
104        NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
105            NoteError::other_with_source(
106                "failed to bind the MinBurnAmountConfig note to its target account",
107                err,
108            )
109        })?;
110
111        let attachments = NoteAttachments::new(attachments)?;
112
113        Ok(Self {
114            sender,
115            target,
116            min_burn_amount,
117            serial_number,
118            attachments,
119        })
120    }
121}
122
123impl MinBurnAmountConfigNote {
124    // CONSTANTS
125    // --------------------------------------------------------------------------------------------
126
127    /// Number of storage items of a MinBurnAmountConfig note: the new minimum burn amount.
128    ///
129    /// Must be kept in sync with `NUM_STORAGE_ITEMS` in the note script, which asserts the count.
130    pub const NUM_STORAGE_ITEMS: usize = 1;
131
132    // PUBLIC ACCESSORS
133    // --------------------------------------------------------------------------------------------
134
135    /// Returns the script of the MinBurnAmountConfig note.
136    pub fn script() -> NoteScript {
137        MIN_BURN_AMOUNT_CONFIG_SCRIPT.clone()
138    }
139
140    /// Returns the MinBurnAmountConfig note script root.
141    pub fn script_root() -> NoteScriptRoot {
142        MIN_BURN_AMOUNT_CONFIG_SCRIPT.root()
143    }
144
145    /// Returns the account ID of the note's sender (the account authorized for the update).
146    pub fn sender(&self) -> AccountId {
147        self.sender
148    }
149
150    /// Returns the account ID of the managed faucet: the account the note is tagged for and bound
151    /// to via its `NetworkAccountTarget` attachment (only this account can consume the note).
152    pub fn target(&self) -> AccountId {
153        self.target
154    }
155
156    /// Returns the minimum burn amount the note sets on the faucet.
157    pub fn min_burn_amount(&self) -> AssetAmount {
158        self.min_burn_amount
159    }
160
161    /// Returns the note's serial number.
162    pub fn serial_number(&self) -> Word {
163        self.serial_number
164    }
165
166    /// Returns the attachments carried by the note.
167    pub fn attachments(&self) -> &NoteAttachments {
168        &self.attachments
169    }
170}
171
172// BUILDER EXTENSIONS
173// ================================================================================================
174
175impl<S: min_burn_amount_config_note_builder::State> MinBurnAmountConfigNoteBuilder<S> {
176    /// Adds a single attachment to the note.
177    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
178        self.attachments.push(attachment.into());
179        self
180    }
181
182    /// Adds multiple attachments to the note.
183    pub fn attachments(
184        mut self,
185        attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
186    ) -> Self {
187        self.attachments.extend(attachments.into_iter().map(Into::into));
188        self
189    }
190}
191
192impl<S: min_burn_amount_config_note_builder::State> MinBurnAmountConfigNoteBuilder<S>
193where
194    S::SerialNumber: min_burn_amount_config_note_builder::IsUnset,
195{
196    /// Draws a serial number from `rng` and sets it on the builder.
197    pub fn generate_serial_number(
198        self,
199        rng: &mut impl FeltRng,
200    ) -> MinBurnAmountConfigNoteBuilder<min_burn_amount_config_note_builder::SetSerialNumber<S>>
201    {
202        self.serial_number(rng.draw_word())
203    }
204}
205
206// CONVERSIONS
207// ================================================================================================
208
209impl From<MinBurnAmountConfigNote> for Note {
210    fn from(note: MinBurnAmountConfigNote) -> Self {
211        // MinBurnAmountConfig notes carry no assets and are always public for network execution;
212        // the new threshold lives in the note storage.
213        let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
214            .with_tag(NoteTag::with_account_target(note.target));
215        let storage = NoteStorage::new(vec![Felt::from(note.min_burn_amount)])
216            .expect("number of storage items should not exceed max storage items");
217        let recipient =
218            NoteRecipient::new(note.serial_number, MinBurnAmountConfigNote::script(), storage);
219
220        Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
221    }
222}
223
224// NOTE CONSUMPTION COST
225// ================================================================================================
226
227impl NoteConsumptionCost for MinBurnAmountConfigNote {
228    fn consumption_cycles() -> u32 {
229        MIN_BURN_AMOUNT_CONFIG_CONSUMPTION_CYCLES
230    }
231}
232
233// TESTS
234// ================================================================================================
235
236#[cfg(test)]
237mod tests {
238    use miden_protocol::account::AccountType;
239    use miden_protocol::crypto::rand::RandomCoin;
240
241    use super::*;
242
243    fn account_id(seed: u8) -> AccountId {
244        AccountId::builder()
245            .account_type(AccountType::Public)
246            .build_with_seed([seed; 32])
247    }
248
249    /// The builder produces a public, asset-less note tagged for the managed faucet.
250    #[test]
251    fn builder_builds_min_burn_amount_config_note() {
252        let mut rng = RandomCoin::new(Word::empty());
253        let faucet = account_id(1);
254        let sender = account_id(2);
255
256        let note = MinBurnAmountConfigNote::builder()
257            .sender(sender)
258            .target(faucet)
259            .min_burn_amount(AssetAmount::new(100).unwrap())
260            .generate_serial_number(&mut rng)
261            .build()
262            .unwrap();
263
264        assert_eq!(note.sender(), sender);
265        assert_eq!(note.target(), faucet);
266        assert_eq!(note.min_burn_amount(), AssetAmount::new(100).unwrap());
267
268        let note = Note::from(note);
269        assert_eq!(note.metadata().note_type(), NoteType::Public);
270        assert_eq!(note.metadata().tag(), NoteTag::with_account_target(faucet));
271        assert_eq!(note.assets().num_assets(), 0);
272    }
273
274    /// The built note carries a `NetworkAccountTarget` attachment bound to the faucet, so the note
275    /// script can reject consumption by any other account.
276    #[test]
277    fn note_is_bound_to_target_account() {
278        let faucet = account_id(1);
279        let note = MinBurnAmountConfigNote::builder()
280            .sender(account_id(2))
281            .target(faucet)
282            .min_burn_amount(AssetAmount::new(100).unwrap())
283            .serial_number(Word::empty())
284            .build()
285            .unwrap();
286
287        let built = Note::from(note);
288        let target = NetworkAccountTarget::try_from(built.attachments())
289            .expect("note should carry a network account target attachment");
290        assert_eq!(target.target_id(), faucet);
291    }
292
293    /// Storage is `[min_burn_amount]`.
294    #[test]
295    fn storage_layout() {
296        let min_burn_amount = AssetAmount::new(777).unwrap();
297
298        let note = MinBurnAmountConfigNote::builder()
299            .sender(account_id(2))
300            .target(account_id(1))
301            .min_burn_amount(min_burn_amount)
302            .serial_number(Word::empty())
303            .build()
304            .unwrap();
305
306        let built = Note::from(note);
307        assert_eq!(built.storage().items(), [Felt::from(min_burn_amount)]);
308        assert_eq!(built.storage().items().len(), MinBurnAmountConfigNote::NUM_STORAGE_ITEMS);
309    }
310
311    /// The config-note script root is registered in the [`StandardNote`](crate::note::StandardNote)
312    /// reverse lookup.
313    #[test]
314    fn script_root_is_registered_standard_note() {
315        use crate::note::StandardNote;
316
317        let standard = StandardNote::from_script_root(MinBurnAmountConfigNote::script_root())
318            .expect("config note script root should be a registered standard note");
319        assert_eq!(standard.name(), "MIN_BURN_AMOUNT_CONFIG");
320    }
321}