miden_standards/note/
account_code_upgrade_attachment.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct AccountCodeUpgradeAttachment {
27 code_upgrade: AccountCodeUpgrade,
28}
29
30impl AccountCodeUpgradeAttachment {
31 pub const ATTACHMENT_SCHEME: NoteAttachmentScheme =
36 StandardNoteAttachment::AccountCodeUpgrade.attachment_scheme();
37
38 pub fn new(code_upgrade: AccountCodeUpgrade) -> Self {
43 Self { code_upgrade }
44 }
45
46 pub fn try_from_attachments(
56 attachments: &NoteAttachments,
57 ) -> Result<Self, AccountCodeUpgradeAttachmentError> {
58 Self::from_chunks(attachments.iter())
59 }
60
61 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 pub fn code_upgrade(&self) -> &AccountCodeUpgrade {
93 &self.code_upgrade
94 }
95
96 pub fn into_code_upgrade(self) -> AccountCodeUpgrade {
98 self.code_upgrade
99 }
100
101 pub fn to_attachments(&self) -> Result<Vec<NoteAttachment>, NoteError> {
107 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 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 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#[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#[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 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 #[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(¬e_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(¬e_attachments),
253 Err(AccountCodeUpgradeAttachmentError::MissingCodeAttachment)
254 );
255
256 Ok(())
257 }
258
259 #[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 #[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 #[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}