Skip to main content

aion_context/
multisig.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Multi-Signature Support Module
3//!
4//! Provides M-of-N threshold signature verification for scenarios
5//! requiring multiple signers to approve a version.
6//!
7//! # Use Cases
8//!
9//! - **Dual Control**: 2-of-2 for financial rules requiring two approvers
10//! - **Committee Approval**: 3-of-5 for board-level policy changes
11//! - **Backup Recovery**: 2-of-3 for key recovery scenarios
12
13use crate::serializer::{SignatureEntry, VersionEntry};
14use crate::signature_chain::verify_attestation;
15use crate::types::AuthorId;
16use crate::{AionError, Result};
17
18/// Multi-signature policy defining required signers
19#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
20pub struct MultiSigPolicy {
21    /// Minimum required signatures (M in M-of-N)
22    pub threshold: u32,
23    /// Total authorized signers (N in M-of-N)
24    pub total_signers: u32,
25    /// List of authorized signer IDs
26    pub authorized_signers: Vec<AuthorId>,
27}
28
29impl MultiSigPolicy {
30    /// Create a new multi-signature policy
31    ///
32    /// # Arguments
33    ///
34    /// * `threshold` - Minimum signatures required (M)
35    /// * `authorized_signers` - List of authorized signer IDs
36    ///
37    /// # Errors
38    ///
39    /// Returns error if threshold > number of signers or threshold is 0
40    ///
41    /// # Example
42    ///
43    /// ```
44    /// use aion_context::multisig::MultiSigPolicy;
45    /// use aion_context::types::AuthorId;
46    ///
47    /// // 2-of-3 policy
48    /// let signers = vec![
49    ///     AuthorId::new(1001),
50    ///     AuthorId::new(1002),
51    ///     AuthorId::new(1003),
52    /// ];
53    /// let policy = MultiSigPolicy::new(2, signers).unwrap();
54    /// assert_eq!(policy.threshold, 2);
55    /// assert_eq!(policy.total_signers, 3);
56    /// ```
57    pub fn new(threshold: u32, authorized_signers: Vec<AuthorId>) -> Result<Self> {
58        if threshold == 0 {
59            return Err(AionError::InvalidFormat {
60                reason: "Threshold must be at least 1".to_string(),
61            });
62        }
63
64        let total = authorized_signers.len() as u32;
65        if threshold > total {
66            return Err(AionError::InvalidFormat {
67                reason: format!(
68                    "Threshold ({threshold}) cannot exceed number of signers ({total})"
69                ),
70            });
71        }
72
73        Ok(Self {
74            threshold,
75            total_signers: total,
76            authorized_signers,
77        })
78    }
79
80    /// Create a 1-of-1 single signer policy
81    #[must_use]
82    pub fn single_signer(signer: AuthorId) -> Self {
83        Self {
84            threshold: 1,
85            total_signers: 1,
86            authorized_signers: vec![signer],
87        }
88    }
89
90    /// Create an M-of-N policy
91    pub fn m_of_n(m: u32, signers: Vec<AuthorId>) -> Result<Self> {
92        Self::new(m, signers)
93    }
94
95    /// Check if an author is an authorized signer
96    #[must_use]
97    pub fn is_authorized(&self, author: AuthorId) -> bool {
98        self.authorized_signers.contains(&author)
99    }
100
101    /// Get the M-of-N description
102    #[must_use]
103    pub fn description(&self) -> String {
104        format!("{}-of-{}", self.threshold, self.total_signers)
105    }
106}
107
108/// Result of multi-signature verification
109#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
110pub struct MultiSigVerification {
111    /// Whether the threshold was met
112    pub threshold_met: bool,
113    /// Number of valid signatures
114    pub valid_count: u32,
115    /// Required threshold
116    pub required: u32,
117    /// List of signers who provided valid signatures
118    pub valid_signers: Vec<AuthorId>,
119    /// List of signers who provided invalid signatures
120    pub invalid_signers: Vec<AuthorId>,
121    /// List of authorized signers who did not sign
122    pub missing_signers: Vec<AuthorId>,
123}
124
125impl MultiSigVerification {
126    /// Check if verification passed
127    #[must_use]
128    pub fn is_valid(&self) -> bool {
129        self.threshold_met && self.invalid_signers.is_empty()
130    }
131}
132
133/// Verify multiple signatures against a policy, pinned by a
134/// [`KeyRegistry`](crate::key_registry::KeyRegistry) — RFC-0021 / RFC-0034.
135///
136/// Each per-signer attestation is checked against `registry` at
137/// `version.version_number` via [`verify_attestation`]. A signer
138/// whose pinned active epoch does not match the signature's
139/// embedded `public_key` is classified as `invalid_signers`, not
140/// `valid_signers`, regardless of whether the raw Ed25519 bytes
141/// would verify on their own.
142///
143/// # Errors
144///
145/// Returns `Ok(_)` in the happy path and in every "signer
146/// rejected" path. Returns `Err` only for structural issues with
147/// the `version` / `signatures` slices themselves.
148pub fn verify_multisig(
149    version: &VersionEntry,
150    signatures: &[SignatureEntry],
151    policy: &MultiSigPolicy,
152    registry: &crate::key_registry::KeyRegistry,
153) -> Result<MultiSigVerification> {
154    let mut valid_signers = Vec::new();
155    let mut invalid_signers = Vec::new();
156    let mut seen: std::collections::HashSet<AuthorId> = std::collections::HashSet::new();
157
158    for sig in signatures {
159        let author = AuthorId::new(sig.author_id);
160        if !policy.is_authorized(author) {
161            continue;
162        }
163        if !seen.insert(author) {
164            continue;
165        }
166        match verify_attestation(version, sig, registry) {
167            Ok(()) => valid_signers.push(author),
168            Err(_) => invalid_signers.push(author),
169        }
170    }
171
172    let missing_signers: Vec<_> = policy
173        .authorized_signers
174        .iter()
175        .filter(|a| !seen.contains(a))
176        .copied()
177        .collect();
178
179    let valid_count = valid_signers.len() as u32;
180    let threshold_met = valid_count >= policy.threshold;
181
182    if threshold_met && invalid_signers.is_empty() {
183        tracing::info!(
184            event = "multisig_threshold_met",
185            version = version.version_number,
186            valid = valid_count,
187            required = policy.threshold,
188        );
189    } else {
190        tracing::warn!(
191            event = "multisig_threshold_short",
192            version = version.version_number,
193            valid = valid_count,
194            required = policy.threshold,
195            invalid = invalid_signers.len() as u32,
196            missing = missing_signers.len() as u32,
197            reason = if invalid_signers.is_empty() {
198                "insufficient_signers"
199            } else {
200                "byzantine_signer"
201            },
202        );
203    }
204
205    Ok(MultiSigVerification {
206        threshold_met,
207        valid_count,
208        required: policy.threshold,
209        valid_signers,
210        invalid_signers,
211        missing_signers,
212    })
213}
214
215/// Aggregate multiple signatures for a version
216///
217/// This collects signatures from multiple signers into a single
218/// list that can be stored with the version.
219#[derive(Debug, Clone)]
220pub struct SignatureAggregator {
221    signatures: Vec<SignatureEntry>,
222}
223
224impl SignatureAggregator {
225    /// Create a new aggregator
226    #[must_use]
227    pub const fn new() -> Self {
228        Self {
229            signatures: Vec::new(),
230        }
231    }
232
233    /// Add a signature to the aggregation
234    pub fn add_signature(&mut self, signature: SignatureEntry) {
235        self.signatures.push(signature);
236    }
237
238    /// Get the number of signatures collected
239    #[must_use]
240    pub fn count(&self) -> usize {
241        self.signatures.len()
242    }
243
244    /// Get the collected signatures
245    #[must_use]
246    pub fn signatures(&self) -> &[SignatureEntry] {
247        &self.signatures
248    }
249
250    /// Consume and return the signatures
251    #[must_use]
252    pub fn into_signatures(self) -> Vec<SignatureEntry> {
253        self.signatures
254    }
255}
256
257impl Default for SignatureAggregator {
258    fn default() -> Self {
259        Self::new()
260    }
261}
262
263#[cfg(test)]
264#[allow(deprecated)] // RFC-0034 Phase D: tests exercise the deprecated raw-key verify_multisig contract
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_policy_creation() {
270        let signers = vec![AuthorId::new(1), AuthorId::new(2), AuthorId::new(3)];
271        let policy = MultiSigPolicy::new(2, signers).unwrap_or_else(|_| std::process::abort());
272
273        assert_eq!(policy.threshold, 2);
274        assert_eq!(policy.total_signers, 3);
275        assert_eq!(policy.description(), "2-of-3");
276    }
277
278    #[test]
279    fn test_policy_invalid_threshold() {
280        let signers = vec![AuthorId::new(1), AuthorId::new(2)];
281
282        // Threshold too high
283        let result = MultiSigPolicy::new(3, signers.clone());
284        assert!(result.is_err());
285
286        // Zero threshold
287        let result = MultiSigPolicy::new(0, signers);
288        assert!(result.is_err());
289    }
290
291    #[test]
292    fn test_single_signer_policy() {
293        let policy = MultiSigPolicy::single_signer(AuthorId::new(42));
294
295        assert_eq!(policy.threshold, 1);
296        assert_eq!(policy.total_signers, 1);
297        assert!(policy.is_authorized(AuthorId::new(42)));
298        assert!(!policy.is_authorized(AuthorId::new(99)));
299    }
300
301    #[test]
302    fn test_signature_aggregator() {
303        let mut agg = SignatureAggregator::new();
304        assert_eq!(agg.count(), 0);
305
306        let sig = SignatureEntry {
307            author_id: 100,
308            public_key: [0u8; 32],
309            signature: [0u8; 64],
310            reserved: [0u8; 8],
311        };
312
313        agg.add_signature(sig);
314        assert_eq!(agg.count(), 1);
315    }
316
317    mod properties {
318        use super::*;
319        use crate::crypto::SigningKey;
320        use crate::key_registry::KeyRegistry;
321        use crate::serializer::VersionEntry;
322        use crate::signature_chain::sign_attestation;
323        use crate::types::VersionNumber;
324        use hegel::generators as gs;
325
326        /// Build a registry pinning every `(author, key)` pair, each
327        /// at epoch 0. Sufficient for the multisig property tests
328        /// that pin all authorized signers under their own keys.
329        fn pin_all(signers: &[(AuthorId, SigningKey)]) -> KeyRegistry {
330            let mut reg = KeyRegistry::new();
331            for (author, key) in signers {
332                let master = SigningKey::generate();
333                reg.register_author(*author, master.verifying_key(), key.verifying_key(), 0)
334                    .unwrap_or_else(|_| std::process::abort());
335            }
336            reg
337        }
338
339        fn make_version(author: AuthorId) -> VersionEntry {
340            VersionEntry::new(
341                VersionNumber::GENESIS,
342                [0u8; 32],
343                [0xAA; 32],
344                author,
345                1_700_000_000_000_000_000,
346                0,
347                0,
348            )
349        }
350
351        /// Build `n` distinct signer identities with fresh keys, none of whom
352        /// collide with `exclude` (the version author). Keeps ids monotonic so
353        /// we can reason about them in the tests below.
354        fn distinct_signers(n: u32, exclude: AuthorId) -> Vec<(AuthorId, SigningKey)> {
355            let mut out = Vec::with_capacity(n as usize);
356            let mut next_id: u64 = 10_000;
357            while (out.len() as u32) < n {
358                if next_id != exclude.as_u64() {
359                    out.push((AuthorId::new(next_id), SigningKey::generate()));
360                }
361                next_id = next_id.saturating_add(1);
362            }
363            out
364        }
365
366        #[hegel::test]
367        fn prop_multisig_k_distinct_signers_accepts(tc: hegel::TestCase) {
368            let n = tc.draw(gs::integers::<u32>().min_value(1).max_value(8));
369            let threshold = tc.draw(gs::integers::<u32>().min_value(1).max_value(n));
370            let version_author =
371                AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(u64::MAX)));
372            let version = make_version(version_author);
373            let signers = distinct_signers(n, version_author);
374            let authorized: Vec<AuthorId> = signers.iter().map(|(a, _)| *a).collect();
375            let policy = MultiSigPolicy::new(threshold, authorized)
376                .unwrap_or_else(|_| std::process::abort());
377            let attestations: Vec<SignatureEntry> = signers
378                .iter()
379                .take(threshold as usize)
380                .map(|(who, key)| sign_attestation(&version, *who, key))
381                .collect();
382            let reg = pin_all(&signers);
383            let result = verify_multisig(&version, &attestations, &policy, &reg)
384                .unwrap_or_else(|_| std::process::abort());
385            assert!(result.threshold_met);
386            assert_eq!(result.valid_count, threshold);
387        }
388
389        #[hegel::test]
390        fn prop_multisig_kminus1_distinct_rejects(tc: hegel::TestCase) {
391            let n = tc.draw(gs::integers::<u32>().min_value(2).max_value(8));
392            let threshold = tc.draw(gs::integers::<u32>().min_value(2).max_value(n));
393            let version_author =
394                AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(u64::MAX)));
395            let version = make_version(version_author);
396            let signers = distinct_signers(n, version_author);
397            let authorized: Vec<AuthorId> = signers.iter().map(|(a, _)| *a).collect();
398            let policy = MultiSigPolicy::new(threshold, authorized)
399                .unwrap_or_else(|_| std::process::abort());
400            let short = threshold.saturating_sub(1) as usize;
401            let attestations: Vec<SignatureEntry> = signers
402                .iter()
403                .take(short)
404                .map(|(who, key)| sign_attestation(&version, *who, key))
405                .collect();
406            let reg = pin_all(&signers);
407            let result = verify_multisig(&version, &attestations, &policy, &reg)
408                .unwrap_or_else(|_| std::process::abort());
409            assert!(!result.threshold_met);
410        }
411
412        #[hegel::test]
413        fn prop_multisig_duplicate_attestations_count_once(tc: hegel::TestCase) {
414            let n = tc.draw(gs::integers::<u32>().min_value(2).max_value(8));
415            let threshold = tc.draw(gs::integers::<u32>().min_value(2).max_value(n));
416            let dups = tc.draw(gs::integers::<u32>().min_value(2).max_value(8));
417            let version_author =
418                AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(u64::MAX)));
419            let version = make_version(version_author);
420            let signers = distinct_signers(n, version_author);
421            let authorized: Vec<AuthorId> = signers.iter().map(|(a, _)| *a).collect();
422            let policy = MultiSigPolicy::new(threshold, authorized)
423                .unwrap_or_else(|_| std::process::abort());
424            // All signatures come from the same signer, repeated `dups` times.
425            let first = signers.first().unwrap_or_else(|| std::process::abort());
426            let att = sign_attestation(&version, first.0, &first.1);
427            let attestations: Vec<SignatureEntry> = (0..dups).map(|_| att).collect();
428            let reg = pin_all(&signers);
429            let result = verify_multisig(&version, &attestations, &policy, &reg)
430                .unwrap_or_else(|_| std::process::abort());
431            assert_eq!(result.valid_count, 1);
432            assert!(!result.threshold_met);
433        }
434
435        #[hegel::test]
436        fn prop_unauthorized_signers_do_not_count(tc: hegel::TestCase) {
437            let author_id = tc.draw(gs::integers::<u64>().min_value(1).max_value(u64::MAX / 2));
438            let author = AuthorId::new(author_id);
439            let version = make_version(author);
440            let impostor = AuthorId::new(author_id.wrapping_add(1).max(2));
441            let key = SigningKey::generate();
442            let sig = sign_attestation(&version, impostor, &key);
443            let policy =
444                MultiSigPolicy::new(1, vec![author]).unwrap_or_else(|_| std::process::abort());
445            // Pin the authorized author, not the impostor — verify still rejects the
446            // impostor because they're not in the policy's authorized_signers.
447            let author_key = SigningKey::generate();
448            let reg = pin_all(&[(author, author_key)]);
449            let result = verify_multisig(&version, &[sig], &policy, &reg)
450                .unwrap_or_else(|_| std::process::abort());
451            assert_eq!(result.valid_count, 0);
452            assert!(!result.threshold_met);
453        }
454
455        #[hegel::test]
456        fn prop_forged_author_id_rejects(tc: hegel::TestCase) {
457            let version_author =
458                AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(u64::MAX / 2)));
459            let version = make_version(version_author);
460            let real_signer = AuthorId::new(version_author.as_u64().saturating_add(1));
461            let fake_signer = AuthorId::new(real_signer.as_u64().saturating_add(1));
462            let key = SigningKey::generate();
463            let mut sig = sign_attestation(&version, real_signer, &key);
464            sig.author_id = fake_signer.as_u64();
465            let policy =
466                MultiSigPolicy::new(1, vec![fake_signer]).unwrap_or_else(|_| std::process::abort());
467            // Pin the fake_signer — registry check detects the pubkey mismatch
468            // (sig was made by real_signer's key, pinned key is different).
469            let fake_key = SigningKey::generate();
470            let reg = pin_all(&[(fake_signer, fake_key)]);
471            let result = verify_multisig(&version, &[sig], &policy, &reg)
472                .unwrap_or_else(|_| std::process::abort());
473            assert_eq!(result.valid_count, 0);
474            assert!(!result.threshold_met);
475        }
476    }
477}