miden_standards/account/auth/network_account/
note_allowlist.rs1use alloc::collections::BTreeSet;
2
3use miden_protocol::account::component::{SchemaType, StorageSlotSchema};
4use miden_protocol::account::{
5 AccountId,
6 AccountStorage,
7 StorageMap,
8 StorageMapKey,
9 StorageSlot,
10 StorageSlotContent,
11 StorageSlotName,
12};
13use miden_protocol::note::NoteScriptRoot;
14use miden_protocol::utils::sync::LazyLock;
15use miden_protocol::{Felt, Word};
16
17static SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
21 StorageSlotName::new("miden::standards::auth::network_account::allowed_note_scripts")
22 .expect("storage slot name should be valid")
23});
24
25const ALLOWED_FLAG: Word = Word::new([Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO]);
30
31#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct NetworkAccountNoteAllowlist {
46 allowed_script_roots: BTreeSet<NoteScriptRoot>,
47}
48
49impl NetworkAccountNoteAllowlist {
50 pub fn new(
57 allowed_script_roots: BTreeSet<NoteScriptRoot>,
58 ) -> Result<Self, NetworkAccountNoteAllowlistError> {
59 if allowed_script_roots.is_empty() {
60 return Err(NetworkAccountNoteAllowlistError::EmptyAllowlist);
61 }
62
63 Ok(Self { allowed_script_roots })
64 }
65
66 pub fn slot_name() -> &'static StorageSlotName {
68 &SLOT_NAME
69 }
70
71 pub fn allowed_script_roots(&self) -> &BTreeSet<NoteScriptRoot> {
73 &self.allowed_script_roots
74 }
75
76 pub fn into_allowed_script_roots(self) -> BTreeSet<NoteScriptRoot> {
78 self.allowed_script_roots
79 }
80
81 pub fn slot_schema() -> (StorageSlotName, StorageSlotSchema) {
83 (
84 Self::slot_name().clone(),
85 StorageSlotSchema::map(
86 "Allowed input note script roots",
87 SchemaType::native_word(),
88 SchemaType::native_word(),
89 ),
90 )
91 }
92
93 pub fn into_storage_slot(self) -> StorageSlot {
96 let entries = self
97 .allowed_script_roots
98 .into_iter()
99 .map(|root| (StorageMapKey::new(root.as_word()), ALLOWED_FLAG));
100
101 let storage_map = StorageMap::with_entries(entries)
102 .expect("allowlist entries should produce a valid storage map");
103
104 StorageSlot::with_map(Self::slot_name().clone(), storage_map)
105 }
106}
107
108impl TryFrom<&AccountStorage> for NetworkAccountNoteAllowlist {
112 type Error = NetworkAccountNoteAllowlistError;
113
114 fn try_from(storage: &AccountStorage) -> Result<Self, Self::Error> {
122 let slot = storage
123 .get(Self::slot_name())
124 .ok_or(NetworkAccountNoteAllowlistError::SlotNotFound)?;
125
126 let StorageSlotContent::Map(map) = slot.content() else {
127 return Err(NetworkAccountNoteAllowlistError::UnexpectedSlotType);
128 };
129
130 let allowed_script_roots = map
131 .entries()
132 .map(|(key, _value)| NoteScriptRoot::from_raw(key.as_word()))
133 .collect();
134
135 Self::new(allowed_script_roots)
136 }
137}
138
139#[derive(Debug, thiserror::Error)]
145pub enum NetworkAccountNoteAllowlistError {
146 #[error(
147 "network account allowlist must contain at least one allowed note script root: an empty \
148 allowlist would prevent the account from consuming any notes"
149 )]
150 EmptyAllowlist,
151 #[error(
152 "network account allowlist storage slot {} not found in account storage",
153 NetworkAccountNoteAllowlist::slot_name()
154 )]
155 SlotNotFound,
156 #[error(
157 "network account allowlist storage slot {} must be a map",
158 NetworkAccountNoteAllowlist::slot_name()
159 )]
160 UnexpectedSlotType,
161 #[error("network account must have public account type, but account {0} does not")]
162 AccountNotPublic(AccountId),
163}
164
165#[cfg(test)]
169mod tests {
170 use miden_protocol::account::{AccountBuilder, StorageSlotContent};
171
172 use super::*;
173 use crate::account::auth::network_account::AuthNetworkAccount;
174 use crate::account::wallets::BasicWallet;
175
176 #[test]
177 fn allowlist_storage_slot_contains_expected_entries() {
178 let root_a = NoteScriptRoot::from_array([1, 2, 3, 4]);
179 let root_b = NoteScriptRoot::from_array([5, 6, 7, 8]);
180
181 let slot = NetworkAccountNoteAllowlist::new(BTreeSet::from_iter([root_a, root_b]))
182 .expect("non-empty allowlist should construct")
183 .into_storage_slot();
184
185 assert_eq!(slot.name(), NetworkAccountNoteAllowlist::slot_name());
186
187 let StorageSlotContent::Map(map) = slot.content() else {
188 panic!("allowlist slot must be a map");
189 };
190
191 assert_eq!(
192 map.get(&StorageMapKey::new(root_a.as_word())),
193 ALLOWED_FLAG,
194 "root_a should resolve to the flag value"
195 );
196 assert_eq!(
197 map.get(&StorageMapKey::new(root_b.as_word())),
198 ALLOWED_FLAG,
199 "root_b should resolve to the flag value"
200 );
201 }
202
203 #[test]
204 fn empty_allowlist_is_rejected() {
205 let result = NetworkAccountNoteAllowlist::new(BTreeSet::new());
206 assert!(matches!(result, Err(NetworkAccountNoteAllowlistError::EmptyAllowlist)));
207 }
208
209 #[test]
210 fn allowlist_round_trips_through_account_storage() {
211 use alloc::collections::BTreeSet;
212
213 let root_a = NoteScriptRoot::from_array([1, 2, 3, 4]);
214 let root_b = NoteScriptRoot::from_array([5, 6, 7, 8]);
215 let root_c = NoteScriptRoot::from_array([9, 10, 11, 12]);
216 let original_roots = BTreeSet::from_iter([root_a, root_b, root_c]);
217
218 let account = AccountBuilder::new([0; 32])
219 .with_auth_component(
220 AuthNetworkAccount::with_allowed_notes(original_roots.clone())
221 .expect("non-empty allowlist should construct"),
222 )
223 .with_component(BasicWallet)
224 .build()
225 .expect("account building with AuthNetworkAccount failed");
226
227 let allowlist = NetworkAccountNoteAllowlist::try_from(account.storage())
228 .expect("allowlist should be reconstructable from account storage");
229
230 let expected: BTreeSet<NoteScriptRoot> = original_roots.into_iter().collect();
232 let actual: BTreeSet<NoteScriptRoot> =
233 allowlist.allowed_script_roots().iter().copied().collect();
234
235 assert_eq!(actual, expected);
236 }
237}