use alloc::vec::Vec;
use miden_protocol::Word;
use miden_protocol::account::{AccountCode, AccountCodeUpgrade, AccountId};
use miden_protocol::assembly::Path;
use miden_protocol::crypto::rand::FeltRng;
use miden_protocol::errors::NoteError;
use miden_protocol::note::{
Note,
NoteAssets,
NoteAttachment,
NoteAttachments,
NoteRecipient,
NoteScript,
NoteScriptRoot,
NoteStorage,
NoteTag,
NoteType,
PartialNoteMetadata,
};
use miden_protocol::utils::sync::LazyLock;
use crate::StandardsLib;
use crate::note::costs::{NoteConsumptionCost, UPGRADE_CONSUMPTION_CYCLES};
use crate::note::{AccountCodeUpgradeAttachment, NetworkAccountTarget};
const UPGRADE_SCRIPT_PATH: &str = "::miden::standards::notes::upgrade::main";
static UPGRADE_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let standards_lib = StandardsLib::default();
let path = Path::new(UPGRADE_SCRIPT_PATH);
NoteScript::from_package_reference(standards_lib.as_ref(), path)
.expect("Standards library contains UPGRADE note script procedure")
});
#[derive(Debug, Clone)]
pub struct UpgradeNote {
sender: AccountId,
target: AccountId,
new_code_commitment: Word,
serial_number: Word,
attachments: NoteAttachments,
}
#[bon::bon]
impl UpgradeNote {
#[builder]
pub fn new(
#[builder(field)] mut attachments: Vec<NoteAttachment>,
sender: AccountId,
target: AccountId,
code: AccountCode,
serial_number: Word,
) -> Result<Self, NoteError> {
let code_upgrade = AccountCodeUpgrade::new(code);
let new_code_commitment = code_upgrade.commitment();
AccountCodeUpgradeAttachment::ensure_presence(&mut attachments, code_upgrade)?;
NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
NoteError::other_with_source(
"failed to bind the upgrade note to its target account",
err,
)
})?;
let attachments = NoteAttachments::new(attachments)?;
Ok(Self {
sender,
target,
new_code_commitment,
serial_number,
attachments,
})
}
}
impl UpgradeNote {
pub const NUM_STORAGE_ITEMS: usize = Word::NUM_ELEMENTS;
pub fn script() -> NoteScript {
UPGRADE_SCRIPT.clone()
}
pub fn script_root() -> NoteScriptRoot {
UPGRADE_SCRIPT.root()
}
pub fn sender(&self) -> AccountId {
self.sender
}
pub fn target(&self) -> AccountId {
self.target
}
pub fn new_code_commitment(&self) -> Word {
self.new_code_commitment
}
pub fn serial_number(&self) -> Word {
self.serial_number
}
pub fn attachments(&self) -> &NoteAttachments {
&self.attachments
}
}
impl<S: upgrade_note_builder::State> UpgradeNoteBuilder<S> {
pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
self.attachments.push(attachment.into());
self
}
pub fn attachments(
mut self,
attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
) -> Self {
self.attachments.extend(attachments.into_iter().map(Into::into));
self
}
}
impl<S: upgrade_note_builder::State> UpgradeNoteBuilder<S>
where
S::SerialNumber: upgrade_note_builder::IsUnset,
{
pub fn generate_serial_number(
self,
rng: &mut impl FeltRng,
) -> UpgradeNoteBuilder<upgrade_note_builder::SetSerialNumber<S>> {
self.serial_number(rng.draw_word())
}
}
impl From<UpgradeNote> for Note {
fn from(note: UpgradeNote) -> Self {
let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
.with_tag(NoteTag::with_account_target(note.target));
let storage = NoteStorage::new(note.new_code_commitment.as_elements().to_vec())
.expect("number of storage items should not exceed max storage items");
let recipient = NoteRecipient::new(note.serial_number, UpgradeNote::script(), storage);
Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
}
}
impl NoteConsumptionCost for UpgradeNote {
fn consumption_cycles() -> u32 {
UPGRADE_CONSUMPTION_CYCLES
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use miden_protocol::account::AccountType;
use miden_protocol::crypto::rand::RandomCoin;
use super::*;
use crate::testing::account_component::{IncrNonceAuthComponent, MockProceduresComponent};
fn account_id(seed: u8) -> AccountId {
AccountId::builder()
.account_type(AccountType::Public)
.build_with_seed([seed; 32])
}
fn build_upgrade_note(target: AccountId, code: AccountCode) -> Result<UpgradeNote, NoteError> {
UpgradeNote::builder()
.sender(account_id(2))
.target(target)
.code(code)
.serial_number(Word::empty())
.build()
}
fn code_with_procedures(num_procedures: usize) -> anyhow::Result<AccountCode> {
Ok(AccountCode::from_components(&[
IncrNonceAuthComponent.into(),
MockProceduresComponent::new(num_procedures).into(),
])?)
}
#[test]
fn builder_builds_upgrade_note() -> anyhow::Result<()> {
let mut rng = RandomCoin::new(Word::empty());
let target = account_id(1);
let sender = account_id(2);
let code = AccountCode::mock();
let note = UpgradeNote::builder()
.sender(sender)
.target(target)
.code(code.clone())
.generate_serial_number(&mut rng)
.build()?;
assert_eq!(note.sender(), sender);
assert_eq!(note.target(), target);
assert_eq!(note.new_code_commitment(), code.commitment());
let note = Note::from(note);
assert_eq!(note.metadata().note_type(), NoteType::Public);
assert_eq!(note.metadata().tag(), NoteTag::with_account_target(target));
assert_eq!(note.assets().num_assets(), 0);
assert_eq!(note.storage().items(), code.commitment().as_elements());
assert_eq!(note.storage().items().len(), UpgradeNote::NUM_STORAGE_ITEMS);
Ok(())
}
#[rstest::rstest]
#[case::single_chunk(1)]
#[case::two_chunks(200)]
fn note_carries_target_and_code_attachments(
#[case] num_procedures: usize,
) -> anyhow::Result<()> {
let target = account_id(1);
let code = code_with_procedures(num_procedures)?;
let note = Note::from(build_upgrade_note(target, code.clone())?);
let network_target = NetworkAccountTarget::try_from(note.attachments())?;
assert_eq!(network_target.target_id(), target);
let code_upgrade = AccountCodeUpgradeAttachment::try_from_attachments(note.attachments())?;
assert_eq!(code_upgrade.code_upgrade().code(), &code);
Ok(())
}
#[test]
fn too_large_code_is_rejected() -> anyhow::Result<()> {
let code = code_with_procedures(AccountCode::MAX_NUM_PROCEDURES - 1)?;
let result = build_upgrade_note(account_id(1), code);
assert_matches!(result, Err(NoteError::NoteAttachmentsTooManyWords(_)));
Ok(())
}
}