use alloc::collections::BTreeSet;
use alloc::vec::Vec;
use miden_protocol::Word;
use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
use miden_protocol::account::component::{
AccountComponentMetadata,
FeltSchema,
SchemaType,
StorageSchema,
StorageSlotSchema,
};
use miden_protocol::account::{
AccountComponent,
AccountType,
StorageMap,
StorageMapKey,
StorageSlot,
StorageSlotName,
};
use miden_protocol::errors::AccountError;
use miden_protocol::utils::sync::LazyLock;
use crate::account::components::multisig_library;
static THRESHOLD_CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::multisig::threshold_config")
.expect("storage slot name should be valid")
});
static APPROVER_PUBKEYS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::multisig::approver_public_keys")
.expect("storage slot name should be valid")
});
static APPROVER_SCHEME_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::multisig::approver_schemes")
.expect("storage slot name should be valid")
});
static EXECUTED_TRANSACTIONS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::multisig::executed_transactions")
.expect("storage slot name should be valid")
});
static PROCEDURE_THRESHOLDS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::multisig::procedure_thresholds")
.expect("storage slot name should be valid")
});
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthMultisigConfig {
approvers: Vec<(PublicKeyCommitment, AuthScheme)>,
default_threshold: u32,
proc_thresholds: Vec<(Word, u32)>,
}
impl AuthMultisigConfig {
pub fn new(
approvers: Vec<(PublicKeyCommitment, AuthScheme)>,
default_threshold: u32,
) -> Result<Self, AccountError> {
if default_threshold == 0 {
return Err(AccountError::other("threshold must be at least 1"));
}
if default_threshold > approvers.len() as u32 {
return Err(AccountError::other(
"threshold cannot be greater than number of approvers",
));
}
let unique_approvers: BTreeSet<_> = approvers.iter().map(|(pk, _)| pk).collect();
if unique_approvers.len() != approvers.len() {
return Err(AccountError::other("duplicate approver public keys are not allowed"));
}
Ok(Self {
approvers,
default_threshold,
proc_thresholds: vec![],
})
}
pub fn with_proc_thresholds(
mut self,
proc_thresholds: Vec<(Word, u32)>,
) -> Result<Self, AccountError> {
for (_, threshold) in &proc_thresholds {
if *threshold == 0 {
return Err(AccountError::other("procedure threshold must be at least 1"));
}
if *threshold > self.approvers.len() as u32 {
return Err(AccountError::other(
"procedure threshold cannot be greater than number of approvers",
));
}
}
self.proc_thresholds = proc_thresholds;
Ok(self)
}
pub fn approvers(&self) -> &[(PublicKeyCommitment, AuthScheme)] {
&self.approvers
}
pub fn default_threshold(&self) -> u32 {
self.default_threshold
}
pub fn proc_thresholds(&self) -> &[(Word, u32)] {
&self.proc_thresholds
}
}
#[derive(Debug)]
pub struct AuthMultisig {
config: AuthMultisigConfig,
}
impl AuthMultisig {
pub const NAME: &'static str = "miden::standards::components::auth::multisig";
pub fn new(config: AuthMultisigConfig) -> Result<Self, AccountError> {
Ok(Self { config })
}
pub fn threshold_config_slot() -> &'static StorageSlotName {
&THRESHOLD_CONFIG_SLOT_NAME
}
pub fn approver_public_keys_slot() -> &'static StorageSlotName {
&APPROVER_PUBKEYS_SLOT_NAME
}
pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
&APPROVER_SCHEME_ID_SLOT_NAME
}
pub fn executed_transactions_slot() -> &'static StorageSlotName {
&EXECUTED_TRANSACTIONS_SLOT_NAME
}
pub fn procedure_thresholds_slot() -> &'static StorageSlotName {
&PROCEDURE_THRESHOLDS_SLOT_NAME
}
pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::threshold_config_slot().clone(),
StorageSlotSchema::value(
"Threshold configuration",
[
FeltSchema::u32("threshold"),
FeltSchema::u32("num_approvers"),
FeltSchema::new_void(),
FeltSchema::new_void(),
],
),
)
}
pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::approver_public_keys_slot().clone(),
StorageSlotSchema::map(
"Approver public keys",
SchemaType::u32(),
SchemaType::pub_key(),
),
)
}
pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::approver_scheme_ids_slot().clone(),
StorageSlotSchema::map(
"Approver scheme IDs",
SchemaType::u32(),
SchemaType::auth_scheme(),
),
)
}
pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::executed_transactions_slot().clone(),
StorageSlotSchema::map(
"Executed transactions",
SchemaType::native_word(),
SchemaType::native_word(),
),
)
}
pub fn procedure_thresholds_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::procedure_thresholds_slot().clone(),
StorageSlotSchema::map(
"Procedure thresholds",
SchemaType::native_word(),
SchemaType::u32(),
),
)
}
pub fn component_metadata() -> AccountComponentMetadata {
let storage_schema = StorageSchema::new([
Self::threshold_config_slot_schema(),
Self::approver_public_keys_slot_schema(),
Self::approver_auth_scheme_slot_schema(),
Self::executed_transactions_slot_schema(),
Self::procedure_thresholds_slot_schema(),
])
.expect("storage schema should be valid");
AccountComponentMetadata::new(Self::NAME, AccountType::all())
.with_description("Multisig authentication component using hybrid signature schemes")
.with_storage_schema(storage_schema)
}
}
impl From<AuthMultisig> for AccountComponent {
fn from(multisig: AuthMultisig) -> Self {
let mut storage_slots = Vec::with_capacity(5);
let num_approvers = multisig.config.approvers().len() as u32;
storage_slots.push(StorageSlot::with_value(
AuthMultisig::threshold_config_slot().clone(),
Word::from([multisig.config.default_threshold(), num_approvers, 0, 0]),
));
let map_entries =
multisig.config.approvers().iter().enumerate().map(|(i, (pub_key, _))| {
(StorageMapKey::from_index(i as u32), Word::from(*pub_key))
});
storage_slots.push(StorageSlot::with_map(
AuthMultisig::approver_public_keys_slot().clone(),
StorageMap::with_entries(map_entries).unwrap(),
));
let scheme_id_entries =
multisig.config.approvers().iter().enumerate().map(|(i, (_, auth_scheme))| {
(StorageMapKey::from_index(i as u32), Word::from([*auth_scheme as u32, 0, 0, 0]))
});
storage_slots.push(StorageSlot::with_map(
AuthMultisig::approver_scheme_ids_slot().clone(),
StorageMap::with_entries(scheme_id_entries).unwrap(),
));
let executed_transactions = StorageMap::default();
storage_slots.push(StorageSlot::with_map(
AuthMultisig::executed_transactions_slot().clone(),
executed_transactions,
));
let proc_threshold_roots = StorageMap::with_entries(
multisig.config.proc_thresholds().iter().map(|(proc_root, threshold)| {
(StorageMapKey::from_raw(*proc_root), Word::from([*threshold, 0, 0, 0]))
}),
)
.unwrap();
storage_slots.push(StorageSlot::with_map(
AuthMultisig::procedure_thresholds_slot().clone(),
proc_threshold_roots,
));
let metadata = AuthMultisig::component_metadata();
AccountComponent::new(multisig_library(), storage_slots, metadata).expect(
"Multisig auth component should satisfy the requirements of a valid account component",
)
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use miden_protocol::Word;
use miden_protocol::account::auth::AuthSecretKey;
use miden_protocol::account::{AccountBuilder, auth};
use super::*;
use crate::account::wallets::BasicWallet;
#[test]
fn test_multisig_component_setup() {
let sec_key_1 = AuthSecretKey::new_falcon512_poseidon2();
let sec_key_2 = AuthSecretKey::new_falcon512_poseidon2();
let sec_key_3 = AuthSecretKey::new_falcon512_poseidon2();
let approvers = vec![
(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
(sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
(sec_key_3.public_key().to_commitment(), sec_key_3.auth_scheme()),
];
let threshold = 2u32;
let multisig_component = AuthMultisig::new(
AuthMultisigConfig::new(approvers.clone(), threshold).expect("invalid multisig config"),
)
.expect("multisig component creation failed");
let account = AccountBuilder::new([0; 32])
.with_auth_component(multisig_component)
.with_component(BasicWallet)
.build()
.expect("account building failed");
let config_slot = account
.storage()
.get_item(AuthMultisig::threshold_config_slot())
.expect("config storage slot access failed");
assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
for (i, (expected_pub_key, _)) in approvers.iter().enumerate() {
let stored_pub_key = account
.storage()
.get_map_item(
AuthMultisig::approver_public_keys_slot(),
Word::from([i as u32, 0, 0, 0]),
)
.expect("approver public key storage map access failed");
assert_eq!(stored_pub_key, Word::from(*expected_pub_key));
}
for (i, (_, expected_auth_scheme)) in approvers.iter().enumerate() {
let stored_scheme_id = account
.storage()
.get_map_item(
AuthMultisig::approver_scheme_ids_slot(),
Word::from([i as u32, 0, 0, 0]),
)
.expect("approver scheme ID storage map access failed");
assert_eq!(stored_scheme_id, Word::from([*expected_auth_scheme as u32, 0, 0, 0]));
}
}
#[test]
fn test_multisig_component_minimum_threshold() {
let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment();
let approvers = vec![(pub_key, auth::AuthScheme::EcdsaK256Keccak)];
let threshold = 1u32;
let multisig_component = AuthMultisig::new(
AuthMultisigConfig::new(approvers.clone(), threshold).expect("invalid multisig config"),
)
.expect("multisig component creation failed");
let account = AccountBuilder::new([0; 32])
.with_auth_component(multisig_component)
.with_component(BasicWallet)
.build()
.expect("account building failed");
let config_slot = account
.storage()
.get_item(AuthMultisig::threshold_config_slot())
.expect("config storage slot access failed");
assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
let stored_pub_key = account
.storage()
.get_map_item(AuthMultisig::approver_public_keys_slot(), Word::from([0u32, 0, 0, 0]))
.expect("approver pub keys storage map access failed");
assert_eq!(stored_pub_key, Word::from(pub_key));
let stored_scheme_id = account
.storage()
.get_map_item(AuthMultisig::approver_scheme_ids_slot(), Word::from([0u32, 0, 0, 0]))
.expect("approver scheme IDs storage map access failed");
assert_eq!(
stored_scheme_id,
Word::from([auth::AuthScheme::EcdsaK256Keccak as u32, 0, 0, 0])
);
}
#[test]
fn test_multisig_component_error_cases() {
let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment();
let approvers = vec![(pub_key, auth::AuthScheme::EcdsaK256Keccak)];
let result = AuthMultisigConfig::new(approvers.clone(), 0);
assert!(result.unwrap_err().to_string().contains("threshold must be at least 1"));
let result = AuthMultisigConfig::new(approvers, 2);
assert!(
result
.unwrap_err()
.to_string()
.contains("threshold cannot be greater than number of approvers")
);
}
#[test]
fn test_multisig_component_duplicate_approvers() {
let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
let approvers = vec![
(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
(sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
];
let result = AuthMultisigConfig::new(approvers, 2);
assert!(
result
.unwrap_err()
.to_string()
.contains("duplicate approver public keys are not allowed")
);
}
}