use alloc::collections::BTreeSet;
use miden_protocol::account::component::{SchemaType, StorageSlotSchema};
use miden_protocol::account::{
AccountId,
AccountStorage,
StorageMap,
StorageMapKey,
StorageSlot,
StorageSlotContent,
StorageSlotName,
};
use miden_protocol::note::NoteScriptRoot;
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::{Felt, Word};
static SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::network_account::allowed_note_scripts")
.expect("storage slot name should be valid")
});
const ALLOWED_FLAG: Word = Word::new([Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO]);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NetworkAccountNoteAllowlist {
allowed_script_roots: BTreeSet<NoteScriptRoot>,
}
impl NetworkAccountNoteAllowlist {
pub fn new(
allowed_script_roots: BTreeSet<NoteScriptRoot>,
) -> Result<Self, NetworkAccountNoteAllowlistError> {
if allowed_script_roots.is_empty() {
return Err(NetworkAccountNoteAllowlistError::EmptyAllowlist);
}
Ok(Self { allowed_script_roots })
}
pub fn slot_name() -> &'static StorageSlotName {
&SLOT_NAME
}
pub fn allowed_script_roots(&self) -> &BTreeSet<NoteScriptRoot> {
&self.allowed_script_roots
}
pub fn into_allowed_script_roots(self) -> BTreeSet<NoteScriptRoot> {
self.allowed_script_roots
}
pub fn slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::slot_name().clone(),
StorageSlotSchema::map(
"Allowed input note script roots",
SchemaType::native_word(),
SchemaType::native_word(),
),
)
}
pub fn into_storage_slot(self) -> StorageSlot {
let entries = self
.allowed_script_roots
.into_iter()
.map(|root| (StorageMapKey::new(root.as_word()), ALLOWED_FLAG));
let storage_map = StorageMap::with_entries(entries)
.expect("allowlist entries should produce a valid storage map");
StorageSlot::with_map(Self::slot_name().clone(), storage_map)
}
}
impl TryFrom<&AccountStorage> for NetworkAccountNoteAllowlist {
type Error = NetworkAccountNoteAllowlistError;
fn try_from(storage: &AccountStorage) -> Result<Self, Self::Error> {
let slot = storage
.get(Self::slot_name())
.ok_or(NetworkAccountNoteAllowlistError::SlotNotFound)?;
let StorageSlotContent::Map(map) = slot.content() else {
return Err(NetworkAccountNoteAllowlistError::UnexpectedSlotType);
};
let allowed_script_roots = map
.entries()
.map(|(key, _value)| NoteScriptRoot::from_raw(key.as_word()))
.collect();
Self::new(allowed_script_roots)
}
}
#[derive(Debug, thiserror::Error)]
pub enum NetworkAccountNoteAllowlistError {
#[error(
"network account allowlist must contain at least one allowed note script root: an empty \
allowlist would prevent the account from consuming any notes"
)]
EmptyAllowlist,
#[error(
"network account allowlist storage slot {} not found in account storage",
NetworkAccountNoteAllowlist::slot_name()
)]
SlotNotFound,
#[error(
"network account allowlist storage slot {} must be a map",
NetworkAccountNoteAllowlist::slot_name()
)]
UnexpectedSlotType,
#[error("network account must have public account type, but account {0} does not")]
AccountNotPublic(AccountId),
}
#[cfg(test)]
mod tests {
use miden_protocol::account::{AccountBuilder, StorageSlotContent};
use super::*;
use crate::account::auth::network_account::AuthNetworkAccount;
use crate::account::wallets::BasicWallet;
#[test]
fn allowlist_storage_slot_contains_expected_entries() {
let root_a = NoteScriptRoot::from_array([1, 2, 3, 4]);
let root_b = NoteScriptRoot::from_array([5, 6, 7, 8]);
let slot = NetworkAccountNoteAllowlist::new(BTreeSet::from_iter([root_a, root_b]))
.expect("non-empty allowlist should construct")
.into_storage_slot();
assert_eq!(slot.name(), NetworkAccountNoteAllowlist::slot_name());
let StorageSlotContent::Map(map) = slot.content() else {
panic!("allowlist slot must be a map");
};
assert_eq!(
map.get(&StorageMapKey::new(root_a.as_word())),
ALLOWED_FLAG,
"root_a should resolve to the flag value"
);
assert_eq!(
map.get(&StorageMapKey::new(root_b.as_word())),
ALLOWED_FLAG,
"root_b should resolve to the flag value"
);
}
#[test]
fn empty_allowlist_is_rejected() {
let result = NetworkAccountNoteAllowlist::new(BTreeSet::new());
assert!(matches!(result, Err(NetworkAccountNoteAllowlistError::EmptyAllowlist)));
}
#[test]
fn allowlist_round_trips_through_account_storage() {
use alloc::collections::BTreeSet;
let root_a = NoteScriptRoot::from_array([1, 2, 3, 4]);
let root_b = NoteScriptRoot::from_array([5, 6, 7, 8]);
let root_c = NoteScriptRoot::from_array([9, 10, 11, 12]);
let original_roots = BTreeSet::from_iter([root_a, root_b, root_c]);
let account = AccountBuilder::new([0; 32])
.with_auth_component(
AuthNetworkAccount::with_allowlist(original_roots.clone())
.expect("non-empty allowlist should construct"),
)
.with_component(BasicWallet)
.build()
.expect("account building with AuthNetworkAccount failed");
let allowlist = NetworkAccountNoteAllowlist::try_from(account.storage())
.expect("allowlist should be reconstructable from account storage");
let expected: BTreeSet<NoteScriptRoot> = original_roots.into_iter().collect();
let actual: BTreeSet<NoteScriptRoot> =
allowlist.allowed_script_roots().iter().copied().collect();
assert_eq!(actual, expected);
}
}