use alloc::vec::Vec;
use miden_protocol::account::AccountCodeUpgrade;
use miden_protocol::errors::NoteError;
use miden_protocol::note::{NoteAttachment, NoteAttachmentScheme, NoteAttachments};
use miden_protocol::utils::serde::DeserializationError;
use miden_protocol::{Felt, Word};
use crate::note::StandardNoteAttachment;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountCodeUpgradeAttachment {
code_upgrade: AccountCodeUpgrade,
}
impl AccountCodeUpgradeAttachment {
pub const ATTACHMENT_SCHEME: NoteAttachmentScheme =
StandardNoteAttachment::AccountCodeUpgrade.attachment_scheme();
pub fn new(code_upgrade: AccountCodeUpgrade) -> Self {
Self { code_upgrade }
}
pub fn try_from_attachments(
attachments: &NoteAttachments,
) -> Result<Self, AccountCodeUpgradeAttachmentError> {
Self::from_chunks(attachments.iter())
}
pub(crate) fn ensure_presence(
attachments: &mut Vec<NoteAttachment>,
code_upgrade: AccountCodeUpgrade,
) -> Result<(), NoteError> {
let is_present =
Self::validate_code(attachments, code_upgrade.commitment()).map_err(|err| {
NoteError::other_with_source("attached account code upgrade is invalid", err)
})?;
if !is_present {
attachments.extend(Self::new(code_upgrade).to_attachments()?);
}
Ok(())
}
pub fn code_upgrade(&self) -> &AccountCodeUpgrade {
&self.code_upgrade
}
pub fn into_code_upgrade(self) -> AccountCodeUpgrade {
self.code_upgrade
}
pub fn to_attachments(&self) -> Result<Vec<NoteAttachment>, NoteError> {
let elements = self.code_upgrade.to_elements();
let words: Vec<Word> = elements
.as_chunks::<{ Word::NUM_ELEMENTS }>()
.0
.iter()
.map(|word_elements| Word::new(*word_elements))
.collect();
words
.chunks(usize::from(NoteAttachment::MAX_NUM_WORDS))
.map(|chunk| NoteAttachment::with_words(Self::ATTACHMENT_SCHEME, chunk.to_vec()))
.collect()
}
fn from_chunks<'attachment>(
attachments: impl IntoIterator<Item = &'attachment NoteAttachment>,
) -> Result<Self, AccountCodeUpgradeAttachmentError> {
let elements: Vec<Felt> = attachments
.into_iter()
.filter(|attachment| attachment.attachment_scheme() == Self::ATTACHMENT_SCHEME)
.flat_map(|attachment| attachment.as_elements().iter().copied())
.collect();
if elements.is_empty() {
return Err(AccountCodeUpgradeAttachmentError::MissingCodeAttachment);
}
AccountCodeUpgrade::try_from_elements(&elements)
.map(Self::new)
.map_err(AccountCodeUpgradeAttachmentError::DecodeCode)
}
fn validate_code(
attachments: &[NoteAttachment],
code_commitment: Word,
) -> Result<bool, AccountCodeUpgradeAttachmentError> {
match Self::from_chunks(attachments) {
Ok(attachment) => {
let actual = attachment.code_upgrade.commitment();
if actual != code_commitment {
return Err(AccountCodeUpgradeAttachmentError::CodeCommitmentMismatch {
expected: code_commitment,
actual,
});
}
Ok(true)
},
Err(AccountCodeUpgradeAttachmentError::MissingCodeAttachment) => Ok(false),
Err(err) => Err(err),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum AccountCodeUpgradeAttachmentError {
#[error(
"note attachments do not contain an attachment of scheme {scheme}",
scheme = AccountCodeUpgradeAttachment::ATTACHMENT_SCHEME
)]
MissingCodeAttachment,
#[error("failed to decode the account code of the attachments")]
DecodeCode(#[source] DeserializationError),
#[error("attached account code {actual} does not match expected code {expected}")]
CodeCommitmentMismatch { expected: Word, actual: Word },
}
#[cfg(test)]
mod tests {
use alloc::vec;
use alloc::vec::Vec;
use assert_matches::assert_matches;
use miden_protocol::Word;
use miden_protocol::account::{AccountCode, AccountCodeUpgrade};
use miden_protocol::errors::NoteError;
use miden_protocol::note::{NoteAttachment, NoteAttachmentScheme, NoteAttachments};
use super::{AccountCodeUpgradeAttachment, AccountCodeUpgradeAttachmentError};
use crate::testing::account_component::{IncrNonceAuthComponent, MockProceduresComponent};
fn code_attachment(num_procedures: usize) -> anyhow::Result<AccountCodeUpgradeAttachment> {
let code = AccountCode::from_components(&[
IncrNonceAuthComponent.into(),
MockProceduresComponent::new(num_procedures).into(),
])?;
Ok(AccountCodeUpgradeAttachment::new(AccountCodeUpgrade::new(code)))
}
#[rstest::rstest]
#[case::single_chunk(1, 1)]
#[case::two_chunks(200, 2)]
fn attachments_roundtrip(
#[case] num_procedures: usize,
#[case] expected_num_chunks: usize,
) -> anyhow::Result<()> {
let attachment = code_attachment(num_procedures)?;
let chunks = attachment.to_attachments()?;
assert_eq!(chunks.len(), expected_num_chunks);
let (_, advice_map_value) = attachment.code_upgrade().to_advice_map_entry();
let chunk_elements: Vec<_> =
chunks.iter().flat_map(|chunk| chunk.as_elements().iter().copied()).collect();
assert_eq!(chunk_elements, advice_map_value);
let note_attachments = NoteAttachments::new(chunks)?;
assert_eq!(
AccountCodeUpgradeAttachment::try_from_attachments(¬e_attachments)?,
attachment
);
Ok(())
}
#[test]
fn attachments_without_chunk_are_rejected() -> anyhow::Result<()> {
let other_attachment =
NoteAttachment::with_word(NoteAttachmentScheme::new(64)?, Word::empty());
let note_attachments = NoteAttachments::new(vec![other_attachment])?;
assert_matches!(
AccountCodeUpgradeAttachment::try_from_attachments(¬e_attachments),
Err(AccountCodeUpgradeAttachmentError::MissingCodeAttachment)
);
Ok(())
}
#[test]
fn ensure_presence_keeps_matching_chunks() -> anyhow::Result<()> {
let attachment = code_attachment(200)?;
let unrelated =
NoteAttachment::with_word(NoteAttachmentScheme::new(64)?, Word::from([7u32, 0, 0, 0]));
let mut attachments = attachment.to_attachments()?;
attachments.push(unrelated);
let supplied = attachments.clone();
AccountCodeUpgradeAttachment::ensure_presence(
&mut attachments,
attachment.code_upgrade().clone(),
)?;
assert_eq!(attachments, supplied);
Ok(())
}
#[test]
fn ensure_presence_rejects_other_code() -> anyhow::Result<()> {
let attachment = code_attachment(1)?;
let other_attachment = code_attachment(2)?;
let mut attachments = other_attachment.to_attachments()?;
let result = AccountCodeUpgradeAttachment::ensure_presence(
&mut attachments,
attachment.code_upgrade().clone(),
);
assert_matches!(result, Err(NoteError::Other { source: Some(source), .. })
if matches!(
source.downcast_ref::<AccountCodeUpgradeAttachmentError>(),
Some(AccountCodeUpgradeAttachmentError::CodeCommitmentMismatch { expected, actual })
if *expected == attachment.code_upgrade().commitment()
&& *actual == other_attachment.code_upgrade().commitment()
)
);
Ok(())
}
#[test]
fn ensure_presence_appends_missing_chunks() -> anyhow::Result<()> {
let attachment = code_attachment(200)?;
let unrelated =
NoteAttachment::with_word(NoteAttachmentScheme::new(64)?, Word::from([7u32, 0, 0, 0]));
let mut attachments = vec![unrelated.clone()];
AccountCodeUpgradeAttachment::ensure_presence(
&mut attachments,
attachment.code_upgrade().clone(),
)?;
let mut expected = vec![unrelated];
expected.extend(attachment.to_attachments()?);
assert_eq!(attachments, expected);
Ok(())
}
}