Skip to main content

miden_standards/note/
account_code_upgrade_attachment.rs

1use alloc::vec::Vec;
2
3use miden_protocol::account::AccountCodeUpgrade;
4use miden_protocol::errors::NoteError;
5use miden_protocol::note::{NoteAttachment, NoteAttachmentScheme, NoteAttachments};
6use miden_protocol::utils::serde::DeserializationError;
7use miden_protocol::{Felt, Word};
8
9use crate::note::StandardNoteAttachment;
10
11// ACCOUNT CODE UPGRADE ATTACHMENT
12// ================================================================================================
13
14/// Carries an [`AccountCodeUpgrade`] in the attachments of a note, so that a transaction consuming
15/// the note can upgrade an account to its code.
16///
17/// The encoding of [`AccountCodeUpgrade::to_elements`], which is also the value of the upgrade's
18/// advice map entry, is split into chunks of at most [`NoteAttachment::MAX_NUM_WORDS`] words, one
19/// [`NoteAttachment`] per chunk. Code that is too large for a single attachment can thus still be
20/// carried by a note, within the limits of [`NoteAttachments`]. The chunks are joined in the order
21/// in which they appear in the note's attachments, which the note commits to.
22///
23/// The [`UpgradeNote`](crate::note::UpgradeNote) script joins the chunks and inserts the code
24/// into the advice map under [`AccountCodeUpgrade::advice_map_key`].
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct AccountCodeUpgradeAttachment {
27    code_upgrade: AccountCodeUpgrade,
28}
29
30impl AccountCodeUpgradeAttachment {
31    // CONSTANTS
32    // --------------------------------------------------------------------------------------------
33
34    /// The standardized scheme of [`AccountCodeUpgradeAttachment`] chunks.
35    pub const ATTACHMENT_SCHEME: NoteAttachmentScheme =
36        StandardNoteAttachment::AccountCodeUpgrade.attachment_scheme();
37
38    // CONSTRUCTORS
39    // --------------------------------------------------------------------------------------------
40
41    /// Returns a new [`AccountCodeUpgradeAttachment`] that carries `code_upgrade`.
42    pub fn new(code_upgrade: AccountCodeUpgrade) -> Self {
43        Self { code_upgrade }
44    }
45
46    /// Decodes the [`AccountCodeUpgradeAttachment`] from the chunks among `attachments`.
47    ///
48    /// Attachments of other schemes are ignored.
49    ///
50    /// # Errors
51    ///
52    /// Returns an error if:
53    /// - `attachments` do not contain any chunk.
54    /// - the joined chunks do not encode valid account code.
55    pub fn try_from_attachments(
56        attachments: &NoteAttachments,
57    ) -> Result<Self, AccountCodeUpgradeAttachmentError> {
58        Self::from_chunks(attachments.iter())
59    }
60
61    /// Ensures `attachments` carry the chunks of `code_upgrade`, appending them if none are
62    /// present.
63    ///
64    /// This lets the caller supply the chunks themselves, e.g. to place them among their other
65    /// attachments in their own order.
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if:
70    /// - the chunks in `attachments` do not decode or carry code other than `code_upgrade`.
71    /// - no chunk is present and the chunks of `code_upgrade` cannot be built.
72    pub(crate) fn ensure_presence(
73        attachments: &mut Vec<NoteAttachment>,
74        code_upgrade: AccountCodeUpgrade,
75    ) -> Result<(), NoteError> {
76        let is_present =
77            Self::validate_code(attachments, code_upgrade.commitment()).map_err(|err| {
78                NoteError::other_with_source("attached account code upgrade is invalid", err)
79            })?;
80
81        if !is_present {
82            attachments.extend(Self::new(code_upgrade).to_attachments()?);
83        }
84
85        Ok(())
86    }
87
88    // PUBLIC ACCESSORS
89    // --------------------------------------------------------------------------------------------
90
91    /// Returns a reference to the carried code upgrade.
92    pub fn code_upgrade(&self) -> &AccountCodeUpgrade {
93        &self.code_upgrade
94    }
95
96    /// Consumes self and returns the carried code upgrade.
97    pub fn into_code_upgrade(self) -> AccountCodeUpgrade {
98        self.code_upgrade
99    }
100
101    /// Returns the chunks of the carried code upgrade as note attachments.
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if a chunk cannot be converted into a note attachment.
106    pub fn to_attachments(&self) -> Result<Vec<NoteAttachment>, NoteError> {
107        // The encoding is padded to whole words, so there is no remainder.
108        let elements = self.code_upgrade.to_elements();
109        let words: Vec<Word> = elements
110            .as_chunks::<{ Word::NUM_ELEMENTS }>()
111            .0
112            .iter()
113            .map(|word_elements| Word::new(*word_elements))
114            .collect();
115
116        words
117            .chunks(usize::from(NoteAttachment::MAX_NUM_WORDS))
118            .map(|chunk| NoteAttachment::with_words(Self::ATTACHMENT_SCHEME, chunk.to_vec()))
119            .collect()
120    }
121
122    // HELPERS
123    // --------------------------------------------------------------------------------------------
124
125    /// Decodes the [`AccountCodeUpgradeAttachment`] from the chunks among `attachments`, see
126    /// [`Self::try_from_attachments`].
127    fn from_chunks<'attachment>(
128        attachments: impl IntoIterator<Item = &'attachment NoteAttachment>,
129    ) -> Result<Self, AccountCodeUpgradeAttachmentError> {
130        let elements: Vec<Felt> = attachments
131            .into_iter()
132            .filter(|attachment| attachment.attachment_scheme() == Self::ATTACHMENT_SCHEME)
133            .flat_map(|attachment| attachment.as_elements().iter().copied())
134            .collect();
135
136        if elements.is_empty() {
137            return Err(AccountCodeUpgradeAttachmentError::MissingCodeAttachment);
138        }
139
140        AccountCodeUpgrade::try_from_elements(&elements)
141            .map(Self::new)
142            .map_err(AccountCodeUpgradeAttachmentError::DecodeCode)
143    }
144
145    /// Validates the chunks among `attachments` against `code_commitment`, returning whether any
146    /// chunk is present.
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if the chunks do not decode or carry code with another commitment.
151    fn validate_code(
152        attachments: &[NoteAttachment],
153        code_commitment: Word,
154    ) -> Result<bool, AccountCodeUpgradeAttachmentError> {
155        match Self::from_chunks(attachments) {
156            Ok(attachment) => {
157                let actual = attachment.code_upgrade.commitment();
158                if actual != code_commitment {
159                    return Err(AccountCodeUpgradeAttachmentError::CodeCommitmentMismatch {
160                        expected: code_commitment,
161                        actual,
162                    });
163                }
164
165                Ok(true)
166            },
167            Err(AccountCodeUpgradeAttachmentError::MissingCodeAttachment) => Ok(false),
168            Err(err) => Err(err),
169        }
170    }
171}
172
173// ACCOUNT CODE UPGRADE ATTACHMENT ERROR
174// ================================================================================================
175
176/// Errors that can occur when decoding or validating an [`AccountCodeUpgradeAttachment`] from note
177/// attachments.
178#[derive(Debug, thiserror::Error)]
179pub enum AccountCodeUpgradeAttachmentError {
180    #[error(
181        "note attachments do not contain an attachment of scheme {scheme}",
182        scheme = AccountCodeUpgradeAttachment::ATTACHMENT_SCHEME
183    )]
184    MissingCodeAttachment,
185    #[error("failed to decode the account code of the attachments")]
186    DecodeCode(#[source] DeserializationError),
187    #[error("attached account code {actual} does not match expected code {expected}")]
188    CodeCommitmentMismatch { expected: Word, actual: Word },
189}
190
191// TESTS
192// ================================================================================================
193
194#[cfg(test)]
195mod tests {
196    use alloc::vec;
197    use alloc::vec::Vec;
198
199    use assert_matches::assert_matches;
200    use miden_protocol::Word;
201    use miden_protocol::account::{AccountCode, AccountCodeUpgrade};
202    use miden_protocol::errors::NoteError;
203    use miden_protocol::note::{NoteAttachment, NoteAttachmentScheme, NoteAttachments};
204
205    use super::{AccountCodeUpgradeAttachment, AccountCodeUpgradeAttachmentError};
206    use crate::testing::account_component::{IncrNonceAuthComponent, MockProceduresComponent};
207
208    /// Returns an attachment carrying code with `num_procedures` procedures.
209    fn code_attachment(num_procedures: usize) -> anyhow::Result<AccountCodeUpgradeAttachment> {
210        let code = AccountCode::from_components(&[
211            IncrNonceAuthComponent.into(),
212            MockProceduresComponent::new(num_procedures).into(),
213        ])?;
214
215        Ok(AccountCodeUpgradeAttachment::new(AccountCodeUpgrade::new(code)))
216    }
217
218    /// The chunks carry the upgrade's advice map entry and decode back to the same upgrade.
219    #[rstest::rstest]
220    #[case::single_chunk(1, 1)]
221    #[case::two_chunks(200, 2)]
222    fn attachments_roundtrip(
223        #[case] num_procedures: usize,
224        #[case] expected_num_chunks: usize,
225    ) -> anyhow::Result<()> {
226        let attachment = code_attachment(num_procedures)?;
227
228        let chunks = attachment.to_attachments()?;
229        assert_eq!(chunks.len(), expected_num_chunks);
230
231        let (_, advice_map_value) = attachment.code_upgrade().to_advice_map_entry();
232        let chunk_elements: Vec<_> =
233            chunks.iter().flat_map(|chunk| chunk.as_elements().iter().copied()).collect();
234        assert_eq!(chunk_elements, advice_map_value);
235
236        let note_attachments = NoteAttachments::new(chunks)?;
237        assert_eq!(
238            AccountCodeUpgradeAttachment::try_from_attachments(&note_attachments)?,
239            attachment
240        );
241
242        Ok(())
243    }
244
245    #[test]
246    fn attachments_without_chunk_are_rejected() -> anyhow::Result<()> {
247        let other_attachment =
248            NoteAttachment::with_word(NoteAttachmentScheme::new(64)?, Word::empty());
249        let note_attachments = NoteAttachments::new(vec![other_attachment])?;
250
251        assert_matches!(
252            AccountCodeUpgradeAttachment::try_from_attachments(&note_attachments),
253            Err(AccountCodeUpgradeAttachmentError::MissingCodeAttachment)
254        );
255
256        Ok(())
257    }
258
259    /// Caller-supplied chunks of the same code are kept in their position, and no duplicate chunks
260    /// are appended.
261    #[test]
262    fn ensure_presence_keeps_matching_chunks() -> anyhow::Result<()> {
263        let attachment = code_attachment(200)?;
264        let unrelated =
265            NoteAttachment::with_word(NoteAttachmentScheme::new(64)?, Word::from([7u32, 0, 0, 0]));
266        let mut attachments = attachment.to_attachments()?;
267        attachments.push(unrelated);
268        let supplied = attachments.clone();
269
270        AccountCodeUpgradeAttachment::ensure_presence(
271            &mut attachments,
272            attachment.code_upgrade().clone(),
273        )?;
274
275        assert_eq!(attachments, supplied);
276
277        Ok(())
278    }
279
280    /// Caller-supplied chunks of other code are rejected instead of being silently shadowed by
281    /// the chunks of the expected code.
282    #[test]
283    fn ensure_presence_rejects_other_code() -> anyhow::Result<()> {
284        let attachment = code_attachment(1)?;
285        let other_attachment = code_attachment(2)?;
286        let mut attachments = other_attachment.to_attachments()?;
287
288        let result = AccountCodeUpgradeAttachment::ensure_presence(
289            &mut attachments,
290            attachment.code_upgrade().clone(),
291        );
292
293        assert_matches!(result, Err(NoteError::Other { source: Some(source), .. })
294            if matches!(
295                source.downcast_ref::<AccountCodeUpgradeAttachmentError>(),
296                Some(AccountCodeUpgradeAttachmentError::CodeCommitmentMismatch { expected, actual })
297                    if *expected == attachment.code_upgrade().commitment()
298                        && *actual == other_attachment.code_upgrade().commitment()
299            )
300        );
301
302        Ok(())
303    }
304
305    /// The appended chunks are placed after the caller's attachments, leaving their order intact.
306    #[test]
307    fn ensure_presence_appends_missing_chunks() -> anyhow::Result<()> {
308        let attachment = code_attachment(200)?;
309        let unrelated =
310            NoteAttachment::with_word(NoteAttachmentScheme::new(64)?, Word::from([7u32, 0, 0, 0]));
311        let mut attachments = vec![unrelated.clone()];
312
313        AccountCodeUpgradeAttachment::ensure_presence(
314            &mut attachments,
315            attachment.code_upgrade().clone(),
316        )?;
317
318        let mut expected = vec![unrelated];
319        expected.extend(attachment.to_attachments()?);
320        assert_eq!(attachments, expected);
321
322        Ok(())
323    }
324}