Skip to main content

miden_standards/note/
upgrade.rs

1use alloc::vec::Vec;
2
3use miden_protocol::Word;
4use miden_protocol::account::{AccountCode, AccountCodeUpgrade, AccountId};
5use miden_protocol::assembly::Path;
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;
22
23use crate::StandardsLib;
24use crate::note::costs::{NoteConsumptionCost, UPGRADE_CONSUMPTION_CYCLES};
25use crate::note::{AccountCodeUpgradeAttachment, NetworkAccountTarget};
26
27// NOTE SCRIPT
28// ================================================================================================
29
30/// Path to the UPGRADE note script procedure in the standards library.
31const UPGRADE_SCRIPT_PATH: &str = "::miden::standards::notes::upgrade::main";
32
33// Initialize the UPGRADE note script only once.
34static UPGRADE_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
35    let standards_lib = StandardsLib::default();
36    let path = Path::new(UPGRADE_SCRIPT_PATH);
37    NoteScript::from_package_reference(standards_lib.as_ref(), path)
38        .expect("Standards library contains UPGRADE note script procedure")
39});
40
41// UPGRADE NOTE
42// ================================================================================================
43
44/// An Upgrade note: upgrades the code of the network account that consumes it by calling the
45/// `upgrade` procedure of its [`UpgradeManager`](crate::account::upgrade::UpgradeManager)
46/// component.
47///
48/// The commitment to the new code is carried in the note's storage as `[NEW_CODE_COMMITMENT]`.
49/// The called `upgrade` procedure authorizes the sender through the account-wide
50/// [`Authority`](crate::account::access::Authority) component.
51///
52/// The new code itself is carried in an [`AccountCodeUpgradeAttachment`], which the note script
53/// requires and inserts into the advice map, from which the transaction host provides it to the
54/// kernel. The code must therefore fit into the note's [`NoteAttachments`]. If the transaction
55/// already provides a different encoding of the same code through the advice map (see
56/// [`AccountCodeUpgrade`]), consuming the note fails.
57///
58/// The note is always public (for network execution) and bound to the `target` account by a
59/// [`NetworkAccountTarget`] attachment. The script asserts both before calling `upgrade`.
60#[derive(Debug, Clone)]
61pub struct UpgradeNote {
62    sender: AccountId,
63    target: AccountId,
64    new_code_commitment: Word,
65    serial_number: Word,
66    attachments: NoteAttachments,
67}
68
69#[bon::bon]
70impl UpgradeNote {
71    /// Builds a new [`UpgradeNote`] that upgrades the code of `target` to `code`.
72    ///
73    /// # Errors
74    ///
75    /// Returns an error if:
76    /// - the attachments carry an [`AccountCodeUpgradeAttachment`] that does not decode or carries
77    ///   code other than `code`.
78    /// - `target` is not a public account (the note is bound to it via a `NetworkAccountTarget`,
79    ///   which requires a public target).
80    /// - the attachments carry a `NetworkAccountTarget` for an account other than `target`.
81    /// - the attachments exceed their protocol limit (see [`NoteAttachments::new`]); the code and
82    ///   target attachments occupy some of the available words and slots.
83    #[builder]
84    pub fn new(
85        #[builder(field)] mut attachments: Vec<NoteAttachment>,
86        sender: AccountId,
87        target: AccountId,
88        code: AccountCode,
89        serial_number: Word,
90    ) -> Result<Self, NoteError> {
91        let code_upgrade = AccountCodeUpgrade::new(code);
92        let new_code_commitment = code_upgrade.commitment();
93        AccountCodeUpgradeAttachment::ensure_presence(&mut attachments, code_upgrade)?;
94
95        // Bind the note to `target`.
96        NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
97            NoteError::other_with_source(
98                "failed to bind the upgrade note to its target account",
99                err,
100            )
101        })?;
102
103        let attachments = NoteAttachments::new(attachments)?;
104
105        Ok(Self {
106            sender,
107            target,
108            new_code_commitment,
109            serial_number,
110            attachments,
111        })
112    }
113}
114
115impl UpgradeNote {
116    // CONSTANTS
117    // --------------------------------------------------------------------------------------------
118
119    /// Number of storage items of an Upgrade note: the new code commitment.
120    ///
121    /// Must be kept in sync with `NUM_STORAGE_ITEMS` in the note script.
122    pub const NUM_STORAGE_ITEMS: usize = Word::NUM_ELEMENTS;
123
124    // PUBLIC ACCESSORS
125    // --------------------------------------------------------------------------------------------
126
127    /// Returns the script of the Upgrade note.
128    pub fn script() -> NoteScript {
129        UPGRADE_SCRIPT.clone()
130    }
131
132    /// Returns the script root of the Upgrade note.
133    pub fn script_root() -> NoteScriptRoot {
134        UPGRADE_SCRIPT.root()
135    }
136
137    /// Returns the account ID of the note's sender (the account authorized for the upgrade).
138    pub fn sender(&self) -> AccountId {
139        self.sender
140    }
141
142    /// Returns the account ID of the upgraded account; the target of the note.
143    pub fn target(&self) -> AccountId {
144        self.target
145    }
146
147    /// Returns the commitment to the code the note upgrades the target account to.
148    pub fn new_code_commitment(&self) -> Word {
149        self.new_code_commitment
150    }
151
152    /// Returns the note's serial number.
153    pub fn serial_number(&self) -> Word {
154        self.serial_number
155    }
156
157    /// Returns the attachments carried by the note.
158    pub fn attachments(&self) -> &NoteAttachments {
159        &self.attachments
160    }
161}
162
163// BUILDER EXTENSIONS
164// ================================================================================================
165
166impl<S: upgrade_note_builder::State> UpgradeNoteBuilder<S> {
167    /// Adds a single attachment to the note.
168    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
169        self.attachments.push(attachment.into());
170        self
171    }
172
173    /// Adds multiple attachments to the note.
174    pub fn attachments(
175        mut self,
176        attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
177    ) -> Self {
178        self.attachments.extend(attachments.into_iter().map(Into::into));
179        self
180    }
181}
182
183impl<S: upgrade_note_builder::State> UpgradeNoteBuilder<S>
184where
185    S::SerialNumber: upgrade_note_builder::IsUnset,
186{
187    /// Draws a serial number from `rng` and sets it on the builder.
188    pub fn generate_serial_number(
189        self,
190        rng: &mut impl FeltRng,
191    ) -> UpgradeNoteBuilder<upgrade_note_builder::SetSerialNumber<S>> {
192        self.serial_number(rng.draw_word())
193    }
194}
195
196// CONVERSIONS
197// ================================================================================================
198
199impl From<UpgradeNote> for Note {
200    fn from(note: UpgradeNote) -> Self {
201        // Upgrade notes carry no assets and are always public for network execution; the new code
202        // commitment lives in the note storage and the new code in an attachment.
203        let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
204            .with_tag(NoteTag::with_account_target(note.target));
205        let storage = NoteStorage::new(note.new_code_commitment.as_elements().to_vec())
206            .expect("number of storage items should not exceed max storage items");
207        let recipient = NoteRecipient::new(note.serial_number, UpgradeNote::script(), storage);
208
209        Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
210    }
211}
212
213// NOTE CONSUMPTION COST
214// ================================================================================================
215
216impl NoteConsumptionCost for UpgradeNote {
217    fn consumption_cycles() -> u32 {
218        UPGRADE_CONSUMPTION_CYCLES
219    }
220}
221
222// TESTS
223// ================================================================================================
224
225#[cfg(test)]
226mod tests {
227    use assert_matches::assert_matches;
228    use miden_protocol::account::AccountType;
229    use miden_protocol::crypto::rand::RandomCoin;
230
231    use super::*;
232    use crate::testing::account_component::{IncrNonceAuthComponent, MockProceduresComponent};
233
234    fn account_id(seed: u8) -> AccountId {
235        AccountId::builder()
236            .account_type(AccountType::Public)
237            .build_with_seed([seed; 32])
238    }
239
240    fn build_upgrade_note(target: AccountId, code: AccountCode) -> Result<UpgradeNote, NoteError> {
241        UpgradeNote::builder()
242            .sender(account_id(2))
243            .target(target)
244            .code(code)
245            .serial_number(Word::empty())
246            .build()
247    }
248
249    /// Returns account code with `num_procedures` procedures next to its auth procedure.
250    fn code_with_procedures(num_procedures: usize) -> anyhow::Result<AccountCode> {
251        Ok(AccountCode::from_components(&[
252            IncrNonceAuthComponent.into(),
253            MockProceduresComponent::new(num_procedures).into(),
254        ])?)
255    }
256
257    /// The builder produces a public, asset-less note tagged for the upgraded account, whose
258    /// storage is the commitment to the new code.
259    #[test]
260    fn builder_builds_upgrade_note() -> anyhow::Result<()> {
261        let mut rng = RandomCoin::new(Word::empty());
262        let target = account_id(1);
263        let sender = account_id(2);
264        let code = AccountCode::mock();
265
266        let note = UpgradeNote::builder()
267            .sender(sender)
268            .target(target)
269            .code(code.clone())
270            .generate_serial_number(&mut rng)
271            .build()?;
272
273        assert_eq!(note.sender(), sender);
274        assert_eq!(note.target(), target);
275        assert_eq!(note.new_code_commitment(), code.commitment());
276
277        let note = Note::from(note);
278        assert_eq!(note.metadata().note_type(), NoteType::Public);
279        assert_eq!(note.metadata().tag(), NoteTag::with_account_target(target));
280        assert_eq!(note.assets().num_assets(), 0);
281        assert_eq!(note.storage().items(), code.commitment().as_elements());
282        assert_eq!(note.storage().items().len(), UpgradeNote::NUM_STORAGE_ITEMS);
283
284        Ok(())
285    }
286
287    /// The built note carries a `NetworkAccountTarget` attachment bound to the upgraded account and
288    /// the new code in `AccountCodeUpgradeAttachment` chunks.
289    #[rstest::rstest]
290    #[case::single_chunk(1)]
291    #[case::two_chunks(200)]
292    fn note_carries_target_and_code_attachments(
293        #[case] num_procedures: usize,
294    ) -> anyhow::Result<()> {
295        let target = account_id(1);
296        let code = code_with_procedures(num_procedures)?;
297        let note = Note::from(build_upgrade_note(target, code.clone())?);
298
299        let network_target = NetworkAccountTarget::try_from(note.attachments())?;
300        assert_eq!(network_target.target_id(), target);
301
302        let code_upgrade = AccountCodeUpgradeAttachment::try_from_attachments(note.attachments())?;
303        assert_eq!(code_upgrade.code_upgrade().code(), &code);
304
305        Ok(())
306    }
307
308    /// Code that does not fit into the note attachments is rejected.
309    #[test]
310    fn too_large_code_is_rejected() -> anyhow::Result<()> {
311        let code = code_with_procedures(AccountCode::MAX_NUM_PROCEDURES - 1)?;
312        let result = build_upgrade_note(account_id(1), code);
313
314        assert_matches!(result, Err(NoteError::NoteAttachmentsTooManyWords(_)));
315
316        Ok(())
317    }
318}