miden_lib/account/auth/
ecdsa_k256_keccak_multisig.rs

1use alloc::collections::BTreeSet;
2use alloc::vec::Vec;
3
4use miden_objects::account::auth::PublicKeyCommitment;
5use miden_objects::account::{AccountComponent, StorageMap, StorageSlot};
6use miden_objects::{AccountError, Word};
7
8use crate::account::components::ecdsa_k256_keccak_multisig_library;
9
10// MULTISIG AUTHENTICATION COMPONENT
11// ================================================================================================
12
13/// Configuration for [`AuthEcdsaK256KeccakMultisig`] component.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct AuthEcdsaK256KeccakMultisigConfig {
16    approvers: Vec<PublicKeyCommitment>,
17    default_threshold: u32,
18    proc_thresholds: Vec<(Word, u32)>,
19}
20
21impl AuthEcdsaK256KeccakMultisigConfig {
22    /// Creates a new configuration with the given approvers and a default threshold.
23    ///
24    /// The `default_threshold` must be at least 1 and at most the number of approvers.
25    pub fn new(
26        approvers: Vec<PublicKeyCommitment>,
27        default_threshold: u32,
28    ) -> Result<Self, AccountError> {
29        if default_threshold == 0 {
30            return Err(AccountError::other("threshold must be at least 1"));
31        }
32        if default_threshold > approvers.len() as u32 {
33            return Err(AccountError::other(
34                "threshold cannot be greater than number of approvers",
35            ));
36        }
37
38        // Check for duplicate approvers
39        if approvers.len() != approvers.iter().collect::<BTreeSet<_>>().len() {
40            return Err(AccountError::other("duplicate approver public keys are not allowed"));
41        }
42
43        Ok(Self {
44            approvers,
45            default_threshold,
46            proc_thresholds: vec![],
47        })
48    }
49
50    /// Attaches a per-procedure threshold map. Each procedure threshold must be at least 1 and
51    /// at most the number of approvers.
52    pub fn with_proc_thresholds(
53        mut self,
54        proc_thresholds: Vec<(Word, u32)>,
55    ) -> Result<Self, AccountError> {
56        for (_, threshold) in &proc_thresholds {
57            if *threshold == 0 {
58                return Err(AccountError::other("procedure threshold must be at least 1"));
59            }
60            if *threshold > self.approvers.len() as u32 {
61                return Err(AccountError::other(
62                    "procedure threshold cannot be greater than number of approvers",
63                ));
64            }
65        }
66        self.proc_thresholds = proc_thresholds;
67        Ok(self)
68    }
69
70    pub fn approvers(&self) -> &[PublicKeyCommitment] {
71        &self.approvers
72    }
73
74    pub fn default_threshold(&self) -> u32 {
75        self.default_threshold
76    }
77
78    pub fn proc_thresholds(&self) -> &[(Word, u32)] {
79        &self.proc_thresholds
80    }
81}
82
83/// An [`AccountComponent`] implementing a multisig based on ECDSA signatures.
84///
85/// It enforces a threshold of approver signatures for every transaction, with optional
86/// per-procedure thresholds overrides. Non-uniform thresholds (especially a threshold of one)
87/// should be used with caution for private multisig accounts, as a single approver could withhold
88///  the new state from other approvers, effectively locking them out.
89///
90/// The storage layout is:
91/// - Slot 0(value): [threshold, num_approvers, 0, 0]
92/// - Slot 1(map): A map with approver public keys (index -> pubkey)
93/// - Slot 2(map): A map which stores executed transactions
94/// - Slot 3(map): A map which stores procedure thresholds (PROC_ROOT -> threshold)
95///
96/// This component supports all account types.
97#[derive(Debug)]
98pub struct AuthEcdsaK256KeccakMultisig {
99    config: AuthEcdsaK256KeccakMultisigConfig,
100}
101
102impl AuthEcdsaK256KeccakMultisig {
103    /// Creates a new [`AuthEcdsaK256KeccakMultisig`] component from the provided configuration.
104    pub fn new(config: AuthEcdsaK256KeccakMultisigConfig) -> Result<Self, AccountError> {
105        Ok(Self { config })
106    }
107}
108
109impl From<AuthEcdsaK256KeccakMultisig> for AccountComponent {
110    fn from(multisig: AuthEcdsaK256KeccakMultisig) -> Self {
111        let mut storage_slots = Vec::with_capacity(3);
112
113        // Slot 0: [threshold, num_approvers, 0, 0]
114        let num_approvers = multisig.config.approvers().len() as u32;
115        storage_slots.push(StorageSlot::Value(Word::from([
116            multisig.config.default_threshold(),
117            num_approvers,
118            0,
119            0,
120        ])));
121
122        // Slot 1: A map with approver public keys
123        let map_entries = multisig
124            .config
125            .approvers()
126            .iter()
127            .enumerate()
128            .map(|(i, pub_key)| (Word::from([i as u32, 0, 0, 0]), (*pub_key).into()));
129
130        // Safe to unwrap because we know that the map keys are unique.
131        storage_slots.push(StorageSlot::Map(StorageMap::with_entries(map_entries).unwrap()));
132
133        // Slot 2: A map which stores executed transactions
134        let executed_transactions = StorageMap::default();
135        storage_slots.push(StorageSlot::Map(executed_transactions));
136
137        // Slot 3: A map which stores procedure thresholds (PROC_ROOT -> threshold)
138        let proc_threshold_roots = StorageMap::with_entries(
139            multisig
140                .config
141                .proc_thresholds()
142                .iter()
143                .map(|(proc_root, threshold)| (*proc_root, Word::from([*threshold, 0, 0, 0]))),
144        )
145        .unwrap();
146        storage_slots.push(StorageSlot::Map(proc_threshold_roots));
147
148        AccountComponent::new(ecdsa_k256_keccak_multisig_library(), storage_slots)
149            .expect("Multisig auth component should satisfy the requirements of a valid account component")
150            .with_supports_all_types()
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use alloc::string::ToString;
157
158    use miden_objects::Word;
159    use miden_objects::account::AccountBuilder;
160
161    use super::*;
162    use crate::account::wallets::BasicWallet;
163
164    /// Test multisig component setup with various configurations
165    #[test]
166    fn test_multisig_component_setup() {
167        // Create test public keys
168        let pub_key_1 = PublicKeyCommitment::from(Word::from([1u32, 0, 0, 0]));
169        let pub_key_2 = PublicKeyCommitment::from(Word::from([2u32, 0, 0, 0]));
170        let pub_key_3 = PublicKeyCommitment::from(Word::from([3u32, 0, 0, 0]));
171        let approvers = vec![pub_key_1, pub_key_2, pub_key_3];
172        let threshold = 2u32;
173
174        // Create multisig component
175        let multisig_component = AuthEcdsaK256KeccakMultisig::new(
176            AuthEcdsaK256KeccakMultisigConfig::new(approvers.clone(), threshold)
177                .expect("invalid multisig config"),
178        )
179        .expect("multisig component creation failed");
180
181        // Build account with multisig component
182        let account = AccountBuilder::new([0; 32])
183            .with_auth_component(multisig_component)
184            .with_component(BasicWallet)
185            .build()
186            .expect("account building failed");
187
188        // Verify slot 0: [threshold, num_approvers, 0, 0]
189        let threshold_slot = account.storage().get_item(0).expect("storage slot 0 access failed");
190        assert_eq!(threshold_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
191
192        // Verify slot 1: Approver public keys in map
193        for (i, expected_pub_key) in approvers.iter().enumerate() {
194            let stored_pub_key = account
195                .storage()
196                .get_map_item(1, Word::from([i as u32, 0, 0, 0]))
197                .expect("storage map access failed");
198            assert_eq!(stored_pub_key, Word::from(*expected_pub_key));
199        }
200    }
201
202    /// Test multisig component with minimum threshold (1 of 1)
203    #[test]
204    fn test_multisig_component_minimum_threshold() {
205        let pub_key = PublicKeyCommitment::from(Word::from([42u32, 0, 0, 0]));
206        let approvers = vec![pub_key];
207        let threshold = 1u32;
208
209        let multisig_component = AuthEcdsaK256KeccakMultisig::new(
210            AuthEcdsaK256KeccakMultisigConfig::new(approvers.clone(), threshold)
211                .expect("invalid multisig config"),
212        )
213        .expect("multisig component creation failed");
214
215        let account = AccountBuilder::new([0; 32])
216            .with_auth_component(multisig_component)
217            .with_component(BasicWallet)
218            .build()
219            .expect("account building failed");
220
221        // Verify storage layout
222        let threshold_slot = account.storage().get_item(0).expect("storage slot 0 access failed");
223        assert_eq!(threshold_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
224
225        let stored_pub_key = account
226            .storage()
227            .get_map_item(1, Word::from([0u32, 0, 0, 0]))
228            .expect("storage map access failed");
229        assert_eq!(stored_pub_key, Word::from(pub_key));
230    }
231
232    /// Test multisig component error cases
233    #[test]
234    fn test_multisig_component_error_cases() {
235        let pub_key = PublicKeyCommitment::from(Word::from([1u32, 0, 0, 0]));
236        let approvers = vec![pub_key];
237
238        // Test threshold = 0 (should fail)
239        let result = AuthEcdsaK256KeccakMultisigConfig::new(approvers.clone(), 0);
240        assert!(result.unwrap_err().to_string().contains("threshold must be at least 1"));
241
242        // Test threshold > number of approvers (should fail)
243        let result = AuthEcdsaK256KeccakMultisigConfig::new(approvers, 2);
244        assert!(
245            result
246                .unwrap_err()
247                .to_string()
248                .contains("threshold cannot be greater than number of approvers")
249        );
250    }
251
252    /// Test multisig component with duplicate approvers (should fail)
253    #[test]
254    fn test_multisig_component_duplicate_approvers() {
255        let pub_key_1 = PublicKeyCommitment::from(Word::from([1u32, 0, 0, 0]));
256        let pub_key_2 = PublicKeyCommitment::from(Word::from([2u32, 0, 0, 0]));
257
258        // Test with duplicate approvers (should fail)
259        let approvers = vec![pub_key_1, pub_key_2, pub_key_1];
260        let result = AuthEcdsaK256KeccakMultisigConfig::new(approvers, 2);
261        assert!(
262            result
263                .unwrap_err()
264                .to_string()
265                .contains("duplicate approver public keys are not allowed")
266        );
267    }
268}