use alloc::collections::BTreeSet;
use miden_protocol::account::component::{SchemaType, StorageSlotSchema};
use miden_protocol::account::{
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()
.filter(|(_key, value)| **value != Word::empty())
.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,
}
#[cfg(test)]
mod tests {
use miden_protocol::account::{AccountBuilder, StorageSlotContent};
use miden_protocol::asset::FungibleAsset;
use super::*;
use crate::account::auth::network_account::AuthNetworkAccount;
use crate::account::fees::FeePolicyManager;
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 try_from_fails_when_all_entries_removed() {
let removed = NoteScriptRoot::from_array([5, 6, 7, 8]);
let map =
StorageMap::with_entries([(StorageMapKey::new(removed.as_word()), Word::empty())])
.expect("map entries should have unique keys");
let slot = StorageSlot::with_map(NetworkAccountNoteAllowlist::slot_name().clone(), map);
let storage = AccountStorage::new(vec![slot]).expect("storage should be valid");
let result = NetworkAccountNoteAllowlist::try_from(&storage);
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_components(
AuthNetworkAccount::new(
original_roots.clone(),
FeePolicyManager::mock(FungibleAsset::mock_issuer()),
)
.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 mut expected: BTreeSet<NoteScriptRoot> = original_roots.into_iter().collect();
expected.insert(crate::note::NetworkAccountConfigNote::script_root());
expected.insert(crate::note::FeeSponsorshipNote::script_root());
let actual: BTreeSet<NoteScriptRoot> =
allowlist.allowed_script_roots().iter().copied().collect();
assert_eq!(actual, expected);
}
}