Skip to main content

falcon_multisig/
threshold.rs

1//! Threshold configuration for an M-of-N Falcon-512 committee.
2//!
3//! A [`ThresholdConfig`] holds the public keys of all N committee members and
4//! the threshold M — the minimum number of valid signatures required to
5//! authorise an action. It is immutable after construction and is typically
6//! shared (via `Arc`) between multiple [`SigningSession`] instances.
7//!
8//! [`SigningSession`]: crate::session::SigningSession
9
10#[cfg(not(feature = "std"))]
11use alloc::{string::String, vec::Vec};
12
13use crate::{
14    address::MultisigAddress,
15    error::Error,
16    keypair::PublicKey,
17    PUBLIC_KEY_BYTES,
18};
19
20/// The minimum allowed committee size.
21pub const MIN_COMMITTEE_SIZE: usize = 2;
22
23/// An immutable M-of-N threshold configuration.
24///
25/// Construct with [`ThresholdConfig::new`]. The configuration validates all
26/// inputs at construction time; a successfully constructed value is guaranteed
27/// to be internally consistent.
28///
29/// # Canonical Address
30///
31/// Every `ThresholdConfig` has a canonical multisig address derived from the
32/// sorted public keys and the (M, N) policy. This address is deterministic and
33/// insertion-order-independent, so it can be used as a stable on-chain
34/// identifier regardless of how committee members submit their keys.
35#[derive(Clone, Debug, PartialEq, Eq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub struct ThresholdConfig {
38    /// Number of signatures required to meet the threshold.
39    required: usize,
40    /// Ordered list of committee public keys.
41    public_keys: Vec<PublicKey>,
42    /// Canonical multisig address (derived at construction time).
43    address: MultisigAddress,
44}
45
46impl ThresholdConfig {
47    /// Construct a new threshold configuration.
48    ///
49    /// # Parameters
50    ///
51    /// - `required`: M — the number of valid signatures required.
52    /// - `public_keys`: The N committee member public keys, in any order.
53    ///   Each key must be exactly [`PUBLIC_KEY_BYTES`] bytes.
54    ///
55    /// # Errors
56    ///
57    /// - [`Error::CommitteeTooSmall`] if fewer than 2 keys are supplied.
58    /// - [`Error::InvalidThreshold`] if `required` is 0 or exceeds the number of keys.
59    /// - [`Error::InvalidPublicKeyLength`] if any key has an incorrect byte length.
60    pub fn new(required: usize, public_keys: Vec<PublicKey>) -> Result<Self, Error> {
61        let total = public_keys.len();
62
63        if total < MIN_COMMITTEE_SIZE {
64            return Err(Error::CommitteeTooSmall { count: total });
65        }
66        if required == 0 || required > total {
67            return Err(Error::InvalidThreshold {
68                required,
69                total_keys: total,
70            });
71        }
72
73        // Validate all public key lengths.
74        for (i, pk) in public_keys.iter().enumerate() {
75            if pk.as_bytes().len() != PUBLIC_KEY_BYTES {
76                return Err(Error::InvalidPublicKeyLength {
77                    expected: PUBLIC_KEY_BYTES,
78                    actual: pk.as_bytes().len(),
79                });
80            }
81            let _ = i; // suppress unused variable warning when iterating
82        }
83
84        let address = MultisigAddress::derive(&public_keys, required, total);
85
86        Ok(Self {
87            required,
88            public_keys,
89            address,
90        })
91    }
92
93    /// The required signature threshold M.
94    pub fn required(&self) -> usize {
95        self.required
96    }
97
98    /// The total number of committee members N.
99    pub fn total(&self) -> usize {
100        self.public_keys.len()
101    }
102
103    /// A human-readable policy string such as `"2-of-3"`.
104    pub fn policy(&self) -> String {
105        format!("{}-of-{}", self.required, self.public_keys.len())
106    }
107
108    /// The ordered list of committee public keys.
109    pub fn public_keys(&self) -> &[PublicKey] {
110        &self.public_keys
111    }
112
113    /// The canonical multisig address for this committee and policy.
114    pub fn address(&self) -> &MultisigAddress {
115        &self.address
116    }
117
118    /// Return the public key at the given committee index.
119    ///
120    /// Returns `None` if `index >= total`.
121    pub fn get_public_key(&self, index: usize) -> Option<&PublicKey> {
122        self.public_keys.get(index)
123    }
124}
125
126// ---------------------------------------------------------------------------
127// Unit tests
128// ---------------------------------------------------------------------------
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::keypair::KeyPair;
134
135    fn make_keys(n: usize) -> Vec<PublicKey> {
136        (0..n)
137            .map(|_| KeyPair::generate().public_key().clone())
138            .collect()
139    }
140
141    #[test]
142    fn valid_2of3_config() {
143        let keys = make_keys(3);
144        let cfg = ThresholdConfig::new(2, keys).unwrap();
145        assert_eq!(cfg.required(), 2);
146        assert_eq!(cfg.total(), 3);
147        assert_eq!(cfg.policy(), "2-of-3");
148    }
149
150    #[test]
151    fn valid_3of5_config() {
152        let keys = make_keys(5);
153        let cfg = ThresholdConfig::new(3, keys).unwrap();
154        assert_eq!(cfg.policy(), "3-of-5");
155    }
156
157    #[test]
158    fn nof_n_is_valid() {
159        let keys = make_keys(4);
160        let cfg = ThresholdConfig::new(4, keys).unwrap();
161        assert_eq!(cfg.required(), 4);
162    }
163
164    #[test]
165    fn single_member_committee_rejected() {
166        let keys = make_keys(1);
167        let err = ThresholdConfig::new(1, keys).unwrap_err();
168        assert!(matches!(err, Error::CommitteeTooSmall { count: 1 }));
169    }
170
171    #[test]
172    fn zero_required_rejected() {
173        let keys = make_keys(3);
174        let err = ThresholdConfig::new(0, keys).unwrap_err();
175        assert!(matches!(err, Error::InvalidThreshold { required: 0, .. }));
176    }
177
178    #[test]
179    fn required_exceeds_total_rejected() {
180        let keys = make_keys(3);
181        let err = ThresholdConfig::new(4, keys).unwrap_err();
182        assert!(matches!(err, Error::InvalidThreshold { required: 4, total_keys: 3 }));
183    }
184
185    #[test]
186    fn address_is_deterministic_regardless_of_insertion_order() {
187        let kp0 = KeyPair::generate();
188        let kp1 = KeyPair::generate();
189        let kp2 = KeyPair::generate();
190
191        let order_a = vec![
192            kp0.public_key().clone(),
193            kp1.public_key().clone(),
194            kp2.public_key().clone(),
195        ];
196        let order_b = vec![
197            kp2.public_key().clone(),
198            kp0.public_key().clone(),
199            kp1.public_key().clone(),
200        ];
201
202        let cfg_a = ThresholdConfig::new(2, order_a).unwrap();
203        let cfg_b = ThresholdConfig::new(2, order_b).unwrap();
204
205        assert_eq!(
206            cfg_a.address(),
207            cfg_b.address(),
208            "multisig address must be insertion-order-independent"
209        );
210    }
211
212    #[test]
213    fn get_public_key_returns_correct_key() {
214        let keys = make_keys(3);
215        let expected = keys[1].clone();
216        let cfg = ThresholdConfig::new(2, keys).unwrap();
217        assert_eq!(cfg.get_public_key(1), Some(&expected));
218    }
219
220    #[test]
221    fn get_public_key_out_of_bounds_returns_none() {
222        let cfg = ThresholdConfig::new(2, make_keys(3)).unwrap();
223        assert!(cfg.get_public_key(99).is_none());
224    }
225}