use std::{collections::BTreeMap, fmt::Display};
use frost_ristretto255 as frost;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use crate::{GenesisCeremonyConfig, Result, RootError};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RootPublicKeyPackage {
pub public_key_package: Vec<u8>,
pub root_public_key: Vec<u8>,
pub verifying_shares: BTreeMap<u16, Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RootKeyPackage {
pub frost_identifier: u16,
pub key_package: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RootDkgOutput {
pub key_packages: BTreeMap<u16, RootKeyPackage>,
pub public_key_package: RootPublicKeyPackage,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RootDkgRound1Output {
pub frost_identifier: u16,
pub round1_secret_package: Vec<u8>,
pub round1_package: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RootDkgRound2Output {
pub frost_identifier: u16,
pub round2_secret_package: Vec<u8>,
pub round2_packages: BTreeMap<u16, Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RootParticipantDkgOutput {
pub key_package: RootKeyPackage,
pub public_key_package: RootPublicKeyPackage,
}
pub(crate) fn frost_identifier(identifier: u16) -> Result<frost::Identifier> {
frost::Identifier::try_from(identifier).map_err(frost_error)
}
fn rostered_frost_identifier(
config: &GenesisCeremonyConfig,
identifier: u16,
operation: &str,
) -> Result<frost::Identifier> {
if config.certifier_by_identifier(identifier).is_none() {
return Err(RootError::InvalidConfig {
reason: format!("{operation} certifier {identifier} is not rostered"),
});
}
frost_identifier(identifier)
}
fn frost_error(error: frost::Error) -> RootError {
RootError::Frost {
detail: error.to_string(),
}
}
fn frost_encoding_error(error: impl Display) -> RootError {
RootError::Frost {
detail: format!("FROST artifact canonical encoding failed: {error}"),
}
}
pub(crate) fn serialize_frost<T: Serialize>(value: &T) -> Result<Vec<u8>> {
let mut bytes = Vec::new();
ciborium::into_writer(value, &mut bytes).map_err(frost_encoding_error)?;
Ok(bytes)
}
pub(crate) fn deserialize_frost<T: DeserializeOwned>(bytes: &[u8]) -> Result<T> {
ciborium::from_reader(bytes).map_err(frost_encoding_error)
}
fn identifier_value(
config: &GenesisCeremonyConfig,
frost_identifier: frost::Identifier,
) -> Result<u16> {
for certifier in &config.certifiers {
if crate::dkg::frost_identifier(certifier.frost_identifier)? == frost_identifier {
return Ok(certifier.frost_identifier);
}
}
Err(RootError::Frost {
detail: "FROST identifier is not rostered".to_owned(),
})
}
fn peer_packages_except(
packages: &BTreeMap<u16, Vec<u8>>,
excluded: u16,
) -> BTreeMap<u16, Vec<u8>> {
packages
.iter()
.filter(|(peer, _)| **peer != excluded)
.map(|(peer, package)| (*peer, package.clone()))
.collect()
}
fn frost_dkg_round1<R>(
identifier: frost::Identifier,
max_signers: u16,
threshold: u16,
rng: &mut R,
) -> Result<(
frost::keys::dkg::round1::SecretPackage,
frost::keys::dkg::round1::Package,
)>
where
R: frost::rand_core::RngCore + frost::rand_core::CryptoRng,
{
frost::keys::dkg::part1(identifier, max_signers, threshold, rng).map_err(frost_error)
}
pub(crate) fn serialize_public_key_package(
config: &GenesisCeremonyConfig,
package: &frost::keys::PublicKeyPackage,
) -> Result<RootPublicKeyPackage> {
let public_key_package = serialize_frost(package)?;
let root_public_key = serialize_frost(package.verifying_key())?;
let mut verifying_shares = BTreeMap::new();
for certifier in &config.certifiers {
let identifier = frost_identifier(certifier.frost_identifier)?;
let share =
package
.verifying_shares()
.get(&identifier)
.ok_or_else(|| RootError::Frost {
detail: format!(
"missing verification share for identifier {}",
certifier.frost_identifier
),
})?;
verifying_shares.insert(certifier.frost_identifier, serialize_frost(share)?);
}
Ok(RootPublicKeyPackage {
public_key_package,
root_public_key,
verifying_shares,
})
}
pub(crate) fn validate_public_key_package(
config: &GenesisCeremonyConfig,
package: &RootPublicKeyPackage,
) -> Result<()> {
let frost_package: frost::keys::PublicKeyPackage =
deserialize_frost(package.public_key_package.as_slice())?;
let expected = serialize_public_key_package(config, &frost_package)?;
if &expected != package {
return Err(RootError::BundleRejected {
reason: "public key package metadata does not match serialized FROST package"
.to_owned(),
});
}
Ok(())
}
pub fn dkg_round1<R>(
config: &GenesisCeremonyConfig,
frost_identifier_value: u16,
rng: &mut R,
) -> Result<RootDkgRound1Output>
where
R: frost::rand_core::RngCore + frost::rand_core::CryptoRng,
{
config.validate()?;
let identifier = rostered_frost_identifier(config, frost_identifier_value, "round-one")?;
let max_signers = config.max_signers;
let threshold = config.threshold;
let round1 = frost_dkg_round1(identifier, max_signers, threshold, rng)?;
let (secret_package, package) = round1;
let output = RootDkgRound1Output {
frost_identifier: frost_identifier_value,
round1_secret_package: serialize_frost(&secret_package)?,
round1_package: serialize_frost(&package)?,
};
Ok(output)
}
pub fn dkg_round2(
config: &GenesisCeremonyConfig,
frost_identifier_value: u16,
round1_secret_package: &[u8],
round1_packages: BTreeMap<u16, Vec<u8>>,
) -> Result<RootDkgRound2Output> {
config.validate()?;
if round1_packages.len() != usize::from(config.max_signers - 1) {
return Err(RootError::Frost {
detail: "round two requires all twelve peer round-one packages".to_owned(),
});
}
let participant_identifier =
rostered_frost_identifier(config, frost_identifier_value, "round-two")?;
let secret_package = deserialize_frost(round1_secret_package)?;
let inbound_round1 =
deserialize_round1_packages(config, participant_identifier, round1_packages)?;
let (round2_secret_package, outbound) =
frost::keys::dkg::part2(secret_package, &inbound_round1).map_err(frost_error)?;
let mut round2_packages = BTreeMap::new();
for (recipient, package) in outbound {
let recipient_value = identifier_value(config, recipient)?;
round2_packages.insert(recipient_value, serialize_frost(&package)?);
}
Ok(RootDkgRound2Output {
frost_identifier: frost_identifier_value,
round2_secret_package: serialize_frost(&round2_secret_package)?,
round2_packages,
})
}
pub fn dkg_finalize_participant(
config: &GenesisCeremonyConfig,
frost_identifier_value: u16,
round2_secret_package: &[u8],
round1_packages: BTreeMap<u16, Vec<u8>>,
round2_packages: BTreeMap<u16, Vec<u8>>,
) -> Result<RootParticipantDkgOutput> {
config.validate()?;
if round1_packages.len() != usize::from(config.max_signers - 1) {
return Err(RootError::Frost {
detail: "finalize requires all twelve peer round-one packages".to_owned(),
});
}
if round2_packages.len() != usize::from(config.max_signers - 1) {
return Err(RootError::Frost {
detail: "finalize requires all twelve peer round-two packages".to_owned(),
});
}
let participant_identifier =
rostered_frost_identifier(config, frost_identifier_value, "finalize")?;
let secret_package = deserialize_frost(round2_secret_package)?;
let inbound_round1 =
deserialize_round1_packages(config, participant_identifier, round1_packages)?;
let inbound_round2 =
deserialize_round2_packages(config, participant_identifier, round2_packages)?;
let (key_package, public_key_package) =
frost::keys::dkg::part3(&secret_package, &inbound_round1, &inbound_round2)
.map_err(frost_error)?;
let key_package = RootKeyPackage {
frost_identifier: frost_identifier_value,
key_package: serialize_frost(&key_package)?,
};
Ok(RootParticipantDkgOutput {
key_package,
public_key_package: serialize_public_key_package(config, &public_key_package)?,
})
}
fn deserialize_round1_packages(
config: &GenesisCeremonyConfig,
participant_identifier: frost::Identifier,
packages: BTreeMap<u16, Vec<u8>>,
) -> Result<BTreeMap<frost::Identifier, frost::keys::dkg::round1::Package>> {
let mut result = BTreeMap::new();
for (sender, package_bytes) in packages {
if config.certifier_by_identifier(sender).is_none() {
return Err(RootError::InvalidConfig {
reason: format!("round-one sender {sender} is not rostered"),
});
}
let sender_identifier = frost_identifier(sender)?;
if sender_identifier == participant_identifier {
return Err(RootError::Frost {
detail: "round-one peer packages must not include self".to_owned(),
});
}
let package = deserialize_frost(package_bytes.as_slice())?;
result.insert(sender_identifier, package);
}
Ok(result)
}
fn deserialize_round2_packages(
config: &GenesisCeremonyConfig,
participant_identifier: frost::Identifier,
packages: BTreeMap<u16, Vec<u8>>,
) -> Result<BTreeMap<frost::Identifier, frost::keys::dkg::round2::Package>> {
let mut result = BTreeMap::new();
for (sender, package_bytes) in packages {
if config.certifier_by_identifier(sender).is_none() {
return Err(RootError::InvalidConfig {
reason: format!("round-two sender {sender} is not rostered"),
});
}
let sender_identifier = frost_identifier(sender)?;
if sender_identifier == participant_identifier {
return Err(RootError::Frost {
detail: "round-two peer packages must not include self".to_owned(),
});
}
let package = deserialize_frost(package_bytes.as_slice())?;
result.insert(sender_identifier, package);
}
Ok(result)
}
pub fn run_complete_dkg<R>(config: &GenesisCeremonyConfig, rng: &mut R) -> Result<RootDkgOutput>
where
R: frost::rand_core::RngCore + frost::rand_core::CryptoRng,
{
config.validate()?;
let mut round1_outputs = BTreeMap::new();
let mut round1_public = BTreeMap::new();
for certifier in &config.certifiers {
let output = dkg_round1(config, certifier.frost_identifier, rng)?;
round1_public.insert(certifier.frost_identifier, output.round1_package.clone());
round1_outputs.insert(certifier.frost_identifier, output);
}
let mut round2_outputs = BTreeMap::new();
let mut round2_by_recipient: BTreeMap<u16, BTreeMap<u16, Vec<u8>>> = BTreeMap::new();
for (identifier, round1_output) in &round1_outputs {
let peer_round1 = peer_packages_except(&round1_public, *identifier);
let secret = &round1_output.round1_secret_package;
let round2 = dkg_round2(config, *identifier, secret, peer_round1)?;
for (recipient, package) in &round2.round2_packages {
let recipient_packages = round2_by_recipient.entry(*recipient).or_default();
recipient_packages.insert(*identifier, package.clone());
}
round2_outputs.insert(*identifier, round2);
}
let mut key_packages = BTreeMap::new();
let finish = dkg_finalize_participant;
let first_identifier = config.certifiers[0].frost_identifier;
let output = &round2_outputs[&first_identifier];
let fr1 = peer_packages_except(&round1_public, first_identifier);
let fs = &output.round2_secret_package;
let fr2 = round2_by_recipient[&first_identifier].clone();
let first_participant = finish(config, first_identifier, fs, fr1, fr2)?;
let public_key_package = first_participant.public_key_package;
key_packages.insert(first_identifier, first_participant.key_package);
for (identifier, round2_output) in round2_outputs
.iter()
.filter(|(identifier, _)| **identifier != first_identifier)
{
let identifier = *identifier;
let peer_round1 = peer_packages_except(&round1_public, identifier);
let secret = &round2_output.round2_secret_package;
let round2 = round2_by_recipient[&identifier].clone();
let participant = finish(config, identifier, secret, peer_round1, round2)?;
key_packages.insert(identifier, participant.key_package);
}
Ok(RootDkgOutput {
key_packages,
public_key_package,
})
}
#[cfg(test)]
mod tests {
use exo_core::{Did, Hash256, PublicKey, Timestamp};
use rand::{SeedableRng, rngs::StdRng};
use super::*;
use crate::CertifierContact;
fn test_config() -> GenesisCeremonyConfig {
let certifiers = (1..=13)
.map(|identifier| {
let byte = u8::try_from(identifier).expect("identifier fits");
CertifierContact {
did: Did::new(&format!("did:exo:unit-{identifier:02}")).expect("valid did"),
frost_identifier: identifier,
signing_public_key: PublicKey::from_bytes([byte; 32]),
transport_public_key: [byte; 32],
}
})
.collect();
GenesisCeremonyConfig {
ceremony_id: "unit-root".into(),
network_id: "unit-net".into(),
repo_commit: "d8927686a34bdc28ba36d53938f665685d2c4c04".into(),
constitution_hash: Hash256::digest(b"constitution"),
threshold: 7,
max_signers: 13,
created_at: Timestamp::new(1, 0),
certifiers,
signing_set: (1..=7).collect(),
}
}
#[test]
fn identifier_value_rejects_unrostered_identifier() {
let config = test_config();
let identifier = frost_identifier(14).expect("identifier");
assert!(identifier_value(&config, identifier).is_err());
}
#[test]
fn frost_identifier_rejects_zero_identifier() {
let error = frost_identifier(0).expect_err("zero identifier");
assert!(error.to_string().contains("frost operation failed"));
}
#[test]
fn rostered_identifier_and_encoding_helpers_are_diagnostic() {
let config = test_config();
let identifier = rostered_frost_identifier(&config, 1, "unit").expect("rostered");
assert_eq!(identifier, frost_identifier(1).expect("identifier"));
let error =
rostered_frost_identifier(&config, 14, "unit").expect_err("unrostered certifier");
assert!(
error
.to_string()
.contains("unit certifier 14 is not rostered")
);
let encoding_error = frost_encoding_error("unit failure");
assert!(
encoding_error
.to_string()
.contains("FROST artifact canonical encoding failed")
);
let encoded = serialize_frost(&7u16).expect("serialize");
let decoded: u16 = deserialize_frost(encoded.as_slice()).expect("deserialize");
assert_eq!(decoded, 7);
assert!(deserialize_frost::<u16>(b"not cbor").is_err());
}
#[test]
fn peer_package_filter_retains_every_non_excluded_package() {
let mut packages = BTreeMap::new();
packages.insert(1, b"one".to_vec());
packages.insert(2, b"two".to_vec());
packages.insert(3, b"three".to_vec());
let peers = peer_packages_except(&packages, 2);
assert_eq!(peers.len(), 2);
assert_eq!(peers.get(&1).expect("peer one"), b"one");
assert_eq!(peers.get(&3).expect("peer three"), b"three");
assert!(!peers.contains_key(&2));
}
#[test]
fn round_one_and_complete_dkg_success_paths_are_diagnostic() {
let config = test_config();
let mut rng = StdRng::seed_from_u64(11);
let round1 = dkg_round1(&config, 1, &mut rng).expect("round one");
assert_eq!(round1.frost_identifier, 1);
assert!(!round1.round1_secret_package.is_empty());
assert!(!round1.round1_package.is_empty());
let dkg = run_complete_dkg(&config, &mut rng).expect("complete dkg");
assert_eq!(dkg.key_packages.len(), usize::from(config.max_signers));
assert_eq!(
dkg.public_key_package.verifying_shares.len(),
usize::from(config.max_signers)
);
}
#[test]
fn deserialize_peer_package_helpers_reject_bad_sender_sets() {
let config = test_config();
let participant = frost_identifier(1).expect("participant");
assert!(
deserialize_round1_packages(&config, participant, BTreeMap::new())
.expect("empty round-one helper input")
.is_empty()
);
assert!(
deserialize_round2_packages(&config, participant, BTreeMap::new())
.expect("empty round-two helper input")
.is_empty()
);
let mut nonrostered_round1 = BTreeMap::new();
nonrostered_round1.insert(14, Vec::new());
assert!(deserialize_round1_packages(&config, participant, nonrostered_round1).is_err());
let mut self_round1 = BTreeMap::new();
self_round1.insert(1, Vec::new());
assert!(deserialize_round1_packages(&config, participant, self_round1).is_err());
let mut malformed_round1 = BTreeMap::new();
malformed_round1.insert(2, b"not a round-one package".to_vec());
assert!(deserialize_round1_packages(&config, participant, malformed_round1).is_err());
let mut nonrostered_round2 = BTreeMap::new();
nonrostered_round2.insert(14, Vec::new());
assert!(deserialize_round2_packages(&config, participant, nonrostered_round2).is_err());
let mut self_round2 = BTreeMap::new();
self_round2.insert(1, Vec::new());
assert!(deserialize_round2_packages(&config, participant, self_round2).is_err());
let mut malformed_round2 = BTreeMap::new();
malformed_round2.insert(2, b"not a round-two package".to_vec());
assert!(deserialize_round2_packages(&config, participant, malformed_round2).is_err());
}
#[test]
fn serialize_public_key_package_rejects_missing_verification_share() {
let config = test_config();
let mut rng = StdRng::seed_from_u64(7);
let dkg = run_complete_dkg(&config, &mut rng).expect("dkg");
let public: frost::keys::PublicKeyPackage =
deserialize_frost(dkg.public_key_package.public_key_package.as_slice())
.expect("public package");
let mut changed_config = config;
changed_config.certifiers[0].frost_identifier = 14;
assert!(serialize_public_key_package(&changed_config, &public).is_err());
}
}