use super::primitives::group::{Private, Share};
use crate::{
bls12381::primitives::{
group::Scalar,
sharing::{Mode, ModeVersion, Sharing},
variant::Variant,
},
transcript::{Summary, Transcript},
BatchVerifier, PublicKey, Secret, Signer,
};
use commonware_codec::{Encode, EncodeSize, RangeCfg, Read, ReadExt, Write};
use commonware_math::{
algebra::{Additive, CryptoGroup, Random, Ring as _},
poly::{Interpolator, Poly},
};
use commonware_parallel::{Sequential, Strategy};
#[cfg(feature = "arbitrary")]
use commonware_utils::N3f1;
use commonware_utils::{
ordered::{Map, Quorum, Set},
Faults, Participant, TryCollect, NZU32,
};
use core::num::NonZeroU32;
use rand_core::CryptoRngCore;
use std::{borrow::Cow, collections::BTreeMap, marker::PhantomData};
use thiserror::Error;
const NAMESPACE: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG";
const SIG_ACK: &[u8] = b"ack";
const SIG_LOG: &[u8] = b"log";
const NOISE_PRE_VERIFY: &[u8] = b"pre_verify";
#[derive(Debug, Error)]
pub enum Error {
#[error("missing dealer's share from the previous round")]
MissingDealerShare,
#[error("player is not present in the list of players")]
UnknownPlayer,
#[error("dealer is not present in the previous list of players")]
UnknownDealer(String),
#[error("invalid number of dealers: {0}")]
NumDealers(usize),
#[error("invalid number of players: {0}")]
NumPlayers(usize),
#[error("dkg failed for some reason")]
DkgFailed,
#[error("logs are bound to a different dkg round")]
MismatchedLogs,
#[error("missing player's dealing")]
MissingPlayerDealing,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Output<V: Variant, P> {
summary: Summary,
public: Sharing<V>,
dealers: Set<P>,
players: Set<P>,
revealed: Set<P>,
}
impl<V: Variant, P: Ord> Output<V, P> {
fn share_commitment(&self, player: &P) -> Option<V::Public> {
self.public.partial_public(self.players.index(player)?).ok()
}
pub fn quorum<M: Faults>(&self) -> u32 {
self.players.quorum::<M>()
}
pub const fn public(&self) -> &Sharing<V> {
&self.public
}
pub const fn dealers(&self) -> &Set<P> {
&self.dealers
}
pub const fn players(&self) -> &Set<P> {
&self.players
}
pub const fn revealed(&self) -> &Set<P> {
&self.revealed
}
}
impl<V: Variant, P: PublicKey> EncodeSize for Output<V, P> {
fn encode_size(&self) -> usize {
self.summary.encode_size()
+ self.public.encode_size()
+ self.dealers.encode_size()
+ self.players.encode_size()
+ self.revealed.encode_size()
}
}
impl<V: Variant, P: PublicKey> Write for Output<V, P> {
fn write(&self, buf: &mut impl bytes::BufMut) {
self.summary.write(buf);
self.public.write(buf);
self.dealers.write(buf);
self.players.write(buf);
self.revealed.write(buf);
}
}
impl<V: Variant, P: PublicKey> Read for Output<V, P> {
type Cfg = (NonZeroU32, ModeVersion);
fn read_cfg(
buf: &mut impl bytes::Buf,
(max_participants, max_supported_mode): &Self::Cfg,
) -> Result<Self, commonware_codec::Error> {
let max_participants_usize = max_participants.get() as usize;
Ok(Self {
summary: ReadExt::read(buf)?,
public: Read::read_cfg(buf, &(*max_participants, *max_supported_mode))?,
dealers: Read::read_cfg(buf, &(RangeCfg::new(1..=max_participants_usize), ()))?, players: Read::read_cfg(buf, &(RangeCfg::new(1..=max_participants_usize), ()))?, revealed: Read::read_cfg(buf, &(RangeCfg::new(0..=max_participants_usize), ()))?, })
}
}
#[cfg(feature = "arbitrary")]
impl<P: PublicKey, V: Variant> arbitrary::Arbitrary<'_> for Output<V, P>
where
P: for<'a> arbitrary::Arbitrary<'a> + Ord,
V::Public: for<'a> arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
let summary = u.arbitrary()?;
let public: Sharing<V> = u.arbitrary()?;
let total = public.total().get() as usize;
let num_dealers = u.int_in_range(1..=total * 2)?;
let dealers = Set::try_from(
u.arbitrary_iter::<P>()?
.take(num_dealers)
.collect::<Result<Vec<_>, _>>()?,
)
.map_err(|_| arbitrary::Error::IncorrectFormat)?;
let players = Set::try_from(
u.arbitrary_iter::<P>()?
.take(total)
.collect::<Result<Vec<_>, _>>()?,
)
.map_err(|_| arbitrary::Error::IncorrectFormat)?;
let max_revealed = N3f1::max_faults(total) as usize;
let revealed = Set::from_iter_dedup(
players
.iter()
.filter(|_| u.arbitrary::<bool>().unwrap_or(false))
.take(max_revealed)
.cloned(),
);
Ok(Self {
summary,
public,
dealers,
players,
revealed,
})
}
}
#[derive(Debug, Clone)]
pub struct Info<V: Variant, P: PublicKey> {
summary: Summary,
round: u64,
previous: Option<Output<V, P>>,
mode: Mode,
dealers: Set<P>,
players: Set<P>,
}
impl<V: Variant, P: PublicKey> PartialEq for Info<V, P> {
fn eq(&self, other: &Self) -> bool {
self.summary == other.summary
}
}
impl<V: Variant, P: PublicKey> Info<V, P> {
fn unwrap_or_random_share(
&self,
mut rng: impl CryptoRngCore,
share: Option<Scalar>,
) -> Result<Scalar, Error> {
let out = match (self.previous.as_ref(), share) {
(None, None) => Scalar::random(&mut rng),
(_, Some(x)) => x,
(Some(_), None) => return Err(Error::MissingDealerShare),
};
Ok(out)
}
const fn num_players(&self) -> NonZeroU32 {
NZU32!(self.players.len() as u32)
}
fn degree<M: Faults>(&self) -> u32 {
self.players.quorum::<M>().saturating_sub(1)
}
fn required_commitments<M: Faults>(&self) -> u32 {
let dealer_quorum = self.dealers.quorum::<M>();
let prev_quorum = self
.previous
.as_ref()
.map(Output::quorum::<M>)
.unwrap_or(u32::MIN);
dealer_quorum.max(prev_quorum)
}
fn max_reveals<M: Faults>(&self) -> u32 {
self.players.max_faults::<M>()
}
fn player_index(&self, player: &P) -> Result<Participant, Error> {
self.players.index(player).ok_or(Error::UnknownPlayer)
}
fn dealer_index(&self, dealer: &P) -> Result<Participant, Error> {
self.dealers
.index(dealer)
.ok_or(Error::UnknownDealer(format!("{dealer:?}")))
}
fn player_scalar(&self, player: &P) -> Result<Scalar, Error> {
Ok(self
.mode
.scalar(self.num_players(), self.player_index(player)?)
.expect("player index should be < num_players"))
}
#[must_use]
fn check_dealer_pub_msg<M: Faults>(&self, dealer: &P, pub_msg: &DealerPubMsg<V>) -> bool {
if self.degree::<M>() != pub_msg.commitment.degree_exact() {
return false;
}
if let Some(previous) = self.previous.as_ref() {
let Some(share_commitment) = previous.share_commitment(dealer) else {
return false;
};
if *pub_msg.commitment.constant() != share_commitment {
return false;
}
}
true
}
#[must_use]
fn check_dealer_priv_msg(
&self,
player: &P,
pub_msg: &DealerPubMsg<V>,
priv_msg: &DealerPrivMsg,
) -> bool {
let Ok(scalar) = self.player_scalar(player) else {
return false;
};
let expected = pub_msg.commitment.eval_msm(&scalar, &Sequential);
priv_msg
.share
.expose(|share| expected == V::Public::generator() * share)
}
#[must_use]
fn check_dealer_log<M: Faults, B: BatchVerifier<PublicKey = P>>(
&self,
rng: &mut impl CryptoRngCore,
strategy: &impl Strategy,
round_transcript: &Transcript,
dealer: &P,
log: &DealerLog<V, P>,
) -> bool {
if self.dealer_index(dealer).is_err() {
return false;
}
if !self.check_dealer_pub_msg::<M>(dealer, &log.pub_msg) {
return false;
}
let Some(results_iter) = log.zip_players(&self.players) else {
return false;
};
let ack_summary = transcript_for_ack(round_transcript, dealer, &log.pub_msg).summarize();
let mut ack_batch = B::new();
let mut reveal_count = 0;
let max_reveals = self.max_reveals::<M>();
let mut reveal_eval_points = Vec::new();
let mut reveal_sum = Scalar::zero();
for (player, result) in results_iter {
match result {
AckOrReveal::Ack(ack) => {
if !ack_summary.add_to_batch(&mut ack_batch, player, &ack.sig) {
return false;
}
}
AckOrReveal::Reveal(priv_msg) => {
reveal_count += 1;
if reveal_count > max_reveals {
return false;
}
let Ok(player_scalar) = self.player_scalar(player) else {
return false;
};
let coeff = if reveal_count == 1 {
Scalar::one()
} else {
Scalar::random(&mut *rng)
};
reveal_eval_points.push((coeff.clone(), player_scalar));
priv_msg
.share
.expose(|share| reveal_sum += &(coeff * share));
}
}
}
if !ack_batch.verify(&mut *rng) {
return false;
}
let lhs = log.pub_msg.commitment.lin_comb_eval(
reveal_eval_points
.into_iter()
.map(|(coeff, point)| (coeff, Cow::Owned(point))),
strategy,
);
lhs == V::Public::generator() * &reveal_sum
}
}
impl<V: Variant, P: PublicKey> Info<V, P> {
pub fn new<M: Faults>(
namespace: &[u8],
round: u64,
previous: Option<Output<V, P>>,
mode: Mode,
dealers: Set<P>,
players: Set<P>,
) -> Result<Self, Error> {
let participant_range = 1..u32::MAX as usize;
if !participant_range.contains(&dealers.len()) {
return Err(Error::NumDealers(dealers.len()));
}
if !participant_range.contains(&players.len()) {
return Err(Error::NumPlayers(players.len()));
}
if let Some(previous) = previous.as_ref() {
if let Some(unknown) = dealers
.iter()
.find(|d| previous.players.position(d).is_none())
{
return Err(Error::UnknownDealer(format!("{unknown:?}")));
}
if dealers.len() < previous.quorum::<M>() as usize {
return Err(Error::NumDealers(dealers.len()));
}
}
let summary = {
let mut transcript = Transcript::new(NAMESPACE);
transcript
.commit(namespace)
.commit(round.encode())
.commit(previous.encode())
.commit(dealers.encode())
.commit(players.encode());
if mode != Mode::default() {
transcript.commit([mode as u8].as_slice());
}
transcript.summarize()
};
Ok(Self {
summary,
round,
previous,
mode,
dealers,
players,
})
}
pub const fn round(&self) -> u64 {
self.round
}
}
#[derive(Clone, Debug)]
pub struct DealerPubMsg<V: Variant> {
commitment: Poly<V::Public>,
}
impl<V: Variant> PartialEq for DealerPubMsg<V> {
fn eq(&self, other: &Self) -> bool {
self.commitment == other.commitment
}
}
impl<V: Variant> Eq for DealerPubMsg<V> {}
impl<V: Variant> EncodeSize for DealerPubMsg<V> {
fn encode_size(&self) -> usize {
self.commitment.encode_size()
}
}
impl<V: Variant> Write for DealerPubMsg<V> {
fn write(&self, buf: &mut impl bytes::BufMut) {
self.commitment.write(buf);
}
}
impl<V: Variant> Read for DealerPubMsg<V> {
type Cfg = NonZeroU32;
fn read_cfg(
buf: &mut impl bytes::Buf,
&max_size: &Self::Cfg,
) -> Result<Self, commonware_codec::Error> {
Ok(Self {
commitment: Read::read_cfg(buf, &(RangeCfg::from(NZU32!(1)..=max_size), ()))?,
})
}
}
#[cfg(feature = "arbitrary")]
impl<V: Variant> arbitrary::Arbitrary<'_> for DealerPubMsg<V>
where
V::Public: for<'a> arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
let commitment = u.arbitrary()?;
Ok(Self { commitment })
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DealerPrivMsg {
share: Secret<Scalar>,
}
impl DealerPrivMsg {
pub const fn new(share: Scalar) -> Self {
Self {
share: Secret::new(share),
}
}
}
impl EncodeSize for DealerPrivMsg {
fn encode_size(&self) -> usize {
self.share.expose(|share| share.encode_size())
}
}
impl Write for DealerPrivMsg {
fn write(&self, buf: &mut impl bytes::BufMut) {
self.share.expose(|share| share.write(buf));
}
}
impl Read for DealerPrivMsg {
type Cfg = ();
fn read_cfg(
buf: &mut impl bytes::Buf,
_cfg: &Self::Cfg,
) -> Result<Self, commonware_codec::Error> {
Ok(Self::new(ReadExt::read(buf)?))
}
}
#[cfg(feature = "arbitrary")]
impl arbitrary::Arbitrary<'_> for DealerPrivMsg {
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
Ok(Self::new(u.arbitrary()?))
}
}
#[derive(Clone, Debug)]
pub struct PlayerAck<P: PublicKey> {
sig: P::Signature,
}
impl<P: PublicKey> PartialEq for PlayerAck<P> {
fn eq(&self, other: &Self) -> bool {
self.sig == other.sig
}
}
impl<P: PublicKey> EncodeSize for PlayerAck<P> {
fn encode_size(&self) -> usize {
self.sig.encode_size()
}
}
impl<P: PublicKey> Write for PlayerAck<P> {
fn write(&self, buf: &mut impl bytes::BufMut) {
self.sig.write(buf);
}
}
impl<P: PublicKey> Read for PlayerAck<P> {
type Cfg = ();
fn read_cfg(
buf: &mut impl bytes::Buf,
_cfg: &Self::Cfg,
) -> Result<Self, commonware_codec::Error> {
Ok(Self {
sig: ReadExt::read(buf)?,
})
}
}
#[cfg(feature = "arbitrary")]
impl<P: PublicKey> arbitrary::Arbitrary<'_> for PlayerAck<P>
where
P::Signature: for<'a> arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
let sig = u.arbitrary()?;
Ok(Self { sig })
}
}
#[derive(Clone, PartialEq)]
enum AckOrReveal<P: PublicKey> {
Ack(PlayerAck<P>),
Reveal(DealerPrivMsg),
}
impl<P: PublicKey> AckOrReveal<P> {
const fn is_reveal(&self) -> bool {
matches!(*self, Self::Reveal(_))
}
}
impl<P: PublicKey> std::fmt::Debug for AckOrReveal<P> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Ack(x) => write!(f, "Ack({x:?})"),
Self::Reveal(_) => write!(f, "Reveal(REDACTED)"),
}
}
}
impl<P: PublicKey> EncodeSize for AckOrReveal<P> {
fn encode_size(&self) -> usize {
1 + match self {
Self::Ack(x) => x.encode_size(),
Self::Reveal(x) => x.encode_size(),
}
}
}
impl<P: PublicKey> Write for AckOrReveal<P> {
fn write(&self, buf: &mut impl bytes::BufMut) {
match self {
Self::Ack(x) => {
0u8.write(buf);
x.write(buf);
}
Self::Reveal(x) => {
1u8.write(buf);
x.write(buf);
}
}
}
}
impl<P: PublicKey> Read for AckOrReveal<P> {
type Cfg = ();
fn read_cfg(
buf: &mut impl bytes::Buf,
_cfg: &Self::Cfg,
) -> Result<Self, commonware_codec::Error> {
let tag = u8::read(buf)?;
match tag {
0 => Ok(Self::Ack(ReadExt::read(buf)?)),
1 => Ok(Self::Reveal(ReadExt::read(buf)?)),
x => Err(commonware_codec::Error::InvalidEnum(x)),
}
}
}
#[cfg(feature = "arbitrary")]
impl<P: PublicKey> arbitrary::Arbitrary<'_> for AckOrReveal<P>
where
P: for<'a> arbitrary::Arbitrary<'a>,
P::Signature: for<'a> arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
let choice = u.int_in_range(0..=1)?;
match choice {
0 => {
let ack = u.arbitrary()?;
Ok(Self::Ack(ack))
}
1 => {
let reveal = u.arbitrary()?;
Ok(Self::Reveal(reveal))
}
_ => unreachable!(),
}
}
}
#[derive(Clone, Debug)]
enum DealerResult<P: PublicKey> {
Ok(Map<P, AckOrReveal<P>>),
TooManyReveals,
}
impl<P: PublicKey> PartialEq for DealerResult<P> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Ok(x), Self::Ok(y)) => x == y,
(Self::TooManyReveals, Self::TooManyReveals) => true,
_ => false,
}
}
}
impl<P: PublicKey> EncodeSize for DealerResult<P> {
fn encode_size(&self) -> usize {
1 + match self {
Self::Ok(r) => r.encode_size(),
Self::TooManyReveals => 0,
}
}
}
impl<P: PublicKey> Write for DealerResult<P> {
fn write(&self, buf: &mut impl bytes::BufMut) {
match self {
Self::Ok(r) => {
0u8.write(buf);
r.write(buf);
}
Self::TooManyReveals => {
1u8.write(buf);
}
}
}
}
impl<P: PublicKey> Read for DealerResult<P> {
type Cfg = NonZeroU32;
fn read_cfg(
buf: &mut impl bytes::Buf,
&max_players: &Self::Cfg,
) -> Result<Self, commonware_codec::Error> {
let tag = u8::read(buf)?;
match tag {
0 => Ok(Self::Ok(Read::read_cfg(
buf,
&(RangeCfg::from(0..=max_players.get() as usize), (), ()),
)?)),
1 => Ok(Self::TooManyReveals),
x => Err(commonware_codec::Error::InvalidEnum(x)),
}
}
}
#[cfg(feature = "arbitrary")]
impl<P: PublicKey> arbitrary::Arbitrary<'_> for DealerResult<P>
where
P: for<'a> arbitrary::Arbitrary<'a>,
P::Signature: for<'a> arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
let choice = u.int_in_range(0..=1)?;
match choice {
0 => {
use commonware_utils::TryFromIterator;
use std::collections::HashMap;
let base: HashMap<P, AckOrReveal<P>> = u.arbitrary()?;
let map =
Map::try_from_iter(base).map_err(|_| arbitrary::Error::IncorrectFormat)?;
Ok(Self::Ok(map))
}
1 => Ok(Self::TooManyReveals),
_ => unreachable!(),
}
}
}
#[derive(Clone, Debug)]
pub struct DealerLog<V: Variant, P: PublicKey> {
pub_msg: DealerPubMsg<V>,
results: DealerResult<P>,
}
impl<V: Variant, P: PublicKey> PartialEq for DealerLog<V, P> {
fn eq(&self, other: &Self) -> bool {
self.pub_msg == other.pub_msg && self.results == other.results
}
}
impl<V: Variant, P: PublicKey> EncodeSize for DealerLog<V, P> {
fn encode_size(&self) -> usize {
self.pub_msg.encode_size() + self.results.encode_size()
}
}
impl<V: Variant, P: PublicKey> Write for DealerLog<V, P> {
fn write(&self, buf: &mut impl bytes::BufMut) {
self.pub_msg.write(buf);
self.results.write(buf);
}
}
impl<V: Variant, P: PublicKey> Read for DealerLog<V, P> {
type Cfg = NonZeroU32;
fn read_cfg(
buf: &mut impl bytes::Buf,
cfg: &Self::Cfg,
) -> Result<Self, commonware_codec::Error> {
Ok(Self {
pub_msg: Read::read_cfg(buf, cfg)?,
results: Read::read_cfg(buf, cfg)?,
})
}
}
impl<V: Variant, P: PublicKey> DealerLog<V, P> {
fn get_ack(&self, player: &P) -> Option<&PlayerAck<P>> {
let DealerResult::Ok(results) = &self.results else {
return None;
};
match results.get_value(player) {
Some(AckOrReveal::Ack(ack)) => Some(ack),
_ => None,
}
}
fn get_reveal(&self, player: &P) -> Option<&DealerPrivMsg> {
let DealerResult::Ok(results) = &self.results else {
return None;
};
match results.get_value(player) {
Some(AckOrReveal::Reveal(priv_msg)) => Some(priv_msg),
_ => None,
}
}
fn zip_players<'a, 'b>(
&'a self,
players: &'b Set<P>,
) -> Option<impl Iterator<Item = (&'b P, &'a AckOrReveal<P>)>> {
match &self.results {
DealerResult::TooManyReveals => None,
DealerResult::Ok(results) => {
if results.keys() != players {
return None;
}
Some(players.iter().zip(results.values().iter()))
}
}
}
pub fn summary(&self) -> DealerLogSummary<P> {
match &self.results {
DealerResult::TooManyReveals => DealerLogSummary::TooManyReveals,
DealerResult::Ok(map) => {
let (reveals, acks): (Vec<_>, Vec<_>) =
map.iter_pairs().partition(|(_, a_r)| a_r.is_reveal());
DealerLogSummary::Ok {
acks: acks
.into_iter()
.map(|(p, _)| p.clone())
.try_collect()
.expect("map keys are deduped"),
reveals: reveals
.into_iter()
.map(|(p, _)| p.clone())
.try_collect()
.expect("map keys are deduped"),
}
}
}
}
}
#[derive(Clone, Debug)]
pub enum DealerLogSummary<P> {
TooManyReveals,
Ok { acks: Set<P>, reveals: Set<P> },
}
#[cfg(feature = "arbitrary")]
impl<V: Variant, P: PublicKey> arbitrary::Arbitrary<'_> for DealerLog<V, P>
where
P: for<'a> arbitrary::Arbitrary<'a>,
V::Public: for<'a> arbitrary::Arbitrary<'a>,
P::Signature: for<'a> arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
let pub_msg = u.arbitrary()?;
let results = u.arbitrary()?;
Ok(Self { pub_msg, results })
}
}
#[derive(Clone, Debug)]
pub struct SignedDealerLog<V: Variant, S: Signer> {
dealer: S::PublicKey,
log: DealerLog<V, S::PublicKey>,
sig: S::Signature,
}
impl<V: Variant, S: Signer> PartialEq for SignedDealerLog<V, S> {
fn eq(&self, other: &Self) -> bool {
self.dealer == other.dealer && self.log == other.log && self.sig == other.sig
}
}
impl<V: Variant, S: Signer> SignedDealerLog<V, S> {
fn sign(sk: &S, info: &Info<V, S::PublicKey>, log: DealerLog<V, S::PublicKey>) -> Self {
let sig = transcript_for_log(info, &log).sign(sk);
Self {
dealer: sk.public_key(),
log,
sig,
}
}
#[allow(clippy::type_complexity)]
pub fn check(
self,
info: &Info<V, S::PublicKey>,
) -> Option<(S::PublicKey, DealerLog<V, S::PublicKey>)> {
if !transcript_for_log(info, &self.log).verify(&self.dealer, &self.sig) {
return None;
}
Some((self.dealer, self.log))
}
}
impl<V: Variant, S: Signer> EncodeSize for SignedDealerLog<V, S> {
fn encode_size(&self) -> usize {
self.dealer.encode_size() + self.log.encode_size() + self.sig.encode_size()
}
}
impl<V: Variant, S: Signer> Write for SignedDealerLog<V, S> {
fn write(&self, buf: &mut impl bytes::BufMut) {
self.dealer.write(buf);
self.log.write(buf);
self.sig.write(buf);
}
}
impl<V: Variant, S: Signer> Read for SignedDealerLog<V, S> {
type Cfg = NonZeroU32;
fn read_cfg(
buf: &mut impl bytes::Buf,
cfg: &Self::Cfg,
) -> Result<Self, commonware_codec::Error> {
Ok(Self {
dealer: ReadExt::read(buf)?,
log: Read::read_cfg(buf, cfg)?,
sig: ReadExt::read(buf)?,
})
}
}
#[cfg(feature = "arbitrary")]
impl<V: Variant, S: Signer> arbitrary::Arbitrary<'_> for SignedDealerLog<V, S>
where
S::PublicKey: for<'a> arbitrary::Arbitrary<'a>,
V::Public: for<'a> arbitrary::Arbitrary<'a>,
S::Signature: for<'a> arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
let dealer = u.arbitrary()?;
let log = u.arbitrary()?;
let sig = u.arbitrary()?;
Ok(Self { dealer, log, sig })
}
}
fn transcript_for_round<V: Variant, P: PublicKey>(info: &Info<V, P>) -> Transcript {
Transcript::resume(info.summary)
}
fn transcript_for_ack<V: Variant, P: PublicKey>(
transcript: &Transcript,
dealer: &P,
pub_msg: &DealerPubMsg<V>,
) -> Transcript {
let mut out = transcript.fork(SIG_ACK);
out.commit(dealer.encode());
out.commit(pub_msg.encode());
out
}
fn transcript_for_log<V: Variant, P: PublicKey>(
info: &Info<V, P>,
log: &DealerLog<V, P>,
) -> Transcript {
let mut out = transcript_for_round(info).fork(SIG_LOG);
out.commit(log.encode());
out
}
#[derive(Clone)]
pub struct Logs<V: Variant, P: PublicKey, M: Faults> {
info: Info<V, P>,
logs: BTreeMap<P, DealerLog<V, P>>,
known: BTreeMap<P, bool>,
phantom_m: PhantomData<M>,
}
type SelectedLogs<V, P> = (Info<V, P>, Map<P, DealerLog<V, P>>);
impl<V: Variant, P: PublicKey, M: Faults> Logs<V, P, M> {
pub fn new(info: Info<V, P>) -> Self {
Self {
info,
logs: Default::default(),
known: Default::default(),
phantom_m: Default::default(),
}
}
fn check_dealers<B: BatchVerifier<PublicKey = P>>(
rng: &mut impl CryptoRngCore,
info: &Info<V, P>,
strategy: &impl Strategy,
transcript: &Transcript,
dealers: &[(&P, &DealerLog<V, P>)],
) -> Vec<(P, bool)> {
let checks: Vec<_> = dealers
.iter()
.map(|&(dealer, log)| {
let seed = Summary::random(&mut *rng);
((*dealer).clone(), log, seed)
})
.collect();
strategy.map_collect_vec(checks, |(dealer, log, seed)| {
let mut local_rng = Transcript::resume(seed).noise(NOISE_PRE_VERIFY);
let valid =
info.check_dealer_log::<M, B>(&mut local_rng, strategy, transcript, &dealer, log);
(dealer, valid)
})
}
pub fn record(&mut self, dealer: P, log: DealerLog<V, P>) -> bool {
self.known.remove(&dealer);
self.logs.insert(dealer, log).is_some()
}
pub fn pre_verify<B: BatchVerifier<PublicKey = P>>(
&mut self,
rng: &mut impl CryptoRngCore,
strategy: &impl Strategy,
) {
let required_commitments = self.info.required_commitments::<M>() as usize;
let transcript = transcript_for_round(&self.info);
let mut need = required_commitments;
let mut pending = Vec::new();
let mut iter = self.logs.iter();
while need > 0 {
let Some((dealer, log)) = iter.next() else {
break;
};
match self.known.get(dealer) {
Some(true) => need -= 1,
Some(false) => {}
None => {
need -= 1;
pending.push((dealer, log));
}
}
}
let pending_results =
Self::check_dealers::<B>(rng, &self.info, strategy, &transcript, &pending);
let mut all_pending_valid = true;
for (dealer, is_valid) in pending_results {
self.known.insert(dealer, is_valid);
all_pending_valid &= is_valid;
}
if all_pending_valid {
return;
}
let remaining: Vec<_> = iter
.filter(|(dealer, _)| !self.known.contains_key(*dealer))
.collect();
let results = Self::check_dealers::<B>(rng, &self.info, strategy, &transcript, &remaining);
for (dealer, is_valid) in results {
self.known.insert(dealer, is_valid);
}
}
fn select<B: BatchVerifier<PublicKey = P>>(
mut self,
rng: &mut impl CryptoRngCore,
strategy: &impl Strategy,
) -> Result<SelectedLogs<V, P>, Error> {
self.pre_verify::<B>(rng, strategy);
let required_commitments = self.info.required_commitments::<M>() as usize;
let out: Map<_, _> = self
.logs
.into_iter()
.filter(|(dealer, _)| matches!(self.known.get(dealer), Some(true)))
.take(required_commitments)
.try_collect()
.expect("dealers should be unique");
if out.len() < required_commitments {
return Err(Error::DkgFailed);
}
Ok((self.info, out))
}
}
pub struct Dealer<V: Variant, S: Signer> {
me: S,
info: Info<V, S::PublicKey>,
pub_msg: DealerPubMsg<V>,
results: Map<S::PublicKey, AckOrReveal<S::PublicKey>>,
transcript: Transcript,
}
impl<V: Variant, S: Signer> Dealer<V, S> {
#[allow(clippy::type_complexity)]
pub fn start<M: Faults>(
mut rng: impl CryptoRngCore,
info: Info<V, S::PublicKey>,
me: S,
share: Option<Share>,
) -> Result<(Self, DealerPubMsg<V>, Vec<(S::PublicKey, DealerPrivMsg)>), Error> {
info.dealer_index(&me.public_key())?;
let share = info.unwrap_or_random_share(
&mut rng,
share.map(|x| x.private.expose_unwrap()),
)?;
let my_poly = Poly::new_with_constant(&mut rng, info.degree::<M>(), share);
let priv_msgs = info
.players
.iter()
.map(|pk| {
(
pk.clone(),
DealerPrivMsg::new(my_poly.eval_msm(
&info.player_scalar(pk).expect("player should exist"),
&Sequential,
)),
)
})
.collect::<Vec<_>>();
let results: Map<_, _> = priv_msgs
.clone()
.into_iter()
.map(|(pk, priv_msg)| (pk, AckOrReveal::Reveal(priv_msg)))
.try_collect()
.expect("players are unique");
let commitment = Poly::commit(my_poly);
let pub_msg = DealerPubMsg { commitment };
let transcript = {
let t = transcript_for_round(&info);
transcript_for_ack(&t, &me.public_key(), &pub_msg)
};
let this = Self {
me,
info,
pub_msg: pub_msg.clone(),
results,
transcript,
};
Ok((this, pub_msg, priv_msgs))
}
pub fn receive_player_ack(
&mut self,
player: S::PublicKey,
ack: PlayerAck<S::PublicKey>,
) -> Result<(), Error> {
let res_mut = self
.results
.get_value_mut(&player)
.ok_or(Error::UnknownPlayer)?;
if self.transcript.verify(&player, &ack.sig) {
*res_mut = AckOrReveal::Ack(ack);
}
Ok(())
}
pub fn finalize<M: Faults>(self) -> SignedDealerLog<V, S> {
let reveals = self
.results
.values()
.iter()
.filter(|x| x.is_reveal())
.count() as u32;
let results = if reveals > self.info.max_reveals::<M>() {
DealerResult::TooManyReveals
} else {
DealerResult::Ok(self.results)
};
let log = DealerLog {
pub_msg: self.pub_msg,
results,
};
SignedDealerLog::sign(&self.me, &self.info, log)
}
}
struct ObserveInner<V: Variant, P: PublicKey> {
output: Output<V, P>,
weights: Option<Interpolator<P, Scalar>>,
}
impl<V: Variant, P: PublicKey> ObserveInner<V, P> {
fn reckon<M: Faults>(
info: Info<V, P>,
selected: Map<P, DealerLog<V, P>>,
strategy: &impl Strategy,
) -> Result<Self, Error> {
let max_faults = info.players.max_faults::<M>();
let mut reveal_counts: BTreeMap<P, u32> = BTreeMap::new();
let mut revealed = Vec::new();
for log in selected.values() {
let Some(iter) = log.zip_players(&info.players) else {
continue;
};
for (player, result) in iter {
if !result.is_reveal() {
continue;
}
let count = reveal_counts.entry(player.clone()).or_insert(0);
*count += 1;
if *count == max_faults + 1 {
revealed.push(player.clone());
}
}
}
let revealed: Set<P> = revealed
.into_iter()
.try_collect()
.expect("players are unique");
let dealers: Set<P> = selected
.keys()
.iter()
.cloned()
.try_collect()
.expect("selected dealers are unique");
let (public, weights) = if let Some(previous) = info.previous.as_ref() {
let weights = previous
.public()
.mode()
.subset_interpolator(previous.players(), selected.keys())
.expect("the result of select should produce a valid subset");
let commitments = selected
.into_iter()
.map(|(dealer, log)| (dealer, log.pub_msg.commitment))
.try_collect::<Map<_, _>>()
.expect("Map should have unique keys");
let public = weights
.interpolate(&commitments, strategy)
.expect("select checks that enough points have been provided");
if previous.public().public() != public.constant() {
return Err(Error::DkgFailed);
}
(public, Some(weights))
} else {
let mut public = Poly::zero();
for log in selected.values() {
public += &log.pub_msg.commitment;
}
(public, None)
};
let n = info.players.len() as u32;
let output = Output {
summary: info.summary,
public: Sharing::new(info.mode, NZU32!(n), public),
dealers,
players: info.players,
revealed,
};
Ok(Self { output, weights })
}
}
pub fn observe<V: Variant, P: PublicKey, M: Faults, B: BatchVerifier<PublicKey = P>>(
rng: &mut impl CryptoRngCore,
logs: Logs<V, P, M>,
strategy: &impl Strategy,
) -> Result<Output<V, P>, Error> {
let (info, selected) = logs.select::<B>(rng, strategy)?;
ObserveInner::<V, P>::reckon::<M>(info, selected, strategy).map(|x| x.output)
}
pub struct Player<V: Variant, S: Signer> {
me: S,
me_pub: S::PublicKey,
info: Info<V, S::PublicKey>,
index: Participant,
transcript: Transcript,
view: BTreeMap<S::PublicKey, (DealerPubMsg<V>, DealerPrivMsg)>,
}
impl<V: Variant, S: Signer> Player<V, S> {
pub fn new(info: Info<V, S::PublicKey>, me: S) -> Result<Self, Error> {
let me_pub = me.public_key();
Ok(Self {
index: info.player_index(&me_pub)?,
me,
me_pub,
transcript: transcript_for_round(&info),
info,
view: BTreeMap::new(),
})
}
#[allow(clippy::type_complexity)]
pub fn resume<M: Faults>(
info: Info<V, S::PublicKey>,
me: S,
logs: &BTreeMap<S::PublicKey, DealerLog<V, S::PublicKey>>,
msgs: impl IntoIterator<Item = (S::PublicKey, DealerPubMsg<V>, DealerPrivMsg)>,
) -> Result<(Self, BTreeMap<S::PublicKey, PlayerAck<S::PublicKey>>), Error> {
let mut this = Self::new(info, me)?;
let mut acks = BTreeMap::new();
for (dealer, pub_msg, priv_msg) in msgs {
if let Some(ack) = this.dealer_message::<M>(dealer.clone(), pub_msg, priv_msg) {
acks.insert(dealer, ack);
}
}
if logs.iter().any(|(dealer, log)| {
let Some(ack) = log.get_ack(&this.me_pub) else {
return false;
};
transcript_for_ack(&this.transcript, dealer, &log.pub_msg)
.verify(&this.me_pub, &ack.sig)
&& !acks.contains_key(dealer)
}) {
return Err(Error::MissingPlayerDealing);
}
Ok((this, acks))
}
pub fn dealer_message<M: Faults>(
&mut self,
dealer: S::PublicKey,
pub_msg: DealerPubMsg<V>,
priv_msg: DealerPrivMsg,
) -> Option<PlayerAck<S::PublicKey>> {
if self.view.contains_key(&dealer) {
return None;
}
self.info.dealer_index(&dealer).ok()?;
if !self.info.check_dealer_pub_msg::<M>(&dealer, &pub_msg) {
return None;
}
if !self
.info
.check_dealer_priv_msg(&self.me_pub, &pub_msg, &priv_msg)
{
return None;
}
let sig = transcript_for_ack(&self.transcript, &dealer, &pub_msg).sign(&self.me);
self.view.insert(dealer, (pub_msg, priv_msg));
Some(PlayerAck { sig })
}
pub fn finalize<M: Faults, B: BatchVerifier<PublicKey = S::PublicKey>>(
self,
rng: &mut impl CryptoRngCore,
logs: Logs<V, S::PublicKey, M>,
strategy: &impl Strategy,
) -> Result<(Output<V, S::PublicKey>, Share), Error> {
if logs.info != self.info {
return Err(Error::MismatchedLogs);
}
let (_, selected) = logs.select::<B>(rng, strategy)?;
if selected
.iter_pairs()
.any(|(d, l)| l.get_ack(&self.me_pub).is_some() && !self.view.contains_key(d))
{
return Err(Error::MissingPlayerDealing);
}
let dealings = selected
.iter_pairs()
.map(|(dealer, log)| {
let share = self
.view
.get(dealer)
.map(|(_, priv_msg)| priv_msg.share.clone().expose_unwrap())
.unwrap_or_else(|| {
log.get_reveal(&self.me_pub).map_or_else(
|| {
unreachable!(
"Logs::select didn't check dealer reveal, or we're not a player?"
)
},
|priv_msg| priv_msg.share.clone().expose_unwrap(),
)
});
(dealer.clone(), share)
})
.try_collect::<Map<_, _>>()
.expect("Logs::select produces at most one entry per dealer");
let ObserveInner { output, weights } =
ObserveInner::<V, S::PublicKey>::reckon::<M>(self.info, selected, strategy)?;
let private = weights.map_or_else(
|| {
let mut out = <Scalar as Additive>::zero();
for s in dealings.values() {
out += s;
}
out
},
|weights| {
weights
.interpolate(&dealings, strategy)
.expect("Logs::select ensures that we can recover")
},
);
let share = Share::new(self.index, Private::new(private));
Ok((output, share))
}
}
pub type DealResult<V, P> = Result<(Output<V, P>, Map<P, Share>), Error>;
pub fn deal<V: Variant, P: Clone + Ord, M: Faults>(
mut rng: impl CryptoRngCore,
mode: Mode,
players: Set<P>,
) -> DealResult<V, P> {
if players.is_empty() {
return Err(Error::NumPlayers(0));
}
let n = NZU32!(players.len() as u32);
let t = players.quorum::<M>();
let private = Poly::new(&mut rng, t - 1);
let shares: Map<_, _> = players
.iter()
.enumerate()
.map(|(i, p)| {
let participant = Participant::from_usize(i);
let eval = private.eval_msm(
&mode
.scalar(n, participant)
.expect("player index should be valid"),
&Sequential,
);
let share = Share::new(participant, Private::new(eval));
(p.clone(), share)
})
.try_collect()
.expect("players are unique");
let output = Output {
summary: Summary::random(&mut rng),
public: Sharing::new(mode, n, Poly::commit(private)),
dealers: players.clone(),
players,
revealed: Set::default(),
};
Ok((output, shares))
}
pub fn deal_anonymous<V: Variant, M: Faults>(
rng: impl CryptoRngCore,
mode: Mode,
n: NonZeroU32,
) -> (Sharing<V>, Vec<Share>) {
let players = (0..n.get()).try_collect().unwrap();
let (output, shares) = deal::<V, _, M>(rng, mode, players).unwrap();
(output.public().clone(), shares.values().to_vec())
}
#[cfg(any(feature = "arbitrary", test))]
mod test_plan {
use super::*;
use crate::{
bls12381::primitives::{
ops::{self, threshold},
variant::Variant,
},
ed25519, PublicKey,
};
use anyhow::anyhow;
use bytes::BytesMut;
use commonware_utils::{Faults, N3f1, TryCollect};
use core::num::NonZeroI32;
use rand::{rngs::StdRng, SeedableRng as _};
use std::collections::BTreeSet;
fn apply_mask(bytes: &mut BytesMut, mask: &[u8]) -> bool {
let mut modified = false;
for (l, &r) in bytes.iter_mut().zip(mask.iter()) {
modified |= r != 0;
*l ^= r;
}
modified
}
#[derive(Clone, Default, Debug)]
pub struct Masks {
pub info_summary: Vec<u8>,
pub dealer: Vec<u8>,
pub pub_msg: Vec<u8>,
pub log: Vec<u8>,
}
impl Masks {
fn modifies_player_ack(&self) -> bool {
self.info_summary.iter().any(|&b| b != 0)
|| self.dealer.iter().any(|&b| b != 0)
|| self.pub_msg.iter().any(|&b| b != 0)
}
fn transcript_for_round<V: Variant, P: PublicKey>(
&self,
info: &Info<V, P>,
) -> anyhow::Result<(bool, Transcript)> {
let mut summary_bs = info.summary.encode_mut();
let modified = apply_mask(&mut summary_bs, &self.info_summary);
let summary = Summary::read(&mut summary_bs)?;
Ok((modified, Transcript::resume(summary)))
}
fn transcript_for_player_ack<V: Variant, P: PublicKey>(
&self,
info: &Info<V, P>,
dealer: &P,
pub_msg: &DealerPubMsg<V>,
) -> anyhow::Result<(bool, Transcript)> {
let (mut modified, transcript) = self.transcript_for_round(info)?;
let mut transcript = transcript.fork(SIG_ACK);
let mut dealer_bs = dealer.encode_mut();
modified |= apply_mask(&mut dealer_bs, &self.dealer);
transcript.commit(&mut dealer_bs);
let mut pub_msg_bs = pub_msg.encode_mut();
modified |= apply_mask(&mut pub_msg_bs, &self.pub_msg);
transcript.commit(&mut pub_msg_bs);
Ok((modified, transcript))
}
fn transcript_for_signed_dealer_log<V: Variant, P: PublicKey>(
&self,
info: &Info<V, P>,
log: &DealerLog<V, P>,
) -> anyhow::Result<(bool, Transcript)> {
let (mut modified, transcript) = self.transcript_for_round(info)?;
let mut transcript = transcript.fork(SIG_LOG);
let mut log_bs = log.encode_mut();
modified |= apply_mask(&mut log_bs, &self.log);
transcript.commit(&mut log_bs);
Ok((modified, transcript))
}
}
#[derive(Debug, Default)]
pub struct Round {
dealers: Vec<u32>,
players: Vec<u32>,
crash_resume_players: BTreeSet<(u32, u32)>,
resume_missing_dealer_msg_fails: BTreeSet<(u32, u32)>,
finalize_missing_dealer_msg_fails: BTreeSet<u32>,
no_acks: BTreeSet<(u32, u32)>,
bad_shares: BTreeSet<(u32, u32)>,
bad_player_sigs: BTreeMap<(u32, u32), Masks>,
bad_reveals: BTreeSet<(u32, u32)>,
bad_dealer_sigs: BTreeMap<u32, Masks>,
replace_shares: BTreeSet<u32>,
shift_degrees: BTreeMap<u32, NonZeroI32>,
}
impl Round {
pub fn new(dealers: Vec<u32>, players: Vec<u32>) -> Self {
Self {
dealers,
players,
..Default::default()
}
}
pub fn no_ack(mut self, dealer: u32, player: u32) -> Self {
self.no_acks.insert((dealer, player));
self
}
pub fn crash_resume_player(mut self, after_dealer: u32, player: u32) -> Self {
self.crash_resume_players.insert((after_dealer, player));
self
}
pub fn resume_missing_dealer_msg_fails(
mut self,
after_dealer: u32,
missing_dealer: u32,
) -> Self {
self.resume_missing_dealer_msg_fails
.insert((after_dealer, missing_dealer));
self
}
pub fn finalize_missing_dealer_msg_fails(mut self, player: u32) -> Self {
self.finalize_missing_dealer_msg_fails.insert(player);
self
}
pub fn bad_share(mut self, dealer: u32, player: u32) -> Self {
self.bad_shares.insert((dealer, player));
self
}
pub fn bad_player_sig(mut self, dealer: u32, player: u32, masks: Masks) -> Self {
self.bad_player_sigs.insert((dealer, player), masks);
self
}
pub fn bad_reveal(mut self, dealer: u32, player: u32) -> Self {
self.bad_reveals.insert((dealer, player));
self
}
pub fn bad_dealer_sig(mut self, dealer: u32, masks: Masks) -> Self {
self.bad_dealer_sigs.insert(dealer, masks);
self
}
pub fn replace_share(mut self, dealer: u32) -> Self {
self.replace_shares.insert(dealer);
self
}
pub fn shift_degree(mut self, dealer: u32, shift: NonZeroI32) -> Self {
self.shift_degrees.insert(dealer, shift);
self
}
pub fn validate(
&self,
num_participants: u32,
previous_players: Option<&[u32]>,
) -> anyhow::Result<()> {
if self.dealers.is_empty() {
return Err(anyhow!("dealers is empty"));
}
if self.players.is_empty() {
return Err(anyhow!("players is empty"));
}
for &d in &self.dealers {
if d >= num_participants {
return Err(anyhow!("dealer {d} out of range [1, {num_participants}]"));
}
}
for &p in &self.players {
if p >= num_participants {
return Err(anyhow!("player {p} out of range [1, {num_participants}]"));
}
}
for &(after_dealer, player) in &self.crash_resume_players {
if !self.dealers.contains(&after_dealer) {
return Err(anyhow!("crash_resume dealer {after_dealer} not in round"));
}
if !self.players.contains(&player) {
return Err(anyhow!("crash_resume player {player} not in round"));
}
}
let dealer_positions: BTreeMap<u32, usize> = self
.dealers
.iter()
.enumerate()
.map(|(idx, &dealer)| (dealer, idx))
.collect();
let previous_successful_round = previous_players.is_some();
for &(after_dealer, missing_dealer) in &self.resume_missing_dealer_msg_fails {
if !self.dealers.contains(&after_dealer) {
return Err(anyhow!("resume_missing dealer {after_dealer} not in round"));
}
if !self.dealers.contains(&missing_dealer) {
return Err(anyhow!(
"resume_missing missing_dealer {missing_dealer} not in round"
));
}
let after_pos = dealer_positions[&after_dealer];
let missing_pos = dealer_positions[&missing_dealer];
if missing_pos > after_pos {
return Err(anyhow!(
"resume_missing missing_dealer {missing_dealer} appears after {after_dealer}"
));
}
if self.bad(previous_successful_round, missing_dealer) {
return Err(anyhow!(
"resume_missing_dealer_msg_fails requires dealer {missing_dealer} to be good"
));
}
let any_valid_ack = self.players.iter().any(|&player| {
let ack_corrupted = self.no_acks.contains(&(missing_dealer, player))
|| self.bad_shares.contains(&(missing_dealer, player))
|| self
.bad_player_sigs
.get(&(missing_dealer, player))
.is_some_and(Masks::modifies_player_ack);
!ack_corrupted
});
if !any_valid_ack {
return Err(anyhow!(
"resume_missing_dealer_msg_fails requires dealer {missing_dealer} to ack at least one player"
));
}
}
for &player in &self.finalize_missing_dealer_msg_fails {
if !self.players.contains(&player) {
return Err(anyhow!("finalize_missing player {player} not in round"));
}
}
if let Some(prev_players) = previous_players {
for &d in &self.dealers {
if !prev_players.contains(&d) {
return Err(anyhow!("dealer {d} was not a player in previous round"));
}
}
let required = N3f1::quorum(prev_players.len());
if (self.dealers.len() as u32) < required {
return Err(anyhow!(
"not enough dealers: have {}, need {} (quorum of {} previous players)",
self.dealers.len(),
required,
prev_players.len()
));
}
}
Ok(())
}
fn bad(&self, previous_successful_round: bool, dealer: u32) -> bool {
if self.replace_shares.contains(&dealer) && previous_successful_round {
return true;
}
if let Some(shift) = self.shift_degrees.get(&dealer) {
let degree = N3f1::quorum(self.players.len()) as i32 - 1;
if (degree + shift.get()).max(0) != degree {
return true;
}
}
if self.bad_reveals.iter().any(|&(d, _)| d == dealer) {
return true;
}
let revealed_players = self
.bad_shares
.iter()
.copied()
.chain(self.no_acks.iter().copied())
.filter_map(|(d, p)| if d == dealer { Some(p) } else { None })
.collect::<BTreeSet<_>>();
revealed_players.len() as u32 > N3f1::max_faults(self.players.len())
}
fn expect_failure(&self, previous_successful_round: Option<u32>) -> bool {
let good_dealer_count = self
.dealers
.iter()
.filter(|&&d| !self.bad(previous_successful_round.is_some(), d))
.count();
let required = previous_successful_round
.map(N3f1::quorum)
.unwrap_or_default()
.max(N3f1::quorum(self.dealers.len())) as usize;
good_dealer_count < required
}
}
#[derive(Debug)]
pub struct Plan {
num_participants: NonZeroU32,
rounds: Vec<Round>,
}
impl Plan {
pub const fn new(num_participants: NonZeroU32) -> Self {
Self {
num_participants,
rounds: Vec::new(),
}
}
pub fn with(mut self, round: Round) -> Self {
self.rounds.push(round);
self
}
pub(crate) fn validate(&self) -> anyhow::Result<()> {
let mut last_successful_players: Option<Vec<u32>> = None;
for round in &self.rounds {
round.validate(
self.num_participants.get(),
last_successful_players.as_deref(),
)?;
if !round.expect_failure(last_successful_players.as_ref().map(|x| x.len() as u32)) {
last_successful_players = Some(round.players.clone());
}
}
Ok(())
}
pub fn run<V: Variant>(self, seed: u64) -> anyhow::Result<()> {
self.validate()?;
let mut rng = StdRng::seed_from_u64(seed);
let keys = (0..self.num_participants.get())
.map(|_| ed25519::PrivateKey::random(&mut rng))
.collect::<Vec<_>>();
let pk_to_key_idx: BTreeMap<ed25519::PublicKey, u32> = keys
.iter()
.enumerate()
.map(|(i, k)| (k.public_key(), i as u32))
.collect();
let max_shift = self
.rounds
.iter()
.flat_map(|r| r.shift_degrees.values())
.map(|s| s.get())
.max()
.unwrap_or(0)
.max(0) as u32;
let max_read_size =
NonZeroU32::new(self.num_participants.get() + max_shift).expect("non-zero");
let mut previous_output: Option<Output<V, ed25519::PublicKey>> = None;
let mut shares: BTreeMap<ed25519::PublicKey, Share> = BTreeMap::new();
let mut threshold_public_key: Option<V::Public> = None;
for (i_round, round) in self.rounds.into_iter().enumerate() {
let previous_successful_round =
previous_output.as_ref().map(|o| o.players.len() as u32);
let dealer_set = round
.dealers
.iter()
.map(|&i| keys[i as usize].public_key())
.try_collect::<Set<_>>()
.unwrap();
let player_set: Set<ed25519::PublicKey> = round
.players
.iter()
.map(|&i| keys[i as usize].public_key())
.try_collect()
.unwrap();
let info = Info::new::<N3f1>(
b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG_TEST",
i_round as u64,
previous_output.clone(),
Default::default(),
dealer_set.clone(),
player_set.clone(),
)?;
let mut players: Map<_, _> = round
.players
.iter()
.map(|&i| {
let sk = keys[i as usize].clone();
let pk = sk.public_key();
let player = Player::new(info.clone(), sk)?;
Ok((pk, player))
})
.collect::<anyhow::Result<Vec<_>>>()?
.try_into()
.unwrap();
let mut acked_dealings: BTreeMap<
ed25519::PublicKey,
Vec<(ed25519::PublicKey, DealerPubMsg<V>, DealerPrivMsg)>,
> = player_set
.iter()
.cloned()
.map(|pk| (pk, Vec::new()))
.collect();
let mut crash_resume_by_dealer: BTreeMap<u32, Vec<u32>> = BTreeMap::new();
for &(after_dealer, player) in &round.crash_resume_players {
crash_resume_by_dealer
.entry(after_dealer)
.or_default()
.push(player);
}
let mut resume_missing_msg_by_dealer: BTreeMap<u32, Vec<u32>> = BTreeMap::new();
for &(after_dealer, missing_dealer) in &round.resume_missing_dealer_msg_fails {
resume_missing_msg_by_dealer
.entry(after_dealer)
.or_default()
.push(missing_dealer);
}
let mut dealer_logs = BTreeMap::new();
for &i_dealer in &round.dealers {
let sk = keys[i_dealer as usize].clone();
let pk = sk.public_key();
let share = match (shares.get(&pk), round.replace_shares.contains(&i_dealer)) {
(None, _) => None,
(Some(s), false) => Some(s.clone()),
(Some(_), true) => Some(Share::new(
Participant::new(i_dealer),
Private::random(&mut rng),
)),
};
let (mut dealer, pub_msg, mut priv_msgs) =
if let Some(shift) = round.shift_degrees.get(&i_dealer) {
let degree = u32::try_from(info.degree::<N3f1>() as i32 + shift.get())
.unwrap_or_default();
let share = info
.unwrap_or_random_share(
&mut rng,
share.map(|s| s.private.expose_unwrap()),
)
.expect("Failed to generate dealer share");
let my_poly = Poly::new_with_constant(&mut rng, degree, share);
let priv_msgs = info
.players
.iter()
.map(|pk| {
(
pk.clone(),
DealerPrivMsg::new(my_poly.eval_msm(
&info.player_scalar(pk).expect("player should exist"),
&Sequential,
)),
)
})
.collect::<Vec<_>>();
let results: Map<_, _> = priv_msgs
.iter()
.map(|(pk, pm)| (pk.clone(), AckOrReveal::Reveal(pm.clone())))
.try_collect()
.unwrap();
let commitment = Poly::commit(my_poly);
let pub_msg = DealerPubMsg { commitment };
let transcript = {
let t = transcript_for_round(&info);
transcript_for_ack(&t, &pk, &pub_msg)
};
let dealer = Dealer {
me: sk.clone(),
info: info.clone(),
pub_msg: pub_msg.clone(),
results,
transcript,
};
(dealer, pub_msg, priv_msgs)
} else {
Dealer::start::<N3f1>(&mut rng, info.clone(), sk.clone(), share)?
};
for (player, priv_msg) in &mut priv_msgs {
let player_key_idx = pk_to_key_idx[player];
if round.bad_shares.contains(&(i_dealer, player_key_idx)) {
*priv_msg = DealerPrivMsg::new(Scalar::random(&mut rng));
}
}
assert_eq!(priv_msgs.len(), players.len());
let mut num_reveals = players.len() as u32;
for (player_pk, priv_msg) in priv_msgs {
assert_eq!(priv_msg, ReadExt::read(&mut priv_msg.encode())?);
let i_player = players
.index(&player_pk)
.ok_or_else(|| anyhow!("unknown player: {:?}", &player_pk))?;
let player_key_idx = pk_to_key_idx[&player_pk];
let player = &mut players.values_mut()[usize::from(i_player)];
let persisted = priv_msg.clone();
let ack =
player.dealer_message::<N3f1>(pk.clone(), pub_msg.clone(), priv_msg);
assert_eq!(ack, ReadExt::read(&mut ack.encode())?);
if let Some(ack) = ack {
acked_dealings
.get_mut(&player_pk)
.expect("player should be present")
.push((pk.clone(), pub_msg.clone(), persisted));
let masks = round
.bad_player_sigs
.get(&(i_dealer, player_key_idx))
.cloned()
.unwrap_or_default();
let (modified, transcript) =
masks.transcript_for_player_ack(&info, &pk, &pub_msg)?;
assert_eq!(transcript.verify(&player_pk, &ack.sig), !modified);
if !round.no_acks.contains(&(i_dealer, player_key_idx)) {
dealer.receive_player_ack(player_pk, ack)?;
num_reveals -= 1;
}
} else {
assert!(
round.bad_shares.contains(&(i_dealer, player_key_idx))
|| round.bad(previous_successful_round.is_some(), i_dealer)
);
}
}
let signed_log = dealer.finalize::<N3f1>();
assert_eq!(
signed_log,
Read::read_cfg(&mut signed_log.encode(), &max_read_size)?
);
let masks = round
.bad_dealer_sigs
.get(&i_dealer)
.cloned()
.unwrap_or_default();
let (modified, transcript) =
masks.transcript_for_signed_dealer_log(&info, &signed_log.log)?;
assert_eq!(transcript.verify(&pk, &signed_log.sig), !modified);
let (found_pk, mut log) = signed_log
.check(&info)
.ok_or_else(|| anyhow!("signed log should verify"))?;
assert_eq!(pk, found_pk);
match &mut log.results {
DealerResult::TooManyReveals => {
assert!(num_reveals > info.max_reveals::<N3f1>());
}
DealerResult::Ok(results) => {
assert_eq!(results.len(), players.len());
for &i_player in &round.players {
if !round.bad_reveals.contains(&(i_dealer, i_player)) {
continue;
}
let player_pk = keys[i_player as usize].public_key();
*results
.get_value_mut(&player_pk)
.ok_or_else(|| anyhow!("unknown player: {:?}", &player_pk))? =
AckOrReveal::Reveal(DealerPrivMsg::new(Scalar::random(
&mut rng,
)));
}
}
}
dealer_logs.insert(pk, log);
for &missing_dealer in resume_missing_msg_by_dealer
.get(&i_dealer)
.into_iter()
.flatten()
{
assert!(
!round.bad(previous_successful_round.is_some(), missing_dealer),
"resume_missing_dealer_msg_fails requires dealer {missing_dealer} to be good"
);
let missing_pk = keys[missing_dealer as usize].public_key();
let missing_log = dealer_logs
.get(&missing_pk)
.unwrap_or_else(|| panic!("missing dealer log for {:?}", &missing_pk));
for &i_player in &round.players {
let player_pk = keys[i_player as usize].public_key();
let was_acked = missing_log.get_ack(&player_pk).is_some();
let replay = acked_dealings
.get(&player_pk)
.cloned()
.expect("player should be present");
let replay_without = replay
.into_iter()
.filter(|(dealer, _, _)| dealer != &missing_pk);
let player_sk = keys[i_player as usize].clone();
let resumed = Player::resume::<N3f1>(
info.clone(),
player_sk,
&dealer_logs,
replay_without,
);
if was_acked {
assert!(
matches!(resumed, Err(Error::MissingPlayerDealing)),
"resume without dealer {missing_dealer} message should report MissingPlayerDealing for player {i_player}"
);
} else {
assert!(
resumed.is_ok(),
"resume without dealer {missing_dealer} message should succeed for unacked player {i_player}"
);
}
}
}
for &i_player in crash_resume_by_dealer.get(&i_dealer).into_iter().flatten() {
let player_pk = keys[i_player as usize].public_key();
let player_sk = keys[i_player as usize].clone();
let replay = acked_dealings
.get(&player_pk)
.cloned()
.expect("player should be present");
let (resumed, _) =
Player::resume::<N3f1>(info.clone(), player_sk, &dealer_logs, replay)
.expect("player resume perturbation should succeed");
*players
.get_value_mut(&player_pk)
.expect("player should be present") = resumed;
}
}
let mut logs = Logs::<_, _, N3f1>::new(info.clone());
for (dealer, log) in &dealer_logs {
logs.record(dealer.clone(), log.clone());
}
let selection = logs.clone().select::<ed25519::Batch>(&mut rng, &Sequential);
if let Ok(ref selection) = selection {
let good_pks = selection
.1
.iter_pairs()
.map(|(pk, _)| pk.clone())
.collect::<BTreeSet<_>>();
for &i_dealer in &round.dealers {
if round.bad(previous_successful_round.is_some(), i_dealer) {
assert!(!good_pks.contains(&keys[i_dealer as usize].public_key()));
}
}
}
let observe_result =
observe::<_, _, N3f1, ed25519::Batch>(&mut rng, logs.clone(), &Sequential);
if round.expect_failure(previous_successful_round) {
assert!(
observe_result.is_err(),
"Round {i_round} should have failed but succeeded",
);
continue;
}
let observer_output = observe_result?;
let selection = selection.expect("select should succeed if observe succeeded");
let required_commitments = info.required_commitments::<N3f1>() as usize;
let expected_dealers: Set<ed25519::PublicKey> = dealer_set
.iter()
.filter(|pk| {
let i = keys.iter().position(|k| &k.public_key() == *pk).unwrap() as u32;
!round.bad(previous_successful_round.is_some(), i)
})
.take(required_commitments)
.cloned()
.try_collect()
.expect("dealers are unique");
let expected_dealer_indices: BTreeSet<u32> = expected_dealers
.iter()
.filter_map(|pk| {
keys.iter()
.position(|k| &k.public_key() == pk)
.map(|i| i as u32)
})
.collect();
assert_eq!(
observer_output.dealers(),
&expected_dealers,
"Output dealers should match expected good dealers"
);
let selected_dealers: BTreeSet<u32> = selection
.1
.keys()
.iter()
.filter_map(|pk| {
keys.iter()
.position(|k| &k.public_key() == pk)
.map(|i| i as u32)
})
.collect();
assert_eq!(
selected_dealers, expected_dealer_indices,
"Selection should match expected dealers"
);
let selected_players: Set<ed25519::PublicKey> = round
.players
.iter()
.map(|&i| keys[i as usize].public_key())
.try_collect()
.expect("players are unique");
for &i_player in &round.finalize_missing_dealer_msg_fails {
let player_pk = keys[i_player as usize].public_key();
let player_sk = keys[i_player as usize].clone();
let mut tested = 0u32;
for &dealer_idx in &selected_dealers {
if round.bad(previous_successful_round.is_some(), dealer_idx) {
continue;
}
let dealer_pk = keys[dealer_idx as usize].public_key();
let dealer_log = dealer_logs
.get(&dealer_pk)
.unwrap_or_else(|| panic!("missing dealer log for {:?}", &dealer_pk));
if dealer_log.get_ack(&player_pk).is_none() {
continue;
}
let replay = acked_dealings
.get(&player_pk)
.cloned()
.expect("player should be present");
let replay_without = replay
.into_iter()
.filter(|(dealer, _, _)| dealer != &dealer_pk);
let resume_logs: BTreeMap<_, _> = dealer_logs
.iter()
.filter(|(dealer, _)| *dealer != &dealer_pk)
.map(|(dealer, log)| (dealer.clone(), log.clone()))
.collect();
let (resumed, _) = Player::resume::<N3f1>(
info.clone(),
player_sk.clone(),
&resume_logs,
replay_without,
)
.expect("resume should succeed with stale logs");
let finalize_res = resumed.finalize::<N3f1, ed25519::Batch>(
&mut rng,
logs.clone(),
&Sequential,
);
assert!(
matches!(finalize_res, Err(Error::MissingPlayerDealing)),
"finalize without dealer {dealer_idx} message should return MissingPlayerDealing for player {i_player}"
);
tested += 1;
}
assert!(
tested > 0,
"finalize_missing_dealer_msg_fails for player {i_player} tested no dealers"
);
}
let mut expected_reveals: BTreeMap<ed25519::PublicKey, u32> = BTreeMap::new();
for &(dealer_idx, player_key_idx) in round.no_acks.union(&round.bad_shares) {
if !selected_dealers.contains(&dealer_idx) {
continue;
}
let pk = keys[player_key_idx as usize].public_key();
if selected_players.position(&pk).is_none() {
continue;
}
*expected_reveals.entry(pk).or_insert(0) += 1;
}
let max_faults = selected_players.max_faults::<N3f1>();
for player in player_set.iter() {
let expected = expected_reveals.get(player).copied().unwrap_or(0) > max_faults;
let actual = observer_output.revealed().position(player).is_some();
assert_eq!(expected, actual, "Unexpected outcome for player {player:?} (expected={expected}, actual={actual})");
}
for (player_pk, player) in players.into_iter() {
let (player_output, share) = player
.finalize::<N3f1, ed25519::Batch>(&mut rng, logs.clone(), &Sequential)
.expect("Player finalize should succeed");
assert_eq!(
player_output, observer_output,
"Player output should match observer output"
);
let expected_public = observer_output
.public
.partial_public(share.index)
.expect("share index should be valid");
let actual_public = share.public::<V>();
assert_eq!(
expected_public, actual_public,
"Share should match public polynomial"
);
shares.insert(player_pk.clone(), share);
}
let current_public = *observer_output.public().public();
match threshold_public_key {
None => threshold_public_key = Some(current_public),
Some(tpk) => {
assert_eq!(
tpk, current_public,
"Public key should remain constant across reshares"
);
}
}
let test_message = format!("test message round {i_round}").into_bytes();
let namespace = b"test";
let mut partial_sigs = Vec::new();
for &i_player in &round.players {
let share = &shares[&keys[i_player as usize].public_key()];
let partial_sig = threshold::sign_message::<V>(share, namespace, &test_message);
threshold::verify_message::<V>(
&observer_output.public,
namespace,
&test_message,
&partial_sig,
)
.expect("Partial signature verification should succeed");
partial_sigs.push(partial_sig);
}
let threshold = observer_output.quorum::<N3f1>();
let threshold_sig = threshold::recover::<V, _, N3f1>(
&observer_output.public,
&partial_sigs[0..threshold as usize],
&Sequential,
)
.expect("Should recover threshold signature");
ops::verify_message::<V>(
threshold_public_key.as_ref().unwrap(),
namespace,
&test_message,
&threshold_sig,
)
.expect("Threshold signature verification should succeed");
previous_output = Some(observer_output);
}
Ok(())
}
}
#[cfg(feature = "arbitrary")]
mod impl_arbitrary {
use super::*;
use arbitrary::{Arbitrary, Unstructured};
use core::ops::ControlFlow;
const MAX_NUM_PARTICIPANTS: u32 = 20;
const MAX_ROUNDS: u32 = 10;
fn arbitrary_masks<'a>(u: &mut Unstructured<'a>) -> arbitrary::Result<Masks> {
Ok(Masks {
info_summary: Arbitrary::arbitrary(u)?,
dealer: Arbitrary::arbitrary(u)?,
pub_msg: Arbitrary::arbitrary(u)?,
log: Arbitrary::arbitrary(u)?,
})
}
fn pick<'a, T>(
u: &mut Unstructured<'a>,
num: usize,
mut data: Vec<T>,
) -> arbitrary::Result<Vec<T>> {
let len = data.len();
let num = num.min(len);
for start in 0..num {
data.swap(start, u.int_in_range(start..=len - 1)?);
}
data.truncate(num);
Ok(data)
}
fn arbitrary_round<'a>(
u: &mut Unstructured<'a>,
num_participants: u32,
last_successful_players: Option<&Set<u32>>,
) -> arbitrary::Result<Round> {
let dealers = if let Some(players) = last_successful_players {
let to_pick = u.int_in_range(players.quorum::<N3f1>() as usize..=players.len())?;
pick(u, to_pick, players.into_iter().copied().collect())?
} else {
let to_pick = u.int_in_range(1..=num_participants as usize)?;
pick(u, to_pick, (0..num_participants).collect())?
};
let players = {
let to_pick = u.int_in_range(1..=num_participants as usize)?;
pick(u, to_pick, (0..num_participants).collect())?
};
let pairs = dealers
.iter()
.flat_map(|d| players.iter().map(|p| (*d, *p)))
.collect::<Vec<_>>();
let pick_pair_set = |u: &mut Unstructured<'a>| {
let num = u.int_in_range(0..=pairs.len())?;
if num == 0 {
return Ok(BTreeSet::new());
}
Ok(pick(u, num, pairs.clone())?.into_iter().collect())
};
let pick_dealer_set = |u: &mut Unstructured<'a>| {
let num = u.int_in_range(0..=dealers.len())?;
if num == 0 {
return Ok(BTreeSet::new());
}
Ok(pick(u, num, dealers.clone())?.into_iter().collect())
};
let round = Round {
crash_resume_players: BTreeSet::new(),
resume_missing_dealer_msg_fails: BTreeSet::new(),
finalize_missing_dealer_msg_fails: BTreeSet::new(),
no_acks: pick_pair_set(u)?,
bad_shares: pick_pair_set(u)?,
bad_player_sigs: {
let indices = pick_pair_set(u)?;
indices
.into_iter()
.map(|k| Ok((k, arbitrary_masks(u)?)))
.collect::<arbitrary::Result<_>>()?
},
bad_reveals: pick_pair_set(u)?,
bad_dealer_sigs: {
let indices = pick_dealer_set(u)?;
indices
.into_iter()
.map(|k| Ok((k, arbitrary_masks(u)?)))
.collect::<arbitrary::Result<_>>()?
},
replace_shares: pick_dealer_set(u)?,
shift_degrees: {
let indices = pick_dealer_set(u)?;
indices
.into_iter()
.map(|k| {
let expected = N3f1::quorum(players.len()) as i32 - 1;
let shift = u.int_in_range(1..=expected.max(1))?;
let shift = if bool::arbitrary(u)? { -shift } else { shift };
Ok((k, NonZeroI32::new(shift).expect("checked to not be zero")))
})
.collect::<arbitrary::Result<_>>()?
},
dealers,
players,
};
Ok(round)
}
impl<'a> Arbitrary<'a> for Plan {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let num_participants = u.int_in_range(1..=MAX_NUM_PARTICIPANTS)?;
let mut rounds = Vec::new();
let mut last_successful_players: Option<Set<u32>> = None;
u.arbitrary_loop(None, Some(MAX_ROUNDS), |u| {
let round =
arbitrary_round(u, num_participants, last_successful_players.as_ref())?;
if !round
.expect_failure(last_successful_players.as_ref().map(|x| x.len() as u32))
{
last_successful_players =
Some(round.players.iter().copied().try_collect().unwrap());
}
rounds.push(round);
Ok(ControlFlow::Continue(()))
})?;
let plan = Self {
num_participants: NZU32!(num_participants),
rounds,
};
plan.validate()
.map_err(|_| arbitrary::Error::IncorrectFormat)?;
Ok(plan)
}
}
}
}
#[cfg(feature = "arbitrary")]
pub use test_plan::Plan as FuzzPlan;
#[cfg(test)]
mod test {
use super::{test_plan::*, *};
use crate::{bls12381::primitives::variant::MinPk, ed25519};
use anyhow::anyhow;
use arbitrary::{Arbitrary, Unstructured};
use commonware_invariants::minifuzz;
use commonware_math::algebra::Random;
use commonware_utils::{test_rng, test_rng_seeded, Faults, N3f1};
use core::num::NonZeroI32;
const PRE_VERIFY_DEALERS: usize = 8;
type PreVerifyLog = DealerLog<MinPk, ed25519::PublicKey>;
type PreVerifyLogs = Logs<MinPk, ed25519::PublicKey, QuorumTwo>;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct QuorumTwo;
impl Faults for QuorumTwo {
fn max_faults(n: impl num_traits::ToPrimitive) -> u32 {
let n = n
.to_u32()
.expect("n must be a non-negative integer that fits in u32");
assert!(n >= 2, "n must be at least 2");
n - 2
}
}
struct PreVerifyDealer {
key: ed25519::PublicKey,
valid: PreVerifyLog,
invalid: PreVerifyLog,
}
struct PreVerifyFixture {
info: Info<MinPk, ed25519::PublicKey>,
wrong_info: Info<MinPk, ed25519::PublicKey>,
dealers: Vec<PreVerifyDealer>,
}
impl PreVerifyFixture {
fn new() -> Self {
fn pre_verify_test_keys() -> Vec<ed25519::PrivateKey> {
(0..PRE_VERIFY_DEALERS as u64)
.map(ed25519::PrivateKey::from_seed)
.collect()
}
fn pre_verify_test_info(
keys: &[ed25519::PrivateKey],
round: u64,
) -> Info<MinPk, ed25519::PublicKey> {
let dealers: Set<_> = keys
.iter()
.map(|sk| sk.public_key())
.try_collect()
.expect("dealers must be unique");
Info::<MinPk, _>::new::<QuorumTwo>(
b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG_TEST",
round,
None,
Default::default(),
dealers.clone(),
dealers,
)
.expect("info must be valid")
}
fn generate_dealer_log(
info: &Info<MinPk, ed25519::PublicKey>,
keys: &[ed25519::PrivateKey],
dealer_index: usize,
seed: u64,
) -> DealerLog<MinPk, ed25519::PublicKey> {
let mut players: BTreeMap<_, _> = keys
.iter()
.cloned()
.map(|sk| {
let pk = sk.public_key();
(
pk,
Player::<MinPk, _>::new(info.clone(), sk)
.expect("player initialization must succeed"),
)
})
.collect();
let dealer_sk = keys[dealer_index].clone();
let dealer_pk = dealer_sk.public_key();
let mut rng = test_rng_seeded(seed);
let (mut dealer, pub_msg, priv_msgs) =
Dealer::start::<QuorumTwo>(&mut rng, info.clone(), dealer_sk, None)
.expect("dealer initialization must succeed");
for (player_pk, priv_msg) in priv_msgs {
let ack = players
.get_mut(&player_pk)
.expect("player should exist")
.dealer_message::<QuorumTwo>(dealer_pk.clone(), pub_msg.clone(), priv_msg)
.expect("dealer message must succeed");
dealer
.receive_player_ack(player_pk, ack)
.expect("ack handling must succeed");
}
dealer
.finalize::<QuorumTwo>()
.check(info)
.expect("signed dealer log must verify against its own info")
.1
}
let keys = pre_verify_test_keys();
let info = pre_verify_test_info(&keys, 0);
let wrong_info = pre_verify_test_info(&keys, 1);
let mut logs_by_key: BTreeMap<_, _> = keys
.iter()
.enumerate()
.map(|(dealer_index, sk)| {
let key = sk.public_key();
let seed = dealer_index as u64;
let valid = generate_dealer_log(&info, &keys, dealer_index, seed);
let invalid = generate_dealer_log(&wrong_info, &keys, dealer_index, seed);
assert_eq!(
valid.pub_msg, invalid.pub_msg,
"wrong-info log generation should only change transcript-bound signatures"
);
(key, (valid, invalid))
})
.collect();
let dealers = info
.dealers
.iter()
.cloned()
.map(|key| {
let (valid, invalid) = logs_by_key
.remove(&key)
.expect("fixture should include every dealer");
PreVerifyDealer {
key,
valid,
invalid,
}
})
.collect();
Self {
info,
wrong_info,
dealers,
}
}
fn required_commitments(&self) -> usize {
self.info.required_commitments::<QuorumTwo>() as usize
}
fn expected(&self, valid: &[bool]) -> Set<ed25519::PublicKey> {
assert_eq!(
valid.len(),
self.dealers.len(),
"fixture size should match case"
);
self.dealers
.iter()
.zip(valid.iter().copied())
.filter(|(_, is_valid)| *is_valid)
.take(self.required_commitments())
.map(|(dealer, _)| dealer.key.clone())
.try_collect()
.expect("dealers must be unique")
}
fn record(&self, logs: &mut PreVerifyLogs, dealer_index: usize, is_valid: bool) {
let dealer = &self.dealers[dealer_index];
let log = if is_valid {
dealer.valid.clone()
} else {
dealer.invalid.clone()
};
logs.record(dealer.key.clone(), log);
}
fn logs_for(
&self,
info: &Info<MinPk, ed25519::PublicKey>,
valid: &[bool],
) -> PreVerifyLogs {
assert_eq!(
valid.len(),
self.dealers.len(),
"fixture size should match case"
);
let mut logs = PreVerifyLogs::new(info.clone());
for (dealer_index, &is_valid) in valid.iter().enumerate() {
self.record(&mut logs, dealer_index, is_valid);
}
logs
}
}
#[derive(Debug)]
struct IncrementalPreVerifyCase {
valid: [bool; PRE_VERIFY_DEALERS],
batches: Vec<Vec<usize>>,
}
impl<'a> Arbitrary<'a> for IncrementalPreVerifyCase {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let mut valid = [false; PRE_VERIFY_DEALERS];
for is_valid in &mut valid {
*is_valid = u.arbitrary()?;
}
let mut order: Vec<_> = (0..valid.len()).collect();
for index in 0..valid.len() {
let last = order.len() - 1;
order.swap(index, u.int_in_range(index..=last)?);
}
let mut batches = Vec::new();
let mut start = 0;
while start < order.len() {
let batch_size = u.int_in_range(1..=order.len() - start)?;
batches.push(order[start..start + batch_size].to_vec());
start += batch_size;
}
Ok(Self { valid, batches })
}
}
impl IncrementalPreVerifyCase {
fn run(self, fixture: &PreVerifyFixture) -> arbitrary::Result<()> {
let required_commitments = fixture.required_commitments();
let expected = fixture.expected(&self.valid);
let fresh = fixture.logs_for(&fixture.info, &self.valid);
let mut incremental = PreVerifyLogs::new(fixture.info.clone());
let mut incremental_rng = test_rng();
for batch in &self.batches {
for &dealer_index in batch {
fixture.record(&mut incremental, dealer_index, self.valid[dealer_index]);
}
incremental.pre_verify::<ed25519::Batch>(&mut incremental_rng, &Sequential);
}
let mut fresh_rng = test_rng();
let fresh_selected = fresh
.select::<ed25519::Batch>(&mut fresh_rng, &Sequential)
.map(|(_, selection)| selection.keys().clone());
let incremental_selected = incremental
.select::<ed25519::Batch>(&mut incremental_rng, &Sequential)
.map(|(_, selection)| selection.keys().clone());
match &fresh_selected {
Ok(selected) => assert_eq!(
selected, &expected,
"all-at-once selection disagreed with validity mask: {:?}",
self
),
Err(_) => assert!(
expected.len() < required_commitments,
"all-at-once selection failed despite quorum-sized expected set: {:?}",
self
),
}
match (fresh_selected, incremental_selected) {
(Err(_), Err(_)) => {}
(Ok(fresh_selected), Ok(incremental_selected)) => assert_eq!(
incremental_selected, fresh_selected,
"incremental selection disagreed with all-at-once selection: {:?}",
self
),
(Ok(fresh_selected), Err(err)) => panic!(
"incremental selection failed with {err:?} but all-at-once selected {fresh_selected:?}: {self:?}"
),
(Err(err), Ok(incremental_selected)) => panic!(
"incremental selection returned {incremental_selected:?} but all-at-once failed with {err:?}: {self:?}"
),
}
Ok(())
}
}
#[test]
fn incremental_pre_verify_preserves_dealer_order() {
let fixture = PreVerifyFixture::new();
minifuzz::test(move |u| u.arbitrary::<IncrementalPreVerifyCase>()?.run(&fixture));
}
#[test]
fn logs_are_bound_to_constructor_info() {
let fixture = PreVerifyFixture::new();
let mut logs = fixture.logs_for(&fixture.info, &[false; PRE_VERIFY_DEALERS]);
let mut wrong_logs = fixture.logs_for(&fixture.wrong_info, &[false; PRE_VERIFY_DEALERS]);
let mut rng = test_rng();
logs.pre_verify::<ed25519::Batch>(&mut rng, &Sequential);
wrong_logs.pre_verify::<ed25519::Batch>(&mut rng, &Sequential);
assert!(
wrong_logs
.select::<ed25519::Batch>(&mut rng, &Sequential)
.is_ok(),
"control check: logs should verify when bound to the round they were created for"
);
assert!(
matches!(
logs.select::<ed25519::Batch>(&mut rng, &Sequential),
Err(Error::DkgFailed)
),
"logs bound to a different round must reject these transcript-bound signatures"
);
}
#[test]
fn finalize_rejects_logs_bound_to_different_round() {
let fixture = PreVerifyFixture::new();
let player =
Player::<MinPk, _>::new(fixture.info.clone(), ed25519::PrivateKey::from_seed(0))
.expect("player initialization must succeed");
let wrong_logs = fixture.logs_for(&fixture.wrong_info, &[false; PRE_VERIFY_DEALERS]);
let result =
player.finalize::<QuorumTwo, ed25519::Batch>(&mut test_rng(), wrong_logs, &Sequential);
assert!(
matches!(result, Err(Error::MismatchedLogs)),
"finalize should reject logs bound to a different round"
);
}
#[test]
fn single_round() -> anyhow::Result<()> {
Plan::new(NZU32!(4))
.with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]))
.run::<MinPk>(0)
}
#[test]
fn multiple_rounds() -> anyhow::Result<()> {
Plan::new(NZU32!(4))
.with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]))
.with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]))
.with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]))
.with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]))
.run::<MinPk>(0)
}
#[test]
fn player_crash_resume_after_dealer() -> anyhow::Result<()> {
Plan::new(NZU32!(4))
.with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).crash_resume_player(1, 2))
.run::<MinPk>(0)
}
#[test]
fn resume_missing_good_dealer_message_fails_after_checkpoint() -> anyhow::Result<()> {
Plan::new(NZU32!(4))
.with(
Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3])
.resume_missing_dealer_msg_fails(2, 1),
)
.run::<MinPk>(0)
}
#[test]
fn resume_missing_good_dealer_message_skips_unacked_players() -> anyhow::Result<()> {
Plan::new(NZU32!(4))
.with(
Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3])
.no_ack(1, 0)
.resume_missing_dealer_msg_fails(2, 1),
)
.run::<MinPk>(0)
}
#[test]
fn finalize_fails_after_resume_without_good_dealer_message() -> anyhow::Result<()> {
Plan::new(NZU32!(4))
.with(
Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3])
.no_ack(0, 1)
.finalize_missing_dealer_msg_fails(0),
)
.run::<MinPk>(0)
}
#[test]
fn invalid_checkpoint_configs_fail_validation() {
assert!(Plan::new(NZU32!(4))
.with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).crash_resume_player(4, 2))
.validate()
.is_err());
assert!(Plan::new(NZU32!(4))
.with(
Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3])
.resume_missing_dealer_msg_fails(1, 2),
)
.validate()
.is_err());
assert!(Plan::new(NZU32!(4))
.with(
Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3])
.bad_reveal(1, 0)
.resume_missing_dealer_msg_fails(2, 1),
)
.validate()
.is_err());
}
#[test]
fn changing_committee() -> anyhow::Result<()> {
Plan::new(NonZeroU32::new(5).unwrap())
.with(Round::new(vec![0, 1, 2], vec![1, 2, 3]))
.with(Round::new(vec![1, 2, 3], vec![2, 3, 4]))
.with(Round::new(vec![2, 3, 4], vec![3, 4, 0]))
.with(Round::new(vec![3, 4, 0], vec![4, 0, 1]))
.run::<MinPk>(0)
}
#[test]
fn missing_ack() -> anyhow::Result<()> {
Plan::new(NonZeroU32::new(4).unwrap())
.with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).no_ack(0, 0))
.with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).no_ack(0, 1))
.with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).no_ack(0, 2))
.with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).no_ack(0, 3))
.run::<MinPk>(0)
}
#[test]
fn increasing_decreasing_committee() -> anyhow::Result<()> {
Plan::new(NonZeroU32::new(5).unwrap())
.with(Round::new(vec![0, 1], vec![0, 1, 2]))
.with(Round::new(vec![0, 1, 2], vec![0, 1, 2, 3]))
.with(Round::new(vec![0, 1, 2], vec![0, 1]))
.with(Round::new(vec![0, 1], vec![0, 1, 2, 3, 4]))
.with(Round::new(vec![0, 1, 2, 3], vec![0, 1]))
.run::<MinPk>(0)
}
#[test]
fn bad_reveal_fails() -> anyhow::Result<()> {
Plan::new(NonZeroU32::new(4).unwrap())
.with(Round::new(vec![0], vec![0, 1, 2, 3]).bad_reveal(0, 1))
.run::<MinPk>(0)
}
#[test]
fn bad_share() -> anyhow::Result<()> {
Plan::new(NonZeroU32::new(4).unwrap())
.with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).bad_share(0, 1))
.with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).bad_share(0, 2))
.run::<MinPk>(0)
}
#[test]
fn shift_degree_fails() -> anyhow::Result<()> {
Plan::new(NonZeroU32::new(4).unwrap())
.with(Round::new(vec![0], vec![0, 1, 2, 3]).shift_degree(
0,
NonZeroI32::new(1).ok_or_else(|| anyhow!("invalid NZI32"))?,
))
.run::<MinPk>(0)
}
#[test]
fn replace_share_fails() -> anyhow::Result<()> {
Plan::new(NonZeroU32::new(4).unwrap())
.with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]))
.with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).replace_share(0))
.run::<MinPk>(0)
}
#[test]
fn too_many_reveals_dealer() -> anyhow::Result<()> {
Plan::new(NonZeroU32::new(4).unwrap())
.with(
Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3])
.no_ack(0, 0)
.no_ack(0, 1),
)
.run::<MinPk>(0)
}
#[test]
fn too_many_reveals_player() -> anyhow::Result<()> {
Plan::new(NonZeroU32::new(4).unwrap())
.with(
Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3])
.no_ack(0, 0)
.no_ack(1, 0)
.no_ack(3, 0),
)
.run::<MinPk>(0)
}
#[test]
fn bad_sigs() -> anyhow::Result<()> {
Plan::new(NonZeroU32::new(4).unwrap())
.with(
Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3])
.bad_dealer_sig(
0,
Masks {
log: vec![0xFF; 8],
..Default::default()
},
)
.bad_player_sig(
0,
1,
Masks {
pub_msg: vec![0xFF; 8],
..Default::default()
},
),
)
.run::<MinPk>(0)
}
#[test]
fn issue_2745_regression() -> anyhow::Result<()> {
Plan::new(NonZeroU32::new(6).unwrap())
.with(
Round::new(vec![0], vec![5, 1, 3, 0, 4])
.no_ack(0, 5)
.bad_share(0, 5),
)
.with(Round::new(vec![0, 1, 3, 4], vec![0]))
.with(Round::new(vec![0], vec![0]))
.run::<MinPk>(0)
}
#[test]
fn signed_dealer_log_commitment() -> Result<(), Error> {
let sk = ed25519::PrivateKey::from_seed(0);
let pk = sk.public_key();
let info = Info::<MinPk, _>::new::<N3f1>(
b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG_TEST",
0,
None,
Default::default(),
vec![sk.public_key()].try_into().unwrap(),
vec![sk.public_key()].try_into().unwrap(),
)?;
let mut log0 = {
let (dealer, _, _) =
Dealer::start::<N3f1>(&mut test_rng(), info.clone(), sk.clone(), None)?;
dealer.finalize::<N3f1>()
};
let mut log1 = {
let (mut dealer, pub_msg, priv_msgs) =
Dealer::start::<N3f1>(&mut test_rng(), info.clone(), sk.clone(), None)?;
let mut player = Player::new(info.clone(), sk)?;
let ack = player
.dealer_message::<N3f1>(pk.clone(), pub_msg, priv_msgs[0].1.clone())
.unwrap();
dealer.receive_player_ack(pk, ack)?;
dealer.finalize::<N3f1>()
};
std::mem::swap(&mut log0.log, &mut log1.log);
assert!(log0.check(&info).is_none());
assert!(log1.check(&info).is_none());
Ok(())
}
#[test]
fn info_with_different_mode_is_not_equal() -> Result<(), Error> {
let sk = ed25519::PrivateKey::from_seed(0);
let pk = sk.public_key();
let dealers: Set<ed25519::PublicKey> = vec![pk.clone()].try_into().unwrap();
let players: Set<ed25519::PublicKey> = vec![pk].try_into().unwrap();
let default_mode_info = Info::<MinPk, _>::new::<N3f1>(
b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG_TEST",
0,
None,
Mode::default(),
dealers.clone(),
players.clone(),
)?;
let roots_of_unity_mode_info = Info::<MinPk, _>::new::<N3f1>(
b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG_TEST",
0,
None,
Mode::RootsOfUnity,
dealers,
players,
)?;
assert_ne!(default_mode_info, roots_of_unity_mode_info);
Ok(())
}
#[test]
fn resume_ignores_invalid_logged_ack_signature() -> Result<(), Error> {
let dealer_sk = ed25519::PrivateKey::from_seed(11);
let dealer_pk = dealer_sk.public_key();
let player_sk = ed25519::PrivateKey::from_seed(22);
let player_pk = player_sk.public_key();
let dealers: Set<ed25519::PublicKey> = vec![dealer_pk.clone()].try_into().unwrap();
let players: Set<ed25519::PublicKey> = vec![player_pk.clone()].try_into().unwrap();
let info = Info::<MinPk, _>::new::<N3f1>(
b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG_TEST",
0,
None,
Default::default(),
dealers.clone(),
players.clone(),
)?;
let wrong_round_info = Info::<MinPk, _>::new::<N3f1>(
b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG_TEST",
1,
None,
Default::default(),
dealers,
players,
)?;
let (_, pub_msg, _) =
Dealer::start::<N3f1>(&mut test_rng(), info.clone(), dealer_sk, None)?;
let bad_ack = PlayerAck {
sig: transcript_for_ack(
&transcript_for_round(&wrong_round_info),
&dealer_pk,
&pub_msg,
)
.sign(&player_sk),
};
let results: Map<_, _> = vec![(player_pk, AckOrReveal::Ack(bad_ack))]
.into_iter()
.try_collect()
.unwrap();
let mut logs = BTreeMap::new();
logs.insert(
dealer_pk,
DealerLog {
pub_msg,
results: DealerResult::Ok(results),
},
);
let resumed = Player::resume::<N3f1>(info, player_sk, &logs, []);
assert!(resumed.is_ok());
let (_, acks) = resumed.unwrap();
assert!(acks.is_empty());
Ok(())
}
#[test]
fn test_dealer_priv_msg_redacted() {
let mut rng = test_rng();
let msg = DealerPrivMsg::new(Scalar::random(&mut rng));
let debug = format!("{:?}", msg);
assert!(debug.contains("REDACTED"));
}
#[cfg(feature = "arbitrary")]
mod conformance {
use super::*;
use commonware_codec::conformance::CodecConformance;
commonware_conformance::conformance_tests! {
CodecConformance<Output<MinPk, ed25519::PublicKey>>,
CodecConformance<DealerPubMsg<MinPk>>,
CodecConformance<DealerPrivMsg>,
CodecConformance<PlayerAck<ed25519::PublicKey>>,
CodecConformance<AckOrReveal<ed25519::PublicKey>>,
CodecConformance<DealerResult<ed25519::PublicKey>>,
CodecConformance<DealerLog<MinPk, ed25519::PublicKey>>,
CodecConformance<SignedDealerLog<MinPk, ed25519::PrivateKey>>,
}
}
}