falcon-multisig 0.1.0

Production-grade post-quantum threshold multisignature library using Falcon-512 (NIST FIPS 206 / FN-DSA)
Documentation
//! Threshold configuration for an M-of-N Falcon-512 committee.
//!
//! A [`ThresholdConfig`] holds the public keys of all N committee members and
//! the threshold M — the minimum number of valid signatures required to
//! authorise an action. It is immutable after construction and is typically
//! shared (via `Arc`) between multiple [`SigningSession`] instances.
//!
//! [`SigningSession`]: crate::session::SigningSession

#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};

use crate::{
    address::MultisigAddress,
    error::Error,
    keypair::PublicKey,
    PUBLIC_KEY_BYTES,
};

/// The minimum allowed committee size.
pub const MIN_COMMITTEE_SIZE: usize = 2;

/// An immutable M-of-N threshold configuration.
///
/// Construct with [`ThresholdConfig::new`]. The configuration validates all
/// inputs at construction time; a successfully constructed value is guaranteed
/// to be internally consistent.
///
/// # Canonical Address
///
/// Every `ThresholdConfig` has a canonical multisig address derived from the
/// sorted public keys and the (M, N) policy. This address is deterministic and
/// insertion-order-independent, so it can be used as a stable on-chain
/// identifier regardless of how committee members submit their keys.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ThresholdConfig {
    /// Number of signatures required to meet the threshold.
    required: usize,
    /// Ordered list of committee public keys.
    public_keys: Vec<PublicKey>,
    /// Canonical multisig address (derived at construction time).
    address: MultisigAddress,
}

impl ThresholdConfig {
    /// Construct a new threshold configuration.
    ///
    /// # Parameters
    ///
    /// - `required`: M — the number of valid signatures required.
    /// - `public_keys`: The N committee member public keys, in any order.
    ///   Each key must be exactly [`PUBLIC_KEY_BYTES`] bytes.
    ///
    /// # Errors
    ///
    /// - [`Error::CommitteeTooSmall`] if fewer than 2 keys are supplied.
    /// - [`Error::InvalidThreshold`] if `required` is 0 or exceeds the number of keys.
    /// - [`Error::InvalidPublicKeyLength`] if any key has an incorrect byte length.
    pub fn new(required: usize, public_keys: Vec<PublicKey>) -> Result<Self, Error> {
        let total = public_keys.len();

        if total < MIN_COMMITTEE_SIZE {
            return Err(Error::CommitteeTooSmall { count: total });
        }
        if required == 0 || required > total {
            return Err(Error::InvalidThreshold {
                required,
                total_keys: total,
            });
        }

        // Validate all public key lengths.
        for (i, pk) in public_keys.iter().enumerate() {
            if pk.as_bytes().len() != PUBLIC_KEY_BYTES {
                return Err(Error::InvalidPublicKeyLength {
                    expected: PUBLIC_KEY_BYTES,
                    actual: pk.as_bytes().len(),
                });
            }
            let _ = i; // suppress unused variable warning when iterating
        }

        let address = MultisigAddress::derive(&public_keys, required, total);

        Ok(Self {
            required,
            public_keys,
            address,
        })
    }

    /// The required signature threshold M.
    pub fn required(&self) -> usize {
        self.required
    }

    /// The total number of committee members N.
    pub fn total(&self) -> usize {
        self.public_keys.len()
    }

    /// A human-readable policy string such as `"2-of-3"`.
    pub fn policy(&self) -> String {
        format!("{}-of-{}", self.required, self.public_keys.len())
    }

    /// The ordered list of committee public keys.
    pub fn public_keys(&self) -> &[PublicKey] {
        &self.public_keys
    }

    /// The canonical multisig address for this committee and policy.
    pub fn address(&self) -> &MultisigAddress {
        &self.address
    }

    /// Return the public key at the given committee index.
    ///
    /// Returns `None` if `index >= total`.
    pub fn get_public_key(&self, index: usize) -> Option<&PublicKey> {
        self.public_keys.get(index)
    }
}

// ---------------------------------------------------------------------------
// Unit tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::keypair::KeyPair;

    fn make_keys(n: usize) -> Vec<PublicKey> {
        (0..n)
            .map(|_| KeyPair::generate().public_key().clone())
            .collect()
    }

    #[test]
    fn valid_2of3_config() {
        let keys = make_keys(3);
        let cfg = ThresholdConfig::new(2, keys).unwrap();
        assert_eq!(cfg.required(), 2);
        assert_eq!(cfg.total(), 3);
        assert_eq!(cfg.policy(), "2-of-3");
    }

    #[test]
    fn valid_3of5_config() {
        let keys = make_keys(5);
        let cfg = ThresholdConfig::new(3, keys).unwrap();
        assert_eq!(cfg.policy(), "3-of-5");
    }

    #[test]
    fn nof_n_is_valid() {
        let keys = make_keys(4);
        let cfg = ThresholdConfig::new(4, keys).unwrap();
        assert_eq!(cfg.required(), 4);
    }

    #[test]
    fn single_member_committee_rejected() {
        let keys = make_keys(1);
        let err = ThresholdConfig::new(1, keys).unwrap_err();
        assert!(matches!(err, Error::CommitteeTooSmall { count: 1 }));
    }

    #[test]
    fn zero_required_rejected() {
        let keys = make_keys(3);
        let err = ThresholdConfig::new(0, keys).unwrap_err();
        assert!(matches!(err, Error::InvalidThreshold { required: 0, .. }));
    }

    #[test]
    fn required_exceeds_total_rejected() {
        let keys = make_keys(3);
        let err = ThresholdConfig::new(4, keys).unwrap_err();
        assert!(matches!(err, Error::InvalidThreshold { required: 4, total_keys: 3 }));
    }

    #[test]
    fn address_is_deterministic_regardless_of_insertion_order() {
        let kp0 = KeyPair::generate();
        let kp1 = KeyPair::generate();
        let kp2 = KeyPair::generate();

        let order_a = vec![
            kp0.public_key().clone(),
            kp1.public_key().clone(),
            kp2.public_key().clone(),
        ];
        let order_b = vec![
            kp2.public_key().clone(),
            kp0.public_key().clone(),
            kp1.public_key().clone(),
        ];

        let cfg_a = ThresholdConfig::new(2, order_a).unwrap();
        let cfg_b = ThresholdConfig::new(2, order_b).unwrap();

        assert_eq!(
            cfg_a.address(),
            cfg_b.address(),
            "multisig address must be insertion-order-independent"
        );
    }

    #[test]
    fn get_public_key_returns_correct_key() {
        let keys = make_keys(3);
        let expected = keys[1].clone();
        let cfg = ThresholdConfig::new(2, keys).unwrap();
        assert_eq!(cfg.get_public_key(1), Some(&expected));
    }

    #[test]
    fn get_public_key_out_of_bounds_returns_none() {
        let cfg = ThresholdConfig::new(2, make_keys(3)).unwrap();
        assert!(cfg.get_public_key(99).is_none());
    }
}