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::costs::{BURN_CONSUMPTION_CYCLES, NoteConsumptionCost};
25use crate::note::{NetworkAccountTarget, NoteExecutionHint};
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/// Construct one with the [builder](BurnNote::builder); convert it into a protocol [`Note`]
55/// infallibly via `Note::from`.
56#[derive(Debug, Clone)]
57pub struct BurnNote {
58    sender: AccountId,
59    serial_number: Word,
60    asset: Asset,
61    attachments: NoteAttachments,
62}
63
64#[bon::bon]
65impl BurnNote {
66    /// Builds a new [`BurnNote`] that burns `asset` against the faucet that issued it.
67    ///
68    /// The target faucet is the asset's own issuing faucet.
69    ///
70    /// # Errors
71    ///
72    /// Returns an error if the attachments exceed their protocol limit (see
73    /// [`NoteAttachments::new`]).
74    #[builder]
75    pub fn new(
76        #[builder(field)] attachments: Vec<NoteAttachment>,
77        sender: AccountId,
78        #[builder(into)] asset: Asset,
79        serial_number: Word,
80    ) -> Result<Self, NoteError> {
81        let network_target =
82            NetworkAccountTarget::new(asset.faucet_id(), NoteExecutionHint::Always).map_err(
83                |err| {
84                    NoteError::other_with_source("failed to target BURN note at asset faucet", err)
85                },
86            )?;
87        let attachments = NoteAttachments::new(
88            core::iter::once(network_target.into()).chain(attachments).collect(),
89        )?;
90
91        Ok(Self {
92            sender,
93            serial_number,
94            asset,
95            attachments,
96        })
97    }
98}
99
100impl BurnNote {
101    // CONSTANTS
102    // --------------------------------------------------------------------------------------------
103
104    /// Expected number of storage items of the BURN note: ASSET_ID(4) + ASSET_VALUE(4).
105    pub const NUM_STORAGE_ITEMS: usize = 8;
106
107    // PUBLIC ACCESSORS
108    // --------------------------------------------------------------------------------------------
109
110    /// Returns the script of the BURN note.
111    pub fn script() -> NoteScript {
112        BURN_SCRIPT.clone()
113    }
114
115    /// Returns the BURN note script root.
116    pub fn script_root() -> NoteScriptRoot {
117        BURN_SCRIPT.root()
118    }
119
120    /// Returns the account ID of the note's sender.
121    pub fn sender(&self) -> AccountId {
122        self.sender
123    }
124
125    /// Returns the account ID of the faucet that will burn the asset (the asset's own faucet).
126    pub fn faucet_id(&self) -> AccountId {
127        self.asset.faucet_id()
128    }
129
130    /// Returns the note's serial number.
131    pub fn serial_number(&self) -> Word {
132        self.serial_number
133    }
134
135    /// Returns the asset carried by the note (the asset to be burned).
136    pub fn asset(&self) -> Asset {
137        self.asset
138    }
139
140    /// Returns the attachments carried by the note.
141    pub fn attachments(&self) -> &NoteAttachments {
142        &self.attachments
143    }
144}
145
146// BUILDER EXTENSIONS
147// ================================================================================================
148
149impl<S: burn_note_builder::State> BurnNoteBuilder<S> {
150    /// Adds a single attachment to the note.
151    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
152        self.attachments.push(attachment.into());
153        self
154    }
155
156    /// Adds multiple attachments to the note.
157    pub fn attachments(
158        mut self,
159        attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
160    ) -> Self {
161        self.attachments.extend(attachments.into_iter().map(Into::into));
162        self
163    }
164}
165
166impl<S: burn_note_builder::State> BurnNoteBuilder<S>
167where
168    S::SerialNumber: burn_note_builder::IsUnset,
169{
170    /// Draws a serial number from `rng` and sets it on the builder.
171    pub fn generate_serial_number(
172        self,
173        rng: &mut impl FeltRng,
174    ) -> BurnNoteBuilder<burn_note_builder::SetSerialNumber<S>> {
175        self.serial_number(rng.draw_word())
176    }
177}
178
179// CONVERSIONS
180// ================================================================================================
181
182impl From<BurnNote> for Note {
183    fn from(note: BurnNote) -> Self {
184        // BURN notes are always public for network execution. The NetworkAccountTarget attachment
185        // routes the note to the asset's issuing faucet, while storage binds the script to the
186        // asset it must burn.
187        let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public);
188        let storage = NoteStorage::new(note.asset.as_elements().to_vec())
189            .expect("an asset always fits in BURN note storage");
190        let recipient = NoteRecipient::new(note.serial_number, BurnNote::script(), storage);
191
192        let assets = NoteAssets::new(vec![note.asset])
193            .expect("a single asset never exceeds the note asset limit");
194        Note::with_attachments(assets, metadata, recipient, note.attachments)
195    }
196}
197
198// NOTE CONSUMPTION COST
199// ================================================================================================
200
201impl NoteConsumptionCost for BurnNote {
202    fn consumption_cycles() -> u32 {
203        BURN_CONSUMPTION_CYCLES
204    }
205}
206
207// TESTS
208// ================================================================================================
209
210#[cfg(test)]
211mod tests {
212    use miden_protocol::account::AccountType;
213    use miden_protocol::asset::FungibleAsset;
214    use miden_protocol::crypto::rand::RandomCoin;
215    use miden_protocol::note::NoteTag;
216
217    use super::*;
218    use crate::note::NetworkAccountTarget;
219
220    fn sender() -> AccountId {
221        AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32])
222    }
223
224    fn faucet() -> AccountId {
225        AccountId::builder().account_type(AccountType::Public).build_with_seed([2; 32])
226    }
227
228    /// The builder produces a public note targeted at the faucet and carrying the asset to burn.
229    #[test]
230    fn builder_builds_public_burn_note() {
231        let mut rng = RandomCoin::new(Word::empty());
232        let asset = FungibleAsset::new(faucet(), 100).unwrap();
233
234        let burn_note = BurnNote::builder()
235            .sender(sender())
236            .asset(asset)
237            .generate_serial_number(&mut rng)
238            .build()
239            .unwrap();
240
241        assert_eq!(burn_note.sender(), sender());
242        assert_eq!(burn_note.faucet_id(), faucet());
243        assert_eq!(burn_note.asset(), asset.into());
244        assert_ne!(burn_note.serial_number(), Word::empty());
245
246        let note = Note::from(burn_note);
247        assert_eq!(note.metadata().note_type(), NoteType::Public);
248        assert_eq!(note.metadata().tag(), NoteTag::default());
249        assert_eq!(note.assets().num_assets(), 1);
250        assert_eq!(note.recipient().storage().items(), Asset::from(asset).as_elements());
251
252        let target = NetworkAccountTarget::try_from(note.attachments()).unwrap();
253        assert_eq!(target.target_id(), faucet());
254        assert_eq!(target.execution_hint(), NoteExecutionHint::Always);
255    }
256}