miden-protocol 0.16.0-alpha.4

Core components of the Miden protocol
Documentation
use alloc::sync::Arc;

use assert_matches::assert_matches;

use super::{PublicOutputNote, RawOutputNote, RawOutputNotes};
use crate::account::AccountId;
use crate::assembly::mast::{ExternalNodeBuilder, JoinNodeBuilder, MastForest};
use crate::asset::FungibleAsset;
use crate::constants::NOTE_MAX_SIZE;
use crate::errors::{OutputNoteError, TransactionOutputError};
use crate::note::{
    Note,
    NoteAssets,
    NoteRecipient,
    NoteScript,
    NoteStorage,
    NoteTag,
    NoteType,
    PartialNoteMetadata,
};
use crate::testing::account_id::{
    ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
    ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
    ACCOUNT_ID_SENDER,
};
use crate::utils::serde::Serializable;
use crate::{Felt, Word};

#[test]
fn test_duplicate_output_notes() -> anyhow::Result<()> {
    let mock_note = Note::mock_noop(Word::empty());
    let mock_note_id = mock_note.id();
    let mock_note_clone = mock_note.clone();

    let error = RawOutputNotes::new(vec![
        RawOutputNote::Full(mock_note),
        RawOutputNote::Full(mock_note_clone),
    ])
    .expect_err("input notes creation should fail");

    assert_matches!(error, TransactionOutputError::DuplicateOutputNote(note_id) if note_id == mock_note_id);

    Ok(())
}

#[test]
fn output_note_size_hint_matches_serialized_length() -> anyhow::Result<()> {
    let sender_id = ACCOUNT_ID_SENDER.try_into().unwrap();

    // Build a note with at least two assets.
    let faucet_id_1 = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
    let faucet_id_2 = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();

    let asset_1 = FungibleAsset::new(faucet_id_1, 100)?.into();
    let asset_2 = FungibleAsset::new(faucet_id_2, 200)?.into();

    let assets = NoteAssets::new(vec![asset_1, asset_2])?;

    // Build metadata similarly to how mock notes are constructed.
    let metadata = PartialNoteMetadata::new(sender_id, NoteType::Private)
        .with_tag(NoteTag::with_account_target(sender_id));

    // Build storage with at least two values.
    let storage = NoteStorage::new(vec![Felt::ONE, Felt::new_unchecked(2)])?;

    let serial_num = Word::empty();
    let script = NoteScript::mock();
    let recipient = NoteRecipient::new(serial_num, script, storage);

    let note = Note::new(assets, metadata, recipient);
    let output_note = RawOutputNote::Full(note);

    let bytes = output_note.to_bytes();

    assert_eq!(bytes.len(), output_note.get_size_hint());

    Ok(())
}

// Construct a public note whose serialized size exceeds NOTE_MAX_SIZE by building a MastForest with
// many reachable external nodes. External nodes carry no debug info, so `minify_script()` (called
// inside `PublicOutputNote::new()`) cannot shrink them below the limit.
#[test]
fn oversized_public_note_triggers_size_limit_error() -> anyhow::Result<()> {
    let sender_id = ACCOUNT_ID_SENDER.try_into().unwrap();

    // Build a large reachable MastForest by joining many external nodes. Each external node stores
    // a 32-byte digest, and each join node keeps the previous nodes reachable after compaction. The
    // joins are balanced to keep recursive traversals shallow.
    let mut mast = MastForest::new();
    let mut roots = alloc::vec::Vec::new();
    for i in 0..7_000_u16 {
        let digest = Word::new([Felt::from(i + 1), Felt::ZERO, Felt::ZERO, Felt::ZERO]);
        let external_id = ExternalNodeBuilder::new(digest)
            .add_to_forest(&mut mast)
            .expect("adding external node should not fail");
        roots.push(external_id);
    }
    while roots.len() > 1 {
        let mut next_roots = alloc::vec::Vec::with_capacity(roots.len().div_ceil(2));
        for chunk in roots.chunks(2) {
            let root_id = match chunk {
                [left, right] => JoinNodeBuilder::new([*left, *right])
                    .add_to_forest(&mut mast)
                    .expect("adding join node should not fail"),
                [root] => *root,
                _ => unreachable!("chunks of two have one or two elements"),
            };
            next_roots.push(root_id);
        }
        roots = next_roots;
    }
    let root_id = roots.pop().expect("at least one root should exist");
    mast.make_root(root_id);

    let script = NoteScript::from_parts(Arc::new(mast), root_id);

    let serial_num = Word::empty();
    let storage = NoteStorage::new(alloc::vec::Vec::new())?;

    // Create a public note (NoteType::Public is required for PublicOutputNote)
    let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
    let asset = FungibleAsset::new(faucet_id, 100)?.into();
    let assets = NoteAssets::new(vec![asset])?;

    let metadata = PartialNoteMetadata::new(sender_id, NoteType::Public)
        .with_tag(NoteTag::with_account_target(sender_id));

    let recipient = NoteRecipient::new(serial_num, script, storage);
    let oversized_note = Note::new(assets, metadata, recipient);

    // Sanity-check that our constructed note is indeed larger than the configured
    // maximum.
    let computed_note_size = oversized_note.get_size_hint();
    assert!(
        computed_note_size > NOTE_MAX_SIZE as usize,
        "Expected note size ({computed_note_size}) to exceed NOTE_MAX_SIZE ({NOTE_MAX_SIZE})"
    );
    let mut minified_note = oversized_note.clone();
    minified_note.clear_debug_info();
    let minified_note_size = minified_note.get_size_hint();
    assert!(
        minified_note_size > NOTE_MAX_SIZE as usize,
        "Expected minified note size ({minified_note_size}) to exceed NOTE_MAX_SIZE ({NOTE_MAX_SIZE})"
    );

    // Creating a PublicOutputNote should fail with size limit error
    let result = PublicOutputNote::new(oversized_note.clone());

    assert_matches!(
        result,
        Err(OutputNoteError::NoteSizeLimitExceeded { note_id: _, note_size })
            if note_size > NOTE_MAX_SIZE as usize
    );

    // to_output_note() should also fail
    let output_note = RawOutputNote::Full(oversized_note);
    let result = output_note.into_output_note();

    assert_matches!(
        result,
        Err(OutputNoteError::NoteSizeLimitExceeded { note_id: _, note_size })
            if note_size > NOTE_MAX_SIZE as usize
    );

    Ok(())
}