miden_standards/note/
network_account_target.rs1use alloc::vec::Vec;
2
3use miden_protocol::Word;
4use miden_protocol::account::AccountId;
5use miden_protocol::errors::AccountIdError;
6use miden_protocol::note::{NoteAttachment, NoteAttachmentScheme, NoteAttachments, NoteType};
7
8use crate::note::{NoteExecutionHint, StandardNoteAttachment};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct NetworkAccountTarget {
25 target_id: AccountId,
26 exec_hint: NoteExecutionHint,
27}
28
29impl NetworkAccountTarget {
30 pub const ATTACHMENT_SCHEME: NoteAttachmentScheme =
35 StandardNoteAttachment::NetworkAccountTarget.attachment_scheme();
36
37 pub fn new(
48 target_id: AccountId,
49 exec_hint: NoteExecutionHint,
50 ) -> Result<Self, NetworkAccountTargetError> {
51 if !target_id.is_public() {
52 return Err(NetworkAccountTargetError::TargetNotPublic(target_id));
53 }
54
55 Ok(Self { target_id, exec_hint })
56 }
57
58 pub(crate) fn ensure_presence(
74 attachments: &mut Vec<NoteAttachment>,
75 target_id: AccountId,
76 ) -> Result<(), NetworkAccountTargetError> {
77 if !Self::validate_target(attachments, target_id)? {
78 let target = Self::new(target_id, NoteExecutionHint::Always)?;
79 attachments.push(NoteAttachment::from(target));
80 }
81
82 Ok(())
83 }
84
85 pub(crate) fn ensure_presence_if_public(
97 attachments: &mut Vec<NoteAttachment>,
98 target_id: AccountId,
99 ) -> Result<(), NetworkAccountTargetError> {
100 if target_id.is_public() {
101 return Self::ensure_presence(attachments, target_id);
102 }
103
104 Self::validate_target(attachments, target_id).map(|_| ())
107 }
108
109 fn validate_target(
118 attachments: &[NoteAttachment],
119 target_id: AccountId,
120 ) -> Result<bool, NetworkAccountTargetError> {
121 let mut is_present = false;
122 for attachment in attachments
123 .iter()
124 .filter(|attachment| attachment.attachment_scheme() == Self::ATTACHMENT_SCHEME)
125 {
126 let attached_target_id = Self::try_from(attachment)?.target_id();
127 if attached_target_id != target_id {
128 return Err(NetworkAccountTargetError::TargetMismatch {
129 expected: target_id,
130 actual: attached_target_id,
131 });
132 }
133
134 is_present = true;
135 }
136
137 Ok(is_present)
138 }
139
140 pub fn target_id(&self) -> AccountId {
145 self.target_id
146 }
147
148 pub fn execution_hint(&self) -> NoteExecutionHint {
150 self.exec_hint
151 }
152}
153
154impl From<NetworkAccountTarget> for NoteAttachment {
155 fn from(network_attachment: NetworkAccountTarget) -> Self {
156 let mut word = Word::empty();
157 word[0] = network_attachment.target_id.suffix();
158 word[1] = network_attachment.target_id.prefix().as_felt();
159 word[2] = network_attachment.exec_hint.into();
160
161 NoteAttachment::with_word(NetworkAccountTarget::ATTACHMENT_SCHEME, word)
162 }
163}
164
165impl TryFrom<&NoteAttachments> for NetworkAccountTarget {
166 type Error = NetworkAccountTargetError;
167
168 fn try_from(attachments: &NoteAttachments) -> Result<Self, Self::Error> {
169 let attachment = attachments
172 .find(NetworkAccountTarget::ATTACHMENT_SCHEME)
173 .ok_or_else(|| NetworkAccountTargetError::MissingAttachmentScheme)?;
174
175 Self::try_from(attachment)
176 }
177}
178impl TryFrom<&NoteAttachment> for NetworkAccountTarget {
179 type Error = NetworkAccountTargetError;
180
181 fn try_from(attachment: &NoteAttachment) -> Result<Self, Self::Error> {
182 if attachment.attachment_scheme() != Self::ATTACHMENT_SCHEME {
183 return Err(NetworkAccountTargetError::AttachmentSchemeMismatch(
184 attachment.attachment_scheme(),
185 ));
186 }
187
188 let words = attachment.content().as_words();
189 if words.len() != 1 {
190 return Err(NetworkAccountTargetError::AttachmentContentNumWordsMismatch(
191 attachment.content().num_words(),
192 ));
193 }
194 let word = words[0];
195
196 let id_suffix = word[0];
197 let id_prefix = word[1];
198 let exec_hint = word[2];
199
200 let target_id = AccountId::try_from_elements(id_suffix, id_prefix)
201 .map_err(NetworkAccountTargetError::DecodeTargetId)?;
202
203 NetworkAccountTarget::new(target_id, NoteExecutionHint::from(exec_hint))
204 }
205}
206
207#[derive(Debug, thiserror::Error)]
211pub enum NetworkAccountTargetError {
212 #[error("note attachments do not contain a network account target scheme")]
213 MissingAttachmentScheme,
214 #[error("target account ID must have public account type")]
215 TargetNotPublic(AccountId),
216 #[error("attached network account target {actual} does not match expected target {expected}")]
217 TargetMismatch { expected: AccountId, actual: AccountId },
218 #[error(
219 "attachment scheme {0} did not match expected type {expected}",
220 expected = NetworkAccountTarget::ATTACHMENT_SCHEME
221 )]
222 AttachmentSchemeMismatch(NoteAttachmentScheme),
223 #[error("network account target expects attachment content with one word, got {0}")]
224 AttachmentContentNumWordsMismatch(u16),
225 #[error("failed to decode target account ID")]
226 DecodeTargetId(#[source] AccountIdError),
227 #[error("network note must be public, but was {0:?}")]
228 NoteNotPublic(NoteType),
229}
230
231#[cfg(test)]
235mod tests {
236 use alloc::vec;
237
238 use assert_matches::assert_matches;
239 use miden_protocol::Felt;
240 use miden_protocol::account::AccountType;
241 use miden_protocol::testing::account_id::AccountIdBuilder;
242
243 use super::*;
244
245 fn public_account_id() -> AccountId {
246 AccountIdBuilder::new()
247 .account_type(AccountType::Public)
248 .build_with_rng(&mut rand::rng())
249 }
250
251 #[test]
252 fn network_account_target_serde() -> anyhow::Result<()> {
253 let id = public_account_id();
254 let network_account_target = NetworkAccountTarget::new(id, NoteExecutionHint::Always)?;
255 assert_eq!(
256 network_account_target,
257 NetworkAccountTarget::try_from(&NoteAttachment::from(network_account_target))?
258 );
259
260 Ok(())
261 }
262
263 #[test]
266 fn unrecognized_execution_hint_preserves_target_id() -> anyhow::Result<()> {
267 let target_id = public_account_id();
268
269 for raw_hint in [7u64, (1 << 8) | 1] {
272 let raw_hint = Felt::new(raw_hint)?;
273 let mut word = Word::empty();
274 word[0] = target_id.suffix();
275 word[1] = target_id.prefix().as_felt();
276 word[2] = raw_hint;
277 let attachment =
278 NoteAttachment::with_word(NetworkAccountTarget::ATTACHMENT_SCHEME, word);
279
280 let target = NetworkAccountTarget::try_from(&attachment)?;
281 assert_eq!(target.target_id(), target_id);
282 assert_eq!(target.execution_hint(), NoteExecutionHint::Unknown(raw_hint));
283 assert_eq!(NoteAttachment::from(target), attachment);
285 }
286
287 Ok(())
288 }
289
290 #[test]
293 fn ensure_presence_keeps_matching_target() -> anyhow::Result<()> {
294 let target_id = public_account_id();
295 let supplied = NetworkAccountTarget::new(target_id, NoteExecutionHint::None)?;
296 let mut attachments = vec![NoteAttachment::from(supplied)];
297
298 NetworkAccountTarget::ensure_presence(&mut attachments, target_id)?;
299
300 assert_eq!(attachments.len(), 1);
301 assert_eq!(NetworkAccountTarget::try_from(&attachments[0])?, supplied);
302
303 Ok(())
304 }
305
306 #[test]
309 fn ensure_presence_rejects_mismatched_target() -> anyhow::Result<()> {
310 let target_id = public_account_id();
311 let other_id = public_account_id();
312 let supplied = NetworkAccountTarget::new(other_id, NoteExecutionHint::Always)?;
313 let mut attachments = vec![NoteAttachment::from(supplied)];
314
315 let err = NetworkAccountTarget::ensure_presence(&mut attachments, target_id).unwrap_err();
316
317 assert_matches!(
318 err,
319 NetworkAccountTargetError::TargetMismatch { expected, actual }
320 if expected == target_id && actual == other_id
321 );
322
323 Ok(())
324 }
325
326 #[test]
328 fn ensure_presence_appends_missing_target() -> anyhow::Result<()> {
329 let target_id = public_account_id();
330 let unrelated =
331 NoteAttachment::with_word(NoteAttachmentScheme::new(64)?, Word::from([7u32, 0, 0, 0]));
332 let mut attachments = vec![unrelated.clone()];
333
334 NetworkAccountTarget::ensure_presence(&mut attachments, target_id)?;
335
336 assert_eq!(
337 attachments,
338 vec![
339 unrelated,
340 NoteAttachment::from(NetworkAccountTarget::new(
341 target_id,
342 NoteExecutionHint::Always
343 )?)
344 ]
345 );
346
347 Ok(())
348 }
349
350 #[test]
353 fn ensure_presence_if_public_skips_private_target() -> anyhow::Result<()> {
354 let private_id = AccountIdBuilder::new()
355 .account_type(AccountType::Private)
356 .build_with_rng(&mut rand::rng());
357 let mut attachments = vec![];
358
359 NetworkAccountTarget::ensure_presence_if_public(&mut attachments, private_id)?;
360 assert!(attachments.is_empty());
361
362 let other_id = public_account_id();
363 let supplied = NetworkAccountTarget::new(other_id, NoteExecutionHint::Always)?;
364 let mut attachments = vec![NoteAttachment::from(supplied)];
365
366 let err = NetworkAccountTarget::ensure_presence_if_public(&mut attachments, private_id)
367 .unwrap_err();
368
369 assert_matches!(
370 err,
371 NetworkAccountTargetError::TargetMismatch { expected, actual }
372 if expected == private_id && actual == other_id
373 );
374
375 Ok(())
376 }
377
378 #[test]
379 fn network_account_target_fails_on_private_target_account() -> anyhow::Result<()> {
380 let id = AccountIdBuilder::new()
381 .account_type(AccountType::Private)
382 .build_with_rng(&mut rand::rng());
383 let err = NetworkAccountTarget::new(id, NoteExecutionHint::Always).unwrap_err();
384
385 assert_matches!(
386 err,
387 NetworkAccountTargetError::TargetNotPublic(account_id) if account_id == id
388 );
389
390 Ok(())
391 }
392}