use alloc::vec::Vec;
use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
use miden_protocol::account::component::{
AccountComponentMetadata,
FeltSchema,
SchemaType,
StorageSchema,
StorageSlotSchema,
};
use miden_protocol::account::{
AccountCode,
AccountComponent,
AccountType,
StorageMap,
StorageMapKey,
StorageSlot,
StorageSlotName,
};
use miden_protocol::errors::AccountError;
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::{Felt, Word};
use crate::account::components::singlesig_acl_library;
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 CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::singlesig_acl::config")
.expect("storage slot name should be valid")
});
static TRIGGER_PROCEDURE_ROOT_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::singlesig_acl::trigger_procedure_roots")
.expect("storage slot name should be valid")
});
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthSingleSigAclConfig {
pub auth_trigger_procedures: Vec<Word>,
pub allow_unauthorized_output_notes: bool,
pub allow_unauthorized_input_notes: bool,
}
impl AuthSingleSigAclConfig {
pub fn new() -> Self {
Self {
auth_trigger_procedures: vec![],
allow_unauthorized_output_notes: false,
allow_unauthorized_input_notes: false,
}
}
pub fn with_auth_trigger_procedures(mut self, procedures: Vec<Word>) -> Self {
self.auth_trigger_procedures = procedures;
self
}
pub fn with_allow_unauthorized_output_notes(mut self, allow: bool) -> Self {
self.allow_unauthorized_output_notes = allow;
self
}
pub fn with_allow_unauthorized_input_notes(mut self, allow: bool) -> Self {
self.allow_unauthorized_input_notes = allow;
self
}
}
impl Default for AuthSingleSigAclConfig {
fn default() -> Self {
Self::new()
}
}
pub struct AuthSingleSigAcl {
pub_key: PublicKeyCommitment,
auth_scheme: AuthScheme,
config: AuthSingleSigAclConfig,
}
impl AuthSingleSigAcl {
pub const NAME: &'static str = "miden::standards::components::auth::singlesig_acl";
pub fn new(
pub_key: PublicKeyCommitment,
auth_scheme: AuthScheme,
config: AuthSingleSigAclConfig,
) -> Result<Self, AccountError> {
let max_procedures = AccountCode::MAX_NUM_PROCEDURES;
if config.auth_trigger_procedures.len() > max_procedures {
return Err(AccountError::other(format!(
"Cannot track more than {max_procedures} procedures (account limit)"
)));
}
Ok(Self { pub_key, auth_scheme, config })
}
pub fn public_key_slot() -> &'static StorageSlotName {
&PUBKEY_SLOT_NAME
}
pub fn scheme_id_slot() -> &'static StorageSlotName {
&SCHEME_ID_SLOT_NAME
}
pub fn config_slot() -> &'static StorageSlotName {
&CONFIG_SLOT_NAME
}
pub fn trigger_procedure_roots_slot() -> &'static StorageSlotName {
&TRIGGER_PROCEDURE_ROOT_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 config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::config_slot().clone(),
StorageSlotSchema::value(
"ACL configuration",
[
FeltSchema::u32("num_trigger_procs").with_default(Felt::new(0)),
FeltSchema::bool("allow_unauthorized_output_notes").with_default(Felt::new(0)),
FeltSchema::bool("allow_unauthorized_input_notes").with_default(Felt::new(0)),
FeltSchema::new_void(),
],
),
)
}
pub fn auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::scheme_id_slot().clone(),
StorageSlotSchema::value("Scheme ID", SchemaType::auth_scheme()),
)
}
pub fn trigger_procedure_roots_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::trigger_procedure_roots_slot().clone(),
StorageSlotSchema::map(
"Trigger procedure roots",
SchemaType::u32(),
SchemaType::native_word(),
),
)
}
pub fn component_metadata() -> AccountComponentMetadata {
let storage_schema = StorageSchema::new(vec![
Self::public_key_slot_schema(),
Self::auth_scheme_slot_schema(),
Self::config_slot_schema(),
Self::trigger_procedure_roots_slot_schema(),
])
.expect("storage schema should be valid");
AccountComponentMetadata::new(Self::NAME, AccountType::all())
.with_description(
"Authentication component with procedure-based 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.pub_key.into(),
));
storage_slots.push(StorageSlot::with_value(
AuthSingleSigAcl::scheme_id_slot().clone(),
Word::from([singlesig_acl.auth_scheme.as_u8(), 0, 0, 0]),
));
let num_procs = singlesig_acl.config.auth_trigger_procedures.len() as u32;
storage_slots.push(StorageSlot::with_value(
AuthSingleSigAcl::config_slot().clone(),
Word::from([
num_procs,
u32::from(singlesig_acl.config.allow_unauthorized_output_notes),
u32::from(singlesig_acl.config.allow_unauthorized_input_notes),
0,
]),
));
let map_entries = singlesig_acl
.config
.auth_trigger_procedures
.iter()
.enumerate()
.map(|(i, proc_root)| (StorageMapKey::from_index(i as u32), *proc_root));
storage_slots.push(StorageSlot::with_map(
AuthSingleSigAcl::trigger_procedure_roots_slot().clone(),
StorageMap::with_entries(map_entries).unwrap(),
));
let metadata = AuthSingleSigAcl::component_metadata();
AccountComponent::new(singlesig_acl_library(), storage_slots, metadata).expect(
"singlesig ACL component should satisfy the requirements of a valid account component",
)
}
}
#[cfg(test)]
mod tests {
use miden_protocol::Word;
use miden_protocol::account::AccountBuilder;
use super::*;
use crate::account::components::StandardAccountComponent;
use crate::account::wallets::BasicWallet;
struct AclTestConfig {
with_procedures: bool,
allow_unauthorized_output_notes: bool,
allow_unauthorized_input_notes: bool,
expected_config_slot: Word,
}
fn get_basic_wallet_procedures() -> Vec<Word> {
let procedures: Vec<Word> =
StandardAccountComponent::BasicWallet.procedure_digests().collect();
assert_eq!(procedures.len(), 2);
procedures
}
fn test_acl_component(config: AclTestConfig) {
let public_key = PublicKeyCommitment::from(Word::empty());
let auth_scheme = AuthScheme::Falcon512Poseidon2;
let mut acl_config = AuthSingleSigAclConfig::new()
.with_allow_unauthorized_output_notes(config.allow_unauthorized_output_notes)
.with_allow_unauthorized_input_notes(config.allow_unauthorized_input_notes);
let auth_trigger_procedures = if config.with_procedures {
let procedures = get_basic_wallet_procedures();
acl_config = acl_config.with_auth_trigger_procedures(procedures.clone());
procedures
} else {
vec![]
};
let component = AuthSingleSigAcl::new(public_key, auth_scheme, acl_config)
.expect("component creation failed");
let account = AccountBuilder::new([0; 32])
.with_auth_component(component)
.with_component(BasicWallet)
.build()
.expect("account building failed");
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 config_slot = account
.storage()
.get_item(AuthSingleSigAcl::config_slot())
.expect("config storage slot access failed");
assert_eq!(config_slot, config.expected_config_slot);
if config.with_procedures {
for (i, expected_proc_root) in auth_trigger_procedures.iter().enumerate() {
let proc_root = account
.storage()
.get_map_item(
AuthSingleSigAcl::trigger_procedure_roots_slot(),
Word::from([i as u32, 0, 0, 0]),
)
.expect("storage map access failed");
assert_eq!(proc_root, *expected_proc_root);
}
} else {
let proc_root = account
.storage()
.get_map_item(AuthSingleSigAcl::trigger_procedure_roots_slot(), Word::empty())
.expect("storage map access failed");
assert_eq!(proc_root, Word::empty());
}
}
#[test]
fn test_singlesig_acl_no_procedures() {
test_acl_component(AclTestConfig {
with_procedures: false,
allow_unauthorized_output_notes: false,
allow_unauthorized_input_notes: false,
expected_config_slot: Word::empty(), });
}
#[test]
fn test_singlesig_acl_with_two_procedures() {
test_acl_component(AclTestConfig {
with_procedures: true,
allow_unauthorized_output_notes: false,
allow_unauthorized_input_notes: false,
expected_config_slot: Word::from([2u32, 0, 0, 0]),
});
}
#[test]
fn test_ecdsa_k256_keccak_acl_with_allow_unauthorized_output_notes() {
test_acl_component(AclTestConfig {
with_procedures: false,
allow_unauthorized_output_notes: true,
allow_unauthorized_input_notes: false,
expected_config_slot: Word::from([0u32, 1, 0, 0]),
});
}
#[test]
fn test_ecdsa_k256_keccak_acl_with_procedures_and_allow_unauthorized_output_notes() {
test_acl_component(AclTestConfig {
with_procedures: true,
allow_unauthorized_output_notes: true,
allow_unauthorized_input_notes: false,
expected_config_slot: Word::from([2u32, 1, 0, 0]),
});
}
#[test]
fn test_ecdsa_k256_keccak_acl_with_allow_unauthorized_input_notes() {
test_acl_component(AclTestConfig {
with_procedures: false,
allow_unauthorized_output_notes: false,
allow_unauthorized_input_notes: true,
expected_config_slot: Word::from([0u32, 0, 1, 0]),
});
}
#[test]
fn test_ecdsa_k256_keccak_acl_with_both_allow_flags() {
test_acl_component(AclTestConfig {
with_procedures: true,
allow_unauthorized_output_notes: true,
allow_unauthorized_input_notes: true,
expected_config_slot: Word::from([2u32, 1, 1, 0]),
});
}
}