miden-standards 0.17.0

Standards of the Miden protocol
Documentation
use alloc::vec::Vec;

use miden_protocol::Word;
use miden_protocol::account::AccountId;
use miden_protocol::errors::AccountIdError;
use miden_protocol::note::{NoteAttachment, NoteAttachmentScheme, NoteAttachments, NoteType};

use crate::note::{NoteExecutionHint, StandardNoteAttachment};

// NETWORK ACCOUNT TARGET
// ================================================================================================

/// A [`NoteAttachment`] for notes targeted at network accounts.
///
/// It can be encoded to and from a single-word attachment content with the following layout:
///
/// ```text
/// - 0th felt: [target_id_suffix (56 bits) | 8 zero bits]
/// - 1st felt: [target_id_prefix (64 bits)]
/// - 2nd felt: [24 zero bits | exec_hint_payload (32 bits) | exec_hint_tag (8 bits)]
/// - 3rd felt: [64 zero bits]
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NetworkAccountTarget {
    target_id: AccountId,
    exec_hint: NoteExecutionHint,
}

impl NetworkAccountTarget {
    // CONSTANTS
    // --------------------------------------------------------------------------------------------

    /// The standardized scheme of [`NetworkAccountTarget`] attachments.
    pub const ATTACHMENT_SCHEME: NoteAttachmentScheme =
        StandardNoteAttachment::NetworkAccountTarget.attachment_scheme();

    // CONSTRUCTORS
    // --------------------------------------------------------------------------------------------

    /// Creates a new [`NetworkAccountTarget`] from the provided parts.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - the provided `target_id` does not have
    ///   [`AccountType::Public`](miden_protocol::account::AccountType::Public).
    pub fn new(
        target_id: AccountId,
        exec_hint: NoteExecutionHint,
    ) -> Result<Self, NetworkAccountTargetError> {
        if !target_id.is_public() {
            return Err(NetworkAccountTargetError::TargetNotPublic(target_id));
        }

        Ok(Self { target_id, exec_hint })
    }

    /// Ensures `attachments` carries a [`NetworkAccountTarget`] for `target_id`, appending one with
    /// [`NoteExecutionHint::Always`] if none is present.
    ///
    /// This lets a note that is always targeted at a single network account derive its target from
    /// that account, while leaving the caller free to supply the target themselves, e.g. to pick a
    /// different execution hint, and to add any number of unrelated attachments in their own order.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - an attachment with the [`NetworkAccountTarget::ATTACHMENT_SCHEME`] does not decode as a
    ///   [`NetworkAccountTarget`] or targets an account other than `target_id`.
    /// - no such attachment is present and `target_id` is not
    ///   [`AccountType::Public`](miden_protocol::account::AccountType::Public), since a network
    ///   account must be public.
    pub(crate) fn ensure_presence(
        attachments: &mut Vec<NoteAttachment>,
        target_id: AccountId,
    ) -> Result<(), NetworkAccountTargetError> {
        if !Self::validate_target(attachments, target_id)? {
            let target = Self::new(target_id, NoteExecutionHint::Always)?;
            attachments.push(NoteAttachment::from(target));
        }

        Ok(())
    }

    /// Behaves like [`Self::ensure_presence`], except that a non-public `target_id` is accepted
    /// without appending a target.
    ///
    /// A private account is never a network account, so it has no routing target to derive. This
    /// lets a note whose target may be either kind of account carry the target exactly when it is
    /// meaningful, while a caller-supplied target for another account is rejected either way.
    ///
    /// # Errors
    ///
    /// Returns an error if an attachment with the [`NetworkAccountTarget::ATTACHMENT_SCHEME`] does
    /// not decode as a [`NetworkAccountTarget`] or targets an account other than `target_id`.
    pub(crate) fn ensure_presence_if_public(
        attachments: &mut Vec<NoteAttachment>,
        target_id: AccountId,
    ) -> Result<(), NetworkAccountTargetError> {
        if target_id.is_public() {
            return Self::ensure_presence(attachments, target_id);
        }

        // No target is derived, but any attachment the caller supplied under the scheme is still
        // validated against `target_id`.
        Self::validate_target(attachments, target_id).map(|_| ())
    }

    /// Validates every attachment carrying the [`NetworkAccountTarget::ATTACHMENT_SCHEME`]
    /// against `target_id`, returning whether one of them is present.
    ///
    /// # Errors
    ///
    /// Returns an error if such an attachment does not decode as a [`NetworkAccountTarget`], which
    /// is the case for one naming a non-public account, or targets an account other than
    /// `target_id`.
    fn validate_target(
        attachments: &[NoteAttachment],
        target_id: AccountId,
    ) -> Result<bool, NetworkAccountTargetError> {
        let mut is_present = false;
        for attachment in attachments
            .iter()
            .filter(|attachment| attachment.attachment_scheme() == Self::ATTACHMENT_SCHEME)
        {
            let attached_target_id = Self::try_from(attachment)?.target_id();
            if attached_target_id != target_id {
                return Err(NetworkAccountTargetError::TargetMismatch {
                    expected: target_id,
                    actual: attached_target_id,
                });
            }

            is_present = true;
        }

        Ok(is_present)
    }

    // ACCESSORS
    // --------------------------------------------------------------------------------------------

    /// Returns the [`AccountId`] at which the note is targeted.
    pub fn target_id(&self) -> AccountId {
        self.target_id
    }

    /// Returns the [`NoteExecutionHint`] of the note.
    pub fn execution_hint(&self) -> NoteExecutionHint {
        self.exec_hint
    }
}

impl From<NetworkAccountTarget> for NoteAttachment {
    fn from(network_attachment: NetworkAccountTarget) -> Self {
        let mut word = Word::empty();
        word[0] = network_attachment.target_id.suffix();
        word[1] = network_attachment.target_id.prefix().as_felt();
        word[2] = network_attachment.exec_hint.into();

        NoteAttachment::with_word(NetworkAccountTarget::ATTACHMENT_SCHEME, word)
    }
}

impl TryFrom<&NoteAttachments> for NetworkAccountTarget {
    type Error = NetworkAccountTargetError;

    fn try_from(attachments: &NoteAttachments) -> Result<Self, Self::Error> {
        // Find the first matching attachment. In case of multiple network account target
        // attachments, we pick the first one as the canonical one.
        let attachment = attachments
            .find(NetworkAccountTarget::ATTACHMENT_SCHEME)
            .ok_or_else(|| NetworkAccountTargetError::MissingAttachmentScheme)?;

        Self::try_from(attachment)
    }
}
impl TryFrom<&NoteAttachment> for NetworkAccountTarget {
    type Error = NetworkAccountTargetError;

    fn try_from(attachment: &NoteAttachment) -> Result<Self, Self::Error> {
        if attachment.attachment_scheme() != Self::ATTACHMENT_SCHEME {
            return Err(NetworkAccountTargetError::AttachmentSchemeMismatch(
                attachment.attachment_scheme(),
            ));
        }

        let words = attachment.content().as_words();
        if words.len() != 1 {
            return Err(NetworkAccountTargetError::AttachmentContentNumWordsMismatch(
                attachment.content().num_words(),
            ));
        }
        let word = words[0];

        let id_suffix = word[0];
        let id_prefix = word[1];
        let exec_hint = word[2];

        let target_id = AccountId::try_from_elements(id_suffix, id_prefix)
            .map_err(NetworkAccountTargetError::DecodeTargetId)?;

        NetworkAccountTarget::new(target_id, NoteExecutionHint::from(exec_hint))
    }
}

// NETWORK ACCOUNT TARGET ERROR
// ================================================================================================

#[derive(Debug, thiserror::Error)]
pub enum NetworkAccountTargetError {
    #[error("note attachments do not contain a network account target scheme")]
    MissingAttachmentScheme,
    #[error("target account ID must have public account type")]
    TargetNotPublic(AccountId),
    #[error("attached network account target {actual} does not match expected target {expected}")]
    TargetMismatch { expected: AccountId, actual: AccountId },
    #[error(
        "attachment scheme {0} did not match expected type {expected}",
        expected = NetworkAccountTarget::ATTACHMENT_SCHEME
    )]
    AttachmentSchemeMismatch(NoteAttachmentScheme),
    #[error("network account target expects attachment content with one word, got {0}")]
    AttachmentContentNumWordsMismatch(u16),
    #[error("failed to decode target account ID")]
    DecodeTargetId(#[source] AccountIdError),
    #[error("network note must be public, but was {0:?}")]
    NoteNotPublic(NoteType),
}

// TESTS
// ================================================================================================

#[cfg(test)]
mod tests {
    use alloc::vec;

    use assert_matches::assert_matches;
    use miden_protocol::Felt;
    use miden_protocol::account::AccountType;
    use miden_protocol::testing::account_id::AccountIdBuilder;

    use super::*;

    fn public_account_id() -> AccountId {
        AccountIdBuilder::new()
            .account_type(AccountType::Public)
            .build_with_rng(&mut rand::rng())
    }

    #[test]
    fn network_account_target_serde() -> anyhow::Result<()> {
        let id = public_account_id();
        let network_account_target = NetworkAccountTarget::new(id, NoteExecutionHint::Always)?;
        assert_eq!(
            network_account_target,
            NetworkAccountTarget::try_from(&NoteAttachment::from(network_account_target))?
        );

        Ok(())
    }

    /// An execution hint encoding this version does not recognize must not hide the target
    /// account, since the on-chain check discards the hint felt entirely.
    #[test]
    fn unrecognized_execution_hint_preserves_target_id() -> anyhow::Result<()> {
        let target_id = public_account_id();

        // Tag 7 is above the highest known tag, and a non-zero payload on the `Always` tag is
        // rejected by `NoteExecutionHint::from_parts`.
        for raw_hint in [7u64, (1 << 8) | 1] {
            let raw_hint = Felt::new(raw_hint)?;
            let mut word = Word::empty();
            word[0] = target_id.suffix();
            word[1] = target_id.prefix().as_felt();
            word[2] = raw_hint;
            let attachment =
                NoteAttachment::with_word(NetworkAccountTarget::ATTACHMENT_SCHEME, word);

            let target = NetworkAccountTarget::try_from(&attachment)?;
            assert_eq!(target.target_id(), target_id);
            assert_eq!(target.execution_hint(), NoteExecutionHint::Unknown(raw_hint));
            // Re-encoding is lossless, so the note commitment is unaffected.
            assert_eq!(NoteAttachment::from(target), attachment);
        }

        Ok(())
    }

    /// A caller-supplied target for the same account is kept as-is, so its execution hint survives
    /// and no duplicate attachment is added.
    #[test]
    fn ensure_presence_keeps_matching_target() -> anyhow::Result<()> {
        let target_id = public_account_id();
        let supplied = NetworkAccountTarget::new(target_id, NoteExecutionHint::None)?;
        let mut attachments = vec![NoteAttachment::from(supplied)];

        NetworkAccountTarget::ensure_presence(&mut attachments, target_id)?;

        assert_eq!(attachments.len(), 1);
        assert_eq!(NetworkAccountTarget::try_from(&attachments[0])?, supplied);

        Ok(())
    }

    /// A caller-supplied target for another account is rejected instead of being silently
    /// shadowed by the note's own target.
    #[test]
    fn ensure_presence_rejects_mismatched_target() -> anyhow::Result<()> {
        let target_id = public_account_id();
        let other_id = public_account_id();
        let supplied = NetworkAccountTarget::new(other_id, NoteExecutionHint::Always)?;
        let mut attachments = vec![NoteAttachment::from(supplied)];

        let err = NetworkAccountTarget::ensure_presence(&mut attachments, target_id).unwrap_err();

        assert_matches!(
            err,
            NetworkAccountTargetError::TargetMismatch { expected, actual }
                if expected == target_id && actual == other_id
        );

        Ok(())
    }

    /// The appended target is placed after the caller's attachments, leaving their order intact.
    #[test]
    fn ensure_presence_appends_missing_target() -> anyhow::Result<()> {
        let target_id = public_account_id();
        let unrelated =
            NoteAttachment::with_word(NoteAttachmentScheme::new(64)?, Word::from([7u32, 0, 0, 0]));
        let mut attachments = vec![unrelated.clone()];

        NetworkAccountTarget::ensure_presence(&mut attachments, target_id)?;

        assert_eq!(
            attachments,
            vec![
                unrelated,
                NoteAttachment::from(NetworkAccountTarget::new(
                    target_id,
                    NoteExecutionHint::Always
                )?)
            ]
        );

        Ok(())
    }

    /// A non-public target has no network routing target, so none is appended, but a
    /// caller-supplied target for another account is still rejected.
    #[test]
    fn ensure_presence_if_public_skips_private_target() -> anyhow::Result<()> {
        let private_id = AccountIdBuilder::new()
            .account_type(AccountType::Private)
            .build_with_rng(&mut rand::rng());
        let mut attachments = vec![];

        NetworkAccountTarget::ensure_presence_if_public(&mut attachments, private_id)?;
        assert!(attachments.is_empty());

        let other_id = public_account_id();
        let supplied = NetworkAccountTarget::new(other_id, NoteExecutionHint::Always)?;
        let mut attachments = vec![NoteAttachment::from(supplied)];

        let err = NetworkAccountTarget::ensure_presence_if_public(&mut attachments, private_id)
            .unwrap_err();

        assert_matches!(
            err,
            NetworkAccountTargetError::TargetMismatch { expected, actual }
                if expected == private_id && actual == other_id
        );

        Ok(())
    }

    #[test]
    fn network_account_target_fails_on_private_target_account() -> anyhow::Result<()> {
        let id = AccountIdBuilder::new()
            .account_type(AccountType::Private)
            .build_with_rng(&mut rand::rng());
        let err = NetworkAccountTarget::new(id, NoteExecutionHint::Always).unwrap_err();

        assert_matches!(
            err,
            NetworkAccountTargetError::TargetNotPublic(account_id) if account_id == id
        );

        Ok(())
    }
}