use alloc::collections::BTreeSet;
use alloc::vec::Vec;
use miden_objects::account::auth::PublicKeyCommitment;
use miden_objects::account::{AccountComponent, StorageMap, StorageSlot};
use miden_objects::{AccountError, Word};
use crate::account::components::rpo_falcon_512_multisig_library;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthRpoFalcon512MultisigConfig {
approvers: Vec<PublicKeyCommitment>,
default_threshold: u32,
proc_thresholds: Vec<(Word, u32)>,
}
impl AuthRpoFalcon512MultisigConfig {
pub fn new(
approvers: Vec<PublicKeyCommitment>,
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",
));
}
if approvers.len() != approvers.iter().collect::<BTreeSet<_>>().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] {
&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 AuthRpoFalcon512Multisig {
config: AuthRpoFalcon512MultisigConfig,
}
impl AuthRpoFalcon512Multisig {
pub fn new(config: AuthRpoFalcon512MultisigConfig) -> Result<Self, AccountError> {
Ok(Self { config })
}
}
impl From<AuthRpoFalcon512Multisig> for AccountComponent {
fn from(multisig: AuthRpoFalcon512Multisig) -> Self {
let mut storage_slots = Vec::with_capacity(3);
let num_approvers = multisig.config.approvers().len() as u32;
storage_slots.push(StorageSlot::Value(Word::from([
multisig.config.default_threshold(),
num_approvers,
0,
0,
])));
let map_entries = multisig
.config
.approvers()
.iter()
.enumerate()
.map(|(i, pub_key)| (Word::from([i as u32, 0, 0, 0]), (*pub_key).into()));
storage_slots.push(StorageSlot::Map(StorageMap::with_entries(map_entries).unwrap()));
let executed_transactions = StorageMap::default();
storage_slots.push(StorageSlot::Map(executed_transactions));
let proc_threshold_roots = StorageMap::with_entries(
multisig
.config
.proc_thresholds()
.iter()
.map(|(proc_root, threshold)| (*proc_root, Word::from([*threshold, 0, 0, 0]))),
)
.unwrap();
storage_slots.push(StorageSlot::Map(proc_threshold_roots));
AccountComponent::new(rpo_falcon_512_multisig_library(), storage_slots)
.expect("Multisig auth component should satisfy the requirements of a valid account component")
.with_supports_all_types()
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use miden_objects::Word;
use miden_objects::account::AccountBuilder;
use super::*;
use crate::account::wallets::BasicWallet;
#[test]
fn test_multisig_component_setup() {
let pub_key_1 = PublicKeyCommitment::from(Word::from([1u32, 0, 0, 0]));
let pub_key_2 = PublicKeyCommitment::from(Word::from([2u32, 0, 0, 0]));
let pub_key_3 = PublicKeyCommitment::from(Word::from([3u32, 0, 0, 0]));
let approvers = vec![pub_key_1, pub_key_2, pub_key_3];
let threshold = 2u32;
let multisig_component = AuthRpoFalcon512Multisig::new(
AuthRpoFalcon512MultisigConfig::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 threshold_slot = account.storage().get_item(0).expect("storage slot 0 access failed");
assert_eq!(threshold_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(1, Word::from([i as u32, 0, 0, 0]))
.expect("storage map access failed");
assert_eq!(stored_pub_key, Word::from(*expected_pub_key));
}
}
#[test]
fn test_multisig_component_minimum_threshold() {
let pub_key = PublicKeyCommitment::from(Word::from([42u32, 0, 0, 0]));
let approvers = vec![pub_key];
let threshold = 1u32;
let multisig_component = AuthRpoFalcon512Multisig::new(
AuthRpoFalcon512MultisigConfig::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 threshold_slot = account.storage().get_item(0).expect("storage slot 0 access failed");
assert_eq!(threshold_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
let stored_pub_key = account
.storage()
.get_map_item(1, Word::from([0u32, 0, 0, 0]))
.expect("storage map access failed");
assert_eq!(stored_pub_key, Word::from(pub_key));
}
#[test]
fn test_multisig_component_error_cases() {
let pub_key = PublicKeyCommitment::from(Word::from([1u32, 0, 0, 0]));
let approvers = vec![pub_key];
let result = AuthRpoFalcon512MultisigConfig::new(approvers.clone(), 0);
assert!(result.unwrap_err().to_string().contains("threshold must be at least 1"));
let result = AuthRpoFalcon512MultisigConfig::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 pub_key_1 = PublicKeyCommitment::from(Word::from([1u32, 0, 0, 0]));
let pub_key_2 = PublicKeyCommitment::from(Word::from([2u32, 0, 0, 0]));
let approvers = vec![pub_key_1, pub_key_2, pub_key_1];
let result = AuthRpoFalcon512MultisigConfig::new(approvers, 2);
assert!(
result
.unwrap_err()
.to_string()
.contains("duplicate approver public keys are not allowed")
);
}
}