use alloc::collections::BTreeSet;
use alloc::vec::Vec;
use miden_protocol::Word;
use miden_protocol::account::component::{
AccountComponentCode,
AccountComponentMetadata,
SchemaType,
StorageSchema,
StorageSlotSchema,
};
use miden_protocol::account::{
AccountCode,
AccountComponent,
AccountComponentName,
AccountProcedureRoot,
StorageMap,
StorageMapKey,
StorageSlot,
StorageSlotName,
};
use miden_protocol::errors::AccountError;
use miden_protocol::utils::sync::LazyLock;
use super::Approver;
use crate::account::account_component_code;
account_component_code!(SINGLESIG_ACL_CODE, "miden-standards-auth-singlesig-acl.masp");
static PUBKEY_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::singlesig_acl::pub_key")
.expect("storage slot name should be valid")
});
static SCHEME_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::singlesig_acl::scheme")
.expect("storage slot name should be valid")
});
static EXEMPT_PROCEDURE_ROOTS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::singlesig_acl::exempt_procedure_roots")
.expect("storage slot name should be valid")
});
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct AuthSingleSigAclConfig {
exempt_procedures: BTreeSet<AccountProcedureRoot>,
}
impl AuthSingleSigAclConfig {
pub fn new(exempt_procedures: BTreeSet<AccountProcedureRoot>) -> Result<Self, AccountError> {
if exempt_procedures.len() > AccountCode::MAX_NUM_PROCEDURES {
return Err(AccountError::other(format!(
"The number of procedures in the exempt procedures set provided exceeds the maximum limit of {}",
AccountCode::MAX_NUM_PROCEDURES
)));
}
Ok(Self { exempt_procedures })
}
}
pub struct AuthSingleSigAcl {
approver: Approver,
config: AuthSingleSigAclConfig,
}
impl AuthSingleSigAcl {
pub const NAME: &'static str = "miden::standards::components::auth::singlesig_acl";
pub const fn name() -> AccountComponentName {
AccountComponentName::from_static_str(Self::NAME)
}
pub fn code() -> &'static AccountComponentCode {
&SINGLESIG_ACL_CODE
}
pub fn new(approver: Approver, config: AuthSingleSigAclConfig) -> Self {
Self { approver, config }
}
pub fn public_key_slot() -> &'static StorageSlotName {
&PUBKEY_SLOT_NAME
}
pub fn scheme_id_slot() -> &'static StorageSlotName {
&SCHEME_ID_SLOT_NAME
}
pub fn exempt_procedure_roots_slot() -> &'static StorageSlotName {
&EXEMPT_PROCEDURE_ROOTS_SLOT_NAME
}
pub fn public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::public_key_slot().clone(),
StorageSlotSchema::value("Public key commitment", SchemaType::pub_key()),
)
}
pub fn auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::scheme_id_slot().clone(),
StorageSlotSchema::value("Scheme ID", SchemaType::auth_scheme()),
)
}
pub fn exempt_procedure_roots_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::exempt_procedure_roots_slot().clone(),
StorageSlotSchema::map(
"Exempt procedure roots",
SchemaType::native_word(),
SchemaType::bool(),
),
)
}
pub fn component_metadata() -> AccountComponentMetadata {
let storage_schema = StorageSchema::new(vec![
Self::public_key_slot_schema(),
Self::auth_scheme_slot_schema(),
Self::exempt_procedure_roots_slot_schema(),
])
.expect("storage schema should be valid");
AccountComponentMetadata::new(Self::NAME)
.with_description(
"Authentication component with exempt-list ACL using ECDSA K256 Keccak or Falcon512 Poseidon2 signature scheme",
)
.with_storage_schema(storage_schema)
}
}
impl From<AuthSingleSigAcl> for AccountComponent {
fn from(singlesig_acl: AuthSingleSigAcl) -> Self {
let mut storage_slots = Vec::with_capacity(3);
storage_slots.push(StorageSlot::with_value(
AuthSingleSigAcl::public_key_slot().clone(),
singlesig_acl.approver.pub_key().into(),
));
storage_slots.push(StorageSlot::with_value(
AuthSingleSigAcl::scheme_id_slot().clone(),
Word::from([singlesig_acl.approver.auth_scheme().as_u8(), 0, 0, 0]),
));
let map_entries = singlesig_acl.config.exempt_procedures.iter().map(|proc_root| {
(StorageMapKey::from_raw(proc_root.as_word()), Word::from([1u32, 0, 0, 0]))
});
storage_slots.push(StorageSlot::with_map(
AuthSingleSigAcl::exempt_procedure_roots_slot().clone(),
StorageMap::with_entries(map_entries).unwrap(),
));
let metadata = AuthSingleSigAcl::component_metadata();
AccountComponent::new(AuthSingleSigAcl::code().clone(), storage_slots, metadata).expect(
"singlesig ACL component should satisfy the requirements of a valid account component",
)
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use miden_protocol::Word;
use miden_protocol::account::AccountBuilder;
use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
use super::*;
use crate::account::components::StandardAccountComponent;
use crate::account::wallets::BasicWallet;
fn get_basic_wallet_procedures() -> BTreeSet<AccountProcedureRoot> {
let procedures: BTreeSet<AccountProcedureRoot> =
StandardAccountComponent::BasicWallet.procedure_roots().collect();
assert_eq!(procedures.len(), 3);
procedures
}
fn build_account(
exempt_procedures: BTreeSet<AccountProcedureRoot>,
) -> Result<(PublicKeyCommitment, miden_protocol::account::Account)> {
let public_key = PublicKeyCommitment::from(Word::empty());
let auth_scheme = AuthScheme::Falcon512Poseidon2;
let acl_config = AuthSingleSigAclConfig::new(exempt_procedures)?;
let component = AuthSingleSigAcl::new(Approver::new(public_key, auth_scheme), acl_config);
let account = AccountBuilder::new([0; 32])
.with_auth_component(component)
.with_component(BasicWallet)
.build()
.expect("account building failed");
Ok((public_key, account))
}
#[test]
fn test_singlesig_acl_empty_exempt_list() -> Result<()> {
let (public_key, account) = build_account(BTreeSet::new())?;
let public_key_slot = account
.storage()
.get_item(AuthSingleSigAcl::public_key_slot())
.expect("public key storage slot access failed");
assert_eq!(public_key_slot, public_key.into());
let probe = account
.storage()
.get_map_item(AuthSingleSigAcl::exempt_procedure_roots_slot(), StorageMapKey::empty())
.expect("storage map access failed");
assert_eq!(probe, Word::empty());
Ok(())
}
#[test]
fn test_singlesig_acl_with_exempt_procedures() -> Result<()> {
let procedures = get_basic_wallet_procedures();
let (_public_key, account) = build_account(procedures.clone())?;
let marker = Word::from([1u32, 0, 0, 0]);
for proc_root in &procedures {
let value = account
.storage()
.get_map_item(
AuthSingleSigAcl::exempt_procedure_roots_slot(),
StorageMapKey::from_raw(proc_root.as_word()),
)
.expect("storage map access failed");
assert_eq!(value, marker);
}
let probe = account
.storage()
.get_map_item(
AuthSingleSigAcl::exempt_procedure_roots_slot(),
StorageMapKey::from_index(42u32),
)
.expect("storage map access failed");
assert_eq!(probe, Word::empty());
Ok(())
}
#[test]
fn test_singlesig_acl_rejects_exempt_list_above_account_limit() -> Result<()> {
let too_many: BTreeSet<AccountProcedureRoot> = (0..=AccountCode::MAX_NUM_PROCEDURES as u32)
.map(|i| AccountProcedureRoot::from_raw(Word::from([i, 0, 0, 0])))
.collect();
let result = AuthSingleSigAclConfig::new(too_many);
assert!(result.is_err(), "exempt list above MAX_NUM_PROCEDURES should be rejected");
Ok(())
}
}