Skip to main content

miden_standards/note/config/
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/// The note must be public: the script rejects a non-public note. See
72/// [the module docs](crate::note::config#note-type) for the layers that enforce it.
73///
74/// Construct one with the [builder](MinBurnAmountConfigNote::builder); convert it into a protocol
75/// [`Note`] infallibly via `Note::from`.
76#[derive(Debug, Clone)]
77pub struct MinBurnAmountConfigNote {
78    sender: AccountId,
79    target: AccountId,
80    min_burn_amount: AssetAmount,
81    serial_number: Word,
82    attachments: NoteAttachments,
83}
84
85#[bon::bon]
86impl MinBurnAmountConfigNote {
87    /// Builds a new [`MinBurnAmountConfigNote`] setting `min_burn_amount` on `target`.
88    ///
89    /// # Errors
90    ///
91    /// Returns an error if:
92    /// - `target` is not a public account (the note is bound to it via a `NetworkAccountTarget`,
93    ///   which requires a public target).
94    /// - the attachments carry a `NetworkAccountTarget` for an account other than `target`.
95    /// - the attachments exceed their protocol limit (see [`NoteAttachments::new`]); the target
96    ///   attachment occupies one of the available slots when the caller does not supply it.
97    #[builder]
98    pub fn new(
99        #[builder(field)] mut attachments: Vec<NoteAttachment>,
100        sender: AccountId,
101        target: AccountId,
102        min_burn_amount: AssetAmount,
103        serial_number: Word,
104    ) -> Result<Self, NoteError> {
105        // Bind the note to `target`: the note script asserts, before calling
106        // `set_min_burn_amount`, that the consuming account matches this `NetworkAccountTarget`.
107        NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
108            NoteError::other_with_source(
109                "failed to bind the MinBurnAmountConfig note to its target account",
110                err,
111            )
112        })?;
113
114        let attachments = NoteAttachments::new(attachments)?;
115
116        Ok(Self {
117            sender,
118            target,
119            min_burn_amount,
120            serial_number,
121            attachments,
122        })
123    }
124}
125
126impl MinBurnAmountConfigNote {
127    // CONSTANTS
128    // --------------------------------------------------------------------------------------------
129
130    /// Number of storage items of a MinBurnAmountConfig note: the new minimum burn amount.
131    ///
132    /// Must be kept in sync with `NUM_STORAGE_ITEMS` in the note script, which asserts the count.
133    pub const NUM_STORAGE_ITEMS: usize = 1;
134
135    // PUBLIC ACCESSORS
136    // --------------------------------------------------------------------------------------------
137
138    /// Returns the script of the MinBurnAmountConfig note.
139    pub fn script() -> NoteScript {
140        MIN_BURN_AMOUNT_CONFIG_SCRIPT.clone()
141    }
142
143    /// Returns the MinBurnAmountConfig note script root.
144    pub fn script_root() -> NoteScriptRoot {
145        MIN_BURN_AMOUNT_CONFIG_SCRIPT.root()
146    }
147
148    /// Returns the account ID of the note's sender (the account authorized for the update).
149    pub fn sender(&self) -> AccountId {
150        self.sender
151    }
152
153    /// Returns the account ID of the managed faucet: the account the note is tagged for and bound
154    /// to via its `NetworkAccountTarget` attachment (only this account can consume the note).
155    pub fn target(&self) -> AccountId {
156        self.target
157    }
158
159    /// Returns the minimum burn amount the note sets on the faucet.
160    pub fn min_burn_amount(&self) -> AssetAmount {
161        self.min_burn_amount
162    }
163
164    /// Returns the note's serial number.
165    pub fn serial_number(&self) -> Word {
166        self.serial_number
167    }
168
169    /// Returns the attachments carried by the note.
170    pub fn attachments(&self) -> &NoteAttachments {
171        &self.attachments
172    }
173}
174
175// BUILDER EXTENSIONS
176// ================================================================================================
177
178impl<S: min_burn_amount_config_note_builder::State> MinBurnAmountConfigNoteBuilder<S> {
179    /// Adds a single attachment to the note.
180    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
181        self.attachments.push(attachment.into());
182        self
183    }
184
185    /// Adds multiple attachments to the note.
186    pub fn attachments(
187        mut self,
188        attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
189    ) -> Self {
190        self.attachments.extend(attachments.into_iter().map(Into::into));
191        self
192    }
193}
194
195impl<S: min_burn_amount_config_note_builder::State> MinBurnAmountConfigNoteBuilder<S>
196where
197    S::SerialNumber: min_burn_amount_config_note_builder::IsUnset,
198{
199    /// Draws a serial number from `rng` and sets it on the builder.
200    pub fn generate_serial_number(
201        self,
202        rng: &mut impl FeltRng,
203    ) -> MinBurnAmountConfigNoteBuilder<min_burn_amount_config_note_builder::SetSerialNumber<S>>
204    {
205        self.serial_number(rng.draw_word())
206    }
207}
208
209// CONVERSIONS
210// ================================================================================================
211
212impl From<MinBurnAmountConfigNote> for Note {
213    fn from(note: MinBurnAmountConfigNote) -> Self {
214        // MinBurnAmountConfig notes carry no assets and are always public for network execution;
215        // the new threshold lives in the note storage.
216        let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
217            .with_tag(NoteTag::with_account_target(note.target));
218        let storage = NoteStorage::new(vec![Felt::from(note.min_burn_amount)])
219            .expect("number of storage items should not exceed max storage items");
220        let recipient =
221            NoteRecipient::new(note.serial_number, MinBurnAmountConfigNote::script(), storage);
222
223        Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
224    }
225}
226
227// NOTE CONSUMPTION COST
228// ================================================================================================
229
230impl NoteConsumptionCost for MinBurnAmountConfigNote {
231    fn consumption_cycles() -> u32 {
232        MIN_BURN_AMOUNT_CONFIG_CONSUMPTION_CYCLES
233    }
234}
235
236// TESTS
237// ================================================================================================
238
239#[cfg(test)]
240mod tests {
241    use miden_protocol::account::AccountType;
242    use miden_protocol::crypto::rand::RandomCoin;
243
244    use super::*;
245
246    fn account_id(seed: u8) -> AccountId {
247        AccountId::builder()
248            .account_type(AccountType::Public)
249            .build_with_seed([seed; 32])
250    }
251
252    /// The builder produces a public, asset-less note tagged for the managed faucet.
253    #[test]
254    fn builder_builds_min_burn_amount_config_note() {
255        let mut rng = RandomCoin::new(Word::empty());
256        let faucet = account_id(1);
257        let sender = account_id(2);
258
259        let note = MinBurnAmountConfigNote::builder()
260            .sender(sender)
261            .target(faucet)
262            .min_burn_amount(AssetAmount::new(100).unwrap())
263            .generate_serial_number(&mut rng)
264            .build()
265            .unwrap();
266
267        assert_eq!(note.sender(), sender);
268        assert_eq!(note.target(), faucet);
269        assert_eq!(note.min_burn_amount(), AssetAmount::new(100).unwrap());
270
271        let note = Note::from(note);
272        assert_eq!(note.metadata().note_type(), NoteType::Public);
273        assert_eq!(note.metadata().tag(), NoteTag::with_account_target(faucet));
274        assert_eq!(note.assets().num_assets(), 0);
275    }
276
277    /// The built note carries a `NetworkAccountTarget` attachment bound to the faucet, so the note
278    /// script can reject consumption by any other account.
279    #[test]
280    fn note_is_bound_to_target_account() {
281        let faucet = account_id(1);
282        let note = MinBurnAmountConfigNote::builder()
283            .sender(account_id(2))
284            .target(faucet)
285            .min_burn_amount(AssetAmount::new(100).unwrap())
286            .serial_number(Word::empty())
287            .build()
288            .unwrap();
289
290        let built = Note::from(note);
291        let target = NetworkAccountTarget::try_from(built.attachments())
292            .expect("note should carry a network account target attachment");
293        assert_eq!(target.target_id(), faucet);
294    }
295
296    /// Storage is `[min_burn_amount]`.
297    #[test]
298    fn storage_layout() {
299        let min_burn_amount = AssetAmount::new(777).unwrap();
300
301        let note = MinBurnAmountConfigNote::builder()
302            .sender(account_id(2))
303            .target(account_id(1))
304            .min_burn_amount(min_burn_amount)
305            .serial_number(Word::empty())
306            .build()
307            .unwrap();
308
309        let built = Note::from(note);
310        assert_eq!(built.storage().items(), [Felt::from(min_burn_amount)]);
311        assert_eq!(built.storage().items().len(), MinBurnAmountConfigNote::NUM_STORAGE_ITEMS);
312    }
313
314    /// The config-note script root is registered in the [`StandardNote`](crate::note::StandardNote)
315    /// reverse lookup.
316    #[test]
317    fn script_root_is_registered_standard_note() {
318        use crate::note::StandardNote;
319
320        let standard = StandardNote::from_script_root(MinBurnAmountConfigNote::script_root())
321            .expect("config note script root should be a registered standard note");
322        assert_eq!(standard.name(), "MIN_BURN_AMOUNT_CONFIG");
323    }
324}