#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
use crate::{
address::MultisigAddress,
error::Error,
keypair::PublicKey,
PUBLIC_KEY_BYTES,
};
pub const MIN_COMMITTEE_SIZE: usize = 2;
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ThresholdConfig {
required: usize,
public_keys: Vec<PublicKey>,
address: MultisigAddress,
}
impl ThresholdConfig {
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,
});
}
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; }
let address = MultisigAddress::derive(&public_keys, required, total);
Ok(Self {
required,
public_keys,
address,
})
}
pub fn required(&self) -> usize {
self.required
}
pub fn total(&self) -> usize {
self.public_keys.len()
}
pub fn policy(&self) -> String {
format!("{}-of-{}", self.required, self.public_keys.len())
}
pub fn public_keys(&self) -> &[PublicKey] {
&self.public_keys
}
pub fn address(&self) -> &MultisigAddress {
&self.address
}
pub fn get_public_key(&self, index: usize) -> Option<&PublicKey> {
self.public_keys.get(index)
}
}
#[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());
}
}