Skip to main content

miden_standards/note/
burn.rs

1use alloc::vec::Vec;
2
3use miden_protocol::Word;
4use miden_protocol::account::AccountId;
5use miden_protocol::assembly::Path;
6use miden_protocol::asset::Asset;
7use miden_protocol::crypto::rand::FeltRng;
8use miden_protocol::errors::NoteError;
9use miden_protocol::note::{
10    Note,
11    NoteAssets,
12    NoteAttachment,
13    NoteAttachments,
14    NoteRecipient,
15    NoteScript,
16    NoteScriptRoot,
17    NoteStorage,
18    NoteType,
19    PartialNoteMetadata,
20};
21use miden_protocol::utils::sync::LazyLock;
22
23use crate::StandardsLib;
24use crate::note::NetworkAccountTarget;
25use crate::note::costs::{BURN_CONSUMPTION_CYCLES, NoteConsumptionCost};
26
27// NOTE SCRIPT
28// ================================================================================================
29
30/// Path to the BURN note script procedure in the standards library.
31const BURN_SCRIPT_PATH: &str = "::miden::standards::notes::burn::main";
32
33// Initialize the BURN note script only once
34static BURN_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
35    let standards_lib = StandardsLib::default();
36    let path = Path::new(BURN_SCRIPT_PATH);
37    NoteScript::from_package_reference(standards_lib.as_ref(), path)
38        .expect("Standards library contains BURN note script procedure")
39});
40
41// BURN NOTE
42// ================================================================================================
43
44/// A BURN note: instructs a faucet to burn the asset carried by the note and embedded in its
45/// storage.
46///
47/// When consumed by the faucet that issued the asset, the note's asset is destroyed via the
48/// faucet's `receive_and_burn` procedure. The single BURN script works against both fungible and
49/// non-fungible faucets: it detects the faucet kind by reflection (via the `CodeInspection`
50/// component) and calls the matching `receive_and_burn`. BURN notes are always public so they are
51/// visible on-chain and discoverable by the network; whether consuming one requires a signature
52/// depends on the target faucet's auth component.
53///
54/// A BURN note for a public faucet carries a
55/// [`NetworkAccountTarget`](crate::note::NetworkAccountTarget) attachment naming that faucet,
56/// derived from the asset the note burns, so the network can route the note to it. A private faucet
57/// can never be a network account, so a note for one carries no such attachment.
58///
59/// Construct one with the [builder](BurnNote::builder); convert it into a protocol [`Note`]
60/// infallibly via `Note::from`.
61#[derive(Debug, Clone)]
62pub struct BurnNote {
63    sender: AccountId,
64    serial_number: Word,
65    asset: Asset,
66    attachments: NoteAttachments,
67}
68
69#[bon::bon]
70impl BurnNote {
71    /// Builds a new [`BurnNote`] that burns `asset` against the faucet that issued it.
72    ///
73    /// The target faucet is the asset's own issuing faucet.
74    ///
75    /// # Errors
76    ///
77    /// Returns an error if:
78    /// - the attachments carry a `NetworkAccountTarget` for an account other than that faucet.
79    /// - the attachments exceed their protocol limit (see [`NoteAttachments::new`]).
80    #[builder]
81    pub fn new(
82        #[builder(field)] mut attachments: Vec<NoteAttachment>,
83        sender: AccountId,
84        #[builder(into)] asset: Asset,
85        serial_number: Word,
86    ) -> Result<Self, NoteError> {
87        // The network routes the note on this attachment; the asset it carries is what binds the
88        // script to the same faucet on consumption for both private and network faucets.
89        NetworkAccountTarget::ensure_presence_if_public(&mut attachments, asset.faucet_id())
90            .map_err(|err| {
91                NoteError::other_with_source("failed to target the BURN note at its faucet", err)
92            })?;
93
94        let attachments = NoteAttachments::new(attachments)?;
95
96        Ok(Self {
97            sender,
98            serial_number,
99            asset,
100            attachments,
101        })
102    }
103}
104
105impl BurnNote {
106    // CONSTANTS
107    // --------------------------------------------------------------------------------------------
108
109    /// Expected number of storage items of the BURN note: ASSET_ID(4) + ASSET_VALUE(4).
110    pub const NUM_STORAGE_ITEMS: usize = 8;
111
112    // PUBLIC ACCESSORS
113    // --------------------------------------------------------------------------------------------
114
115    /// Returns the script of the BURN note.
116    pub fn script() -> NoteScript {
117        BURN_SCRIPT.clone()
118    }
119
120    /// Returns the BURN note script root.
121    pub fn script_root() -> NoteScriptRoot {
122        BURN_SCRIPT.root()
123    }
124
125    /// Returns the account ID of the note's sender.
126    pub fn sender(&self) -> AccountId {
127        self.sender
128    }
129
130    /// Returns the account ID of the faucet that will burn the asset (the asset's own faucet).
131    pub fn faucet_id(&self) -> AccountId {
132        self.asset.faucet_id()
133    }
134
135    /// Returns the note's serial number.
136    pub fn serial_number(&self) -> Word {
137        self.serial_number
138    }
139
140    /// Returns the asset carried by the note (the asset to be burned).
141    pub fn asset(&self) -> Asset {
142        self.asset
143    }
144
145    /// Returns the attachments carried by the note.
146    pub fn attachments(&self) -> &NoteAttachments {
147        &self.attachments
148    }
149}
150
151// BUILDER EXTENSIONS
152// ================================================================================================
153
154impl<S: burn_note_builder::State> BurnNoteBuilder<S> {
155    /// Adds a single attachment to the note.
156    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
157        self.attachments.push(attachment.into());
158        self
159    }
160
161    /// Adds multiple attachments to the note.
162    pub fn attachments(
163        mut self,
164        attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
165    ) -> Self {
166        self.attachments.extend(attachments.into_iter().map(Into::into));
167        self
168    }
169}
170
171impl<S: burn_note_builder::State> BurnNoteBuilder<S>
172where
173    S::SerialNumber: burn_note_builder::IsUnset,
174{
175    /// Draws a serial number from `rng` and sets it on the builder.
176    pub fn generate_serial_number(
177        self,
178        rng: &mut impl FeltRng,
179    ) -> BurnNoteBuilder<burn_note_builder::SetSerialNumber<S>> {
180        self.serial_number(rng.draw_word())
181    }
182}
183
184// CONVERSIONS
185// ================================================================================================
186
187impl From<BurnNote> for Note {
188    fn from(note: BurnNote) -> Self {
189        // BURN notes are always public for network execution. The NetworkAccountTarget attachment
190        // routes the note to the asset's issuing faucet, while storage binds the script to the
191        // asset it must burn.
192        let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public);
193        let storage = NoteStorage::new(note.asset.as_elements().to_vec())
194            .expect("an asset always fits in BURN note storage");
195        let recipient = NoteRecipient::new(note.serial_number, BurnNote::script(), storage);
196
197        let assets = NoteAssets::new(vec![note.asset])
198            .expect("a single asset never exceeds the note asset limit");
199        Note::with_attachments(assets, metadata, recipient, note.attachments)
200    }
201}
202
203// NOTE CONSUMPTION COST
204// ================================================================================================
205
206impl NoteConsumptionCost for BurnNote {
207    fn consumption_cycles() -> u32 {
208        BURN_CONSUMPTION_CYCLES
209    }
210}
211
212// TESTS
213// ================================================================================================
214
215#[cfg(test)]
216mod tests {
217    use miden_protocol::account::AccountType;
218    use miden_protocol::asset::FungibleAsset;
219    use miden_protocol::crypto::rand::RandomCoin;
220    use miden_protocol::note::NoteTag;
221
222    use super::*;
223    use crate::note::{NetworkNoteExt, NoteExecutionHint};
224
225    fn sender() -> AccountId {
226        AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32])
227    }
228
229    fn faucet() -> AccountId {
230        AccountId::builder().account_type(AccountType::Public).build_with_seed([2; 32])
231    }
232
233    fn private_faucet() -> AccountId {
234        AccountId::builder().account_type(AccountType::Private).build_with_seed([2; 32])
235    }
236
237    fn build_burn_note(faucet_id: AccountId) -> BurnNote {
238        let mut rng = RandomCoin::new(Word::empty());
239        BurnNote::builder()
240            .sender(sender())
241            .asset(FungibleAsset::new(faucet_id, 100).unwrap())
242            .generate_serial_number(&mut rng)
243            .build()
244            .unwrap()
245    }
246
247    /// The builder produces a public note carrying the asset to burn and routed to its faucet by a
248    /// derived network target. How that target treats caller-supplied attachments is covered by the
249    /// `network_account_target` tests.
250    #[test]
251    fn builder_builds_public_burn_note() {
252        let asset = FungibleAsset::new(faucet(), 100).unwrap();
253
254        let burn_note = build_burn_note(faucet());
255
256        assert_eq!(burn_note.sender(), sender());
257        assert_eq!(burn_note.faucet_id(), faucet());
258        assert_eq!(burn_note.asset(), asset.into());
259        assert_ne!(burn_note.serial_number(), Word::empty());
260        assert_eq!(burn_note.attachments().num_attachments(), 1);
261
262        let note = Note::from(burn_note);
263        assert_eq!(note.metadata().note_type(), NoteType::Public);
264        assert_eq!(note.metadata().tag(), NoteTag::default());
265        assert_eq!(note.assets().num_assets(), 1);
266        assert_eq!(note.recipient().storage().items(), Asset::from(asset).as_elements());
267        assert!(note.is_network_note());
268
269        let target = NetworkAccountTarget::try_from(note.attachments()).unwrap();
270        assert_eq!(target.target_id(), faucet());
271        assert_eq!(target.execution_hint(), NoteExecutionHint::Always);
272    }
273
274    /// A private faucet is never a network account, so no target is derived for it. The note stays
275    /// consumable by that faucet, which is bound by the asset the note carries.
276    #[test]
277    fn builder_omits_network_target_for_private_faucet() {
278        let burn_note = build_burn_note(private_faucet());
279
280        assert_eq!(burn_note.attachments().num_attachments(), 0);
281        assert!(!Note::from(burn_note).is_network_note());
282    }
283}