mod evrf;
use crate::{
bls12381::primitives::{
group::{Private, Scalar, ScalarReadCfg, Share, SmallScalar, G1},
sharing::{Mode, ModeVersion, Sharing},
variant::MinPk,
},
transcript::{Summary, Transcript},
Signer as _, Verifier as _,
};
use bytes::{Buf, BufMut, Bytes};
use commonware_codec::{Encode, EncodeSize, RangeCfg, Read, ReadExt, Write};
use commonware_math::{
algebra::{Additive, CryptoGroup, Random, Space},
poly::{Interpolator, Poly},
};
use commonware_parallel::Strategy;
use commonware_utils::{
ordered::{Map, Quorum as _, Set},
Faults, Participant, TryCollect as _, NZU32,
};
pub use evrf::{PrivateKey, PublicKey, Setup};
use evrf::{Signature, VrfCommitments};
use rand_core::CryptoRng;
use std::{borrow::Cow, collections::BTreeMap, num::NonZeroU32};
const NAMESPACE: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_GOLDEN_DKG";
#[derive(Debug)]
pub enum Error {
DkgFailed,
NumDealers(usize),
NumPlayers(usize),
MissingDealerShare,
UnknownDealer(String),
UnknownPlayer,
UnsupportedNumPlayers {
max: u32,
num_players: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Output<P> {
summary: Summary,
public: Sharing<MinPk>,
quorum: NonZeroU32,
dealers: Set<P>,
players: Set<P>,
revealed: Set<P>,
}
impl<P: Ord + Clone> Output<P> {
fn new(
summary: Summary,
public: Sharing<MinPk>,
quorum: NonZeroU32,
dealers: Set<P>,
players: Set<P>,
) -> Self {
Self {
summary,
public,
quorum,
dealers,
players: players.clone(),
revealed: players,
}
}
pub const fn quorum(&self) -> NonZeroU32 {
self.quorum
}
pub const fn public(&self) -> &Sharing<MinPk> {
&self.public
}
pub const fn dealers(&self) -> &Set<P> {
&self.dealers
}
pub const fn players(&self) -> &Set<P> {
&self.players
}
}
impl<P: Write> Write for Output<P> {
fn write(&self, buf: &mut impl BufMut) {
self.summary.write(buf);
self.public.write(buf);
self.quorum.write(buf);
self.dealers.write(buf);
self.players.write(buf);
self.revealed.write(buf);
}
}
impl<P: EncodeSize> EncodeSize for Output<P> {
fn encode_size(&self) -> usize {
self.summary.encode_size()
+ self.public.encode_size()
+ self.quorum.encode_size()
+ self.dealers.encode_size()
+ self.players.encode_size()
+ self.revealed.encode_size()
}
}
impl<P: Read<Cfg = ()> + Ord + Clone> Read for Output<P> {
type Cfg = (NonZeroU32, ModeVersion);
fn read_cfg(
buf: &mut impl Buf,
(max_participants, max_supported_mode): &Self::Cfg,
) -> Result<Self, commonware_codec::Error> {
let max_usize = max_participants.get() as usize;
let summary: Summary = ReadExt::read(buf)?;
let public: Sharing<MinPk> =
Read::read_cfg(buf, &(*max_participants, *max_supported_mode))?;
let quorum: NonZeroU32 = ReadExt::read(buf)?;
let dealers: Set<P> = Read::read_cfg(buf, &(RangeCfg::new(1..=max_usize), ()))?;
let players: Set<P> = Read::read_cfg(buf, &(RangeCfg::new(1..=max_usize), ()))?;
let revealed: Set<P> = Read::read_cfg(buf, &(RangeCfg::new(0..=max_usize), ()))?;
if public.total().get() as usize != players.len() {
return Err(commonware_codec::Error::Invalid(
"Output",
"sharing total must equal players length",
));
}
if quorum.get() > players.len() as u32 {
return Err(commonware_codec::Error::Invalid(
"Output",
"quorum must not exceed players length",
));
}
if revealed
.iter()
.any(|player| players.position(player).is_none())
{
return Err(commonware_codec::Error::Invalid(
"Output",
"revealed players must be a subset of players",
));
}
Ok(Self {
summary,
public,
quorum,
dealers,
players,
revealed,
})
}
}
#[derive(Clone)]
#[allow(dead_code)]
pub struct Info {
summary: Summary,
round: u64,
previous: Option<Output<PublicKey>>,
mode: Mode,
player_quorum: NonZeroU32,
required_commitments: NonZeroU32,
dealers: Set<PublicKey>,
players: Set<PublicKey>,
}
#[allow(dead_code)]
impl Info {
pub fn new<M: Faults>(
namespace: &[u8],
round: u64,
previous: Option<Output<PublicKey>>,
dealers: Set<PublicKey>,
players: Set<PublicKey>,
) -> 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(|dealer| previous.players.position(dealer).is_none())
{
return Err(Error::UnknownDealer(format!("{unknown:?}")));
}
if dealers.len() < previous.quorum().get() as usize {
return Err(Error::NumDealers(dealers.len()));
}
}
let mode = Mode::default();
let player_quorum =
NonZeroU32::new(players.quorum::<M>()).expect("non-empty players have non-zero quorum");
let dealer_quorum =
NonZeroU32::new(dealers.quorum::<M>()).expect("non-empty dealers have non-zero quorum");
let required_commitments = previous
.as_ref()
.map(|previous| dealer_quorum.max(previous.quorum()))
.unwrap_or(dealer_quorum);
let summary = {
let mut transcript = Transcript::new(NAMESPACE);
transcript
.commit(namespace)
.commit(round.encode())
.commit(previous.encode())
.commit(dealers.encode())
.commit(players.encode())
.commit(player_quorum.encode())
.commit(required_commitments.encode());
transcript.summarize()
};
Ok(Self {
summary,
round,
previous,
mode,
player_quorum,
required_commitments,
dealers,
players,
})
}
pub const fn summary(&self) -> &Summary {
&self.summary
}
fn player_index(&self, player: &PublicKey) -> Result<Participant, Error> {
self.players.index(player).ok_or(Error::UnknownPlayer)
}
fn dealer_index(&self, dealer: &PublicKey) -> Result<Participant, Error> {
self.dealers
.index(dealer)
.ok_or(Error::UnknownDealer(format!("{dealer:?}")))
}
fn unwrap_or_random_share(
&self,
mut rng: impl CryptoRng,
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)
}
const fn degree(&self) -> u32 {
self.player_quorum.get() - 1
}
const fn required_commitments(&self) -> NonZeroU32 {
self.required_commitments
}
fn player_scalar(&self, player: &PublicKey) -> Result<Scalar, Error> {
Ok(self
.mode
.scalar(self.num_players(), self.player_index(player)?)
.expect("player index should be < num_players"))
}
}
const fn check_setup(setup: &Setup, info: &Info) -> Result<(), Error> {
let num_players = info.players.len() as u32;
if !setup.supports(num_players) {
return Err(Error::UnsupportedNumPlayers {
max: setup.max_players().get(),
num_players,
});
}
Ok(())
}
pub fn deal(
rng: &mut impl CryptoRng,
setup: &Setup,
info: &Info,
me: &PrivateKey,
share: Option<Share>,
strategy: &impl Strategy,
) -> Result<SignedDealerLog, Error> {
check_setup(setup, info)?;
let me_pub = me.public();
info.dealer_index(&me_pub)?;
let share = info.unwrap_or_random_share(
&mut *rng,
share.map(|x| x.private.expose_unwrap()),
)?;
let poly = Poly::new_with_constant(&mut *rng, info.degree(), share);
let nonce = Summary::random(&mut *rng);
let (masks, commitments) = me.vrf_batch_checked(
&mut *rng,
setup,
Transcript::resume(*info.summary())
.fork(b"dealer vrf")
.commit(me_pub.encode()),
&nonce,
info.players.iter().cloned(),
strategy,
);
Ok(SignedDealerLog::sign(
me,
info,
DealerLog {
dealing: Dealing::reckon(info, nonce, poly, masks)?,
commitments,
},
))
}
struct Selection {
weights: Option<Interpolator<PublicKey, Scalar>>,
dealings: BTreeMap<PublicKey, Dealing>,
}
#[allow(dead_code)]
impl Selection {
fn public_poly(&self, strategy: &impl Strategy) -> Poly<G1> {
self.weights.as_ref().map_or_else(
|| {
let mut public = Poly::zero();
for dealing in self.dealings.values() {
public += &dealing.poly;
}
public
},
|weights| {
let commitments: Map<PublicKey, Poly<G1>> = self
.dealings
.iter()
.map(|(dealer, dealing)| (dealer.clone(), dealing.poly.clone()))
.try_collect()
.expect("Map should have unique keys");
weights
.interpolate(&commitments, strategy)
.expect("select checks that enough points have been provided")
},
)
}
}
fn select(
rng: &mut impl CryptoRng,
setup: &Setup,
info: &Info,
logs: BTreeMap<PublicKey, DealerLog>,
strategy: &impl Strategy,
) -> Result<Selection, Error> {
let required = info.required_commitments().get() as usize;
let (first_required, rest) = {
let mut head = logs
.into_iter()
.filter(|(dealer, _)| info.dealers.position(dealer).is_some())
.collect::<Vec<_>>();
if head.len() < required {
return Err(Error::DkgFailed);
}
let tail = head.split_off(required);
(head, tail)
};
let mut checked = DealerLog::batch_check(rng, setup, info, first_required, strategy);
let missing = required.saturating_sub(checked.len());
if missing > 0 {
let rest = DealerLog::batch_check(rng, setup, info, rest, strategy);
if rest.len() < missing {
return Err(Error::DkgFailed);
}
checked.extend(rest.into_iter().take(missing));
}
assert_eq!(checked.len(), required);
let weights = info.previous.as_ref().map(|previous| {
let dealers: Set<PublicKey> = checked
.keys()
.cloned()
.try_collect()
.expect("selected dealers are unique");
previous
.public()
.mode()
.subset_interpolator(previous.players(), &dealers)
.expect("the result of select should produce a valid subset")
});
Ok(Selection {
weights,
dealings: checked,
})
}
pub fn observe(
rng: &mut impl CryptoRng,
setup: &Setup,
info: &Info,
logs: BTreeMap<PublicKey, DealerLog>,
strategy: &impl Strategy,
) -> Result<Output<PublicKey>, Error> {
check_setup(setup, info)?;
let selection = select(rng, setup, info, logs, strategy)?;
let public = selection.public_poly(strategy);
let n = info.players.len() as u32;
let sharing = Sharing::new(info.mode, NZU32!(n), public);
let dealers: Set<PublicKey> = selection
.dealings
.keys()
.cloned()
.try_collect()
.expect("selected dealers are unique");
Ok(Output::new(
*info.summary(),
sharing,
info.player_quorum,
dealers,
info.players.clone(),
))
}
pub fn play(
rng: &mut impl CryptoRng,
setup: &Setup,
info: &Info,
logs: BTreeMap<PublicKey, DealerLog>,
me: &PrivateKey,
strategy: &impl Strategy,
) -> Result<(Output<PublicKey>, Share), Error> {
check_setup(setup, info)?;
let me_pub = me.public();
let my_index = info.player_index(&me_pub)?;
let selection = select(rng, setup, info, logs, strategy)?;
let dealings: Map<PublicKey, Scalar> = selection
.dealings
.iter()
.map(|(dealer, dealing)| {
let mask = me.vrf_recv(&dealing.nonce, dealer);
let masked_share = dealing
.masked_shares
.get_value(&me_pub)
.expect("select checks that all players have shares");
(dealer.clone(), masked_share.clone() - &mask)
})
.try_collect()
.expect("selected dealers are unique");
let public = selection.public_poly(strategy);
let private = selection.weights.map_or_else(
|| {
let mut out = Scalar::zero();
for s in dealings.values() {
out += s;
}
out
},
|weights| {
weights
.interpolate(&dealings, strategy)
.expect("select ensures that we can recover")
},
);
let n = info.players.len() as u32;
let sharing = Sharing::new(info.mode, NZU32!(n), public);
let dealers: Set<PublicKey> = selection
.dealings
.keys()
.cloned()
.try_collect()
.expect("selected dealers are unique");
let output = Output::new(
*info.summary(),
sharing,
info.player_quorum,
dealers,
info.players.clone(),
);
let share = Share::new(my_index, Private::new(private));
Ok((output, share))
}
#[derive(Clone)]
pub struct SignedDealerLog {
dealer: PublicKey,
signature: Signature,
log: DealerLog,
}
impl Write for SignedDealerLog {
fn write(&self, buf: &mut impl BufMut) {
self.dealer.write(buf);
self.signature.write(buf);
self.log.write(buf);
}
}
impl EncodeSize for SignedDealerLog {
fn encode_size(&self) -> usize {
self.dealer.encode_size() + self.signature.encode_size() + self.log.encode_size()
}
}
impl Read for SignedDealerLog {
type Cfg = (NonZeroU32, ModeVersion);
fn read_cfg(buf: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
let dealer: PublicKey = ReadExt::read(buf)?;
let signature: Signature = ReadExt::read(buf)?;
let log = Read::read_cfg(buf, cfg)?;
Ok(Self {
dealer,
signature,
log,
})
}
}
impl SignedDealerLog {
fn sign(signer: &PrivateKey, info: &Info, log: DealerLog) -> Self {
let dealer = signer.public();
let msg = Self::signature_message(info, &log);
let signature = signer.sign(NAMESPACE, &msg);
Self {
dealer,
signature,
log,
}
}
pub fn identify(self, info: &Info) -> Option<(PublicKey, DealerLog)> {
let msg = Self::signature_message(info, &self.log);
if !self.dealer.verify(NAMESPACE, &msg, &self.signature) {
return None;
}
Some((self.dealer, self.log))
}
fn signature_message(info: &Info, log: &DealerLog) -> Vec<u8> {
Transcript::resume(*info.summary())
.fork(b"dealer log")
.commit(log.encode())
.summarize()
.encode()
.to_vec()
}
}
#[derive(Clone)]
#[allow(dead_code)]
pub struct DealerLog {
commitments: VrfCommitments,
dealing: Dealing,
}
impl Write for DealerLog {
fn write(&self, buf: &mut impl bytes::BufMut) {
self.dealing.write(buf);
self.commitments.write(buf);
}
}
impl EncodeSize for DealerLog {
fn encode_size(&self) -> usize {
self.dealing.encode_size() + self.commitments.encode_size()
}
}
impl Read for DealerLog {
type Cfg = (NonZeroU32, ModeVersion);
fn read_cfg(buf: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
let dealing = Read::read_cfg(buf, cfg)?;
let commitments = Read::read_cfg(buf, &cfg.0)?;
Ok(Self {
dealing,
commitments,
})
}
}
#[allow(dead_code)]
impl DealerLog {
#[allow(dead_code)]
fn batch_check(
rng: &mut impl CryptoRng,
setup: &Setup,
info: &Info,
batch: impl IntoIterator<Item = (PublicKey, Self)>,
strategy: &impl Strategy,
) -> BTreeMap<PublicKey, Dealing> {
let (commitments, dealings) = batch
.into_iter()
.map(|(d, log)| {
(
(
d.clone(),
Bytes::copy_from_slice(log.dealing.nonce.as_ref()),
log.commitments,
),
(d, log.dealing),
)
})
.collect::<(Vec<_>, Vec<_>)>();
let mask_commitments = VrfCommitments::check_batch(
rng,
setup,
&Transcript::resume(*info.summary()),
&info.players,
commitments,
strategy,
);
dealings
.into_iter()
.filter_map(|(d, dealing)| {
let mask_commitments = mask_commitments.get_value(&d)?;
if !dealing.check(rng, info, &d, mask_commitments, strategy) {
return None;
}
Some((d, dealing))
})
.collect()
}
}
#[derive(Clone)]
struct Dealing {
nonce: Summary,
poly: Poly<G1>,
masked_shares: Map<PublicKey, Scalar>,
}
impl Write for Dealing {
fn write(&self, buf: &mut impl bytes::BufMut) {
self.nonce.write(buf);
self.poly.write(buf);
self.masked_shares.write(buf);
}
}
impl EncodeSize for Dealing {
fn encode_size(&self) -> usize {
self.nonce.encode_size() + self.poly.encode_size() + self.masked_shares.encode_size()
}
}
impl Read for Dealing {
type Cfg = (NonZeroU32, ModeVersion);
fn read_cfg(
buf: &mut impl Buf,
(max_players, _mode_version): &Self::Cfg,
) -> Result<Self, commonware_codec::Error> {
let nonce = ReadExt::read(buf)?;
let poly = Read::read_cfg(buf, &(RangeCfg::from(NZU32!(1)..=*max_players), ()))?;
let masked_shares = Read::read_cfg(
buf,
&(
RangeCfg::new(0..=max_players.get() as usize),
(),
ScalarReadCfg::AllowZero,
),
)?;
Ok(Self {
nonce,
poly,
masked_shares,
})
}
}
impl Dealing {
fn reckon(
info: &Info,
nonce: Summary,
poly: Poly<Scalar>,
masks: Map<PublicKey, Scalar>,
) -> Result<Self, Error> {
let mut inner = masks;
for (player, mask) in inner.iter_pairs_mut() {
*mask += &poly.eval(&info.player_scalar(player)?);
}
let poly = Poly::commit(poly);
Ok(Self {
nonce,
poly,
masked_shares: inner,
})
}
#[must_use]
fn check(
&self,
rng: &mut impl CryptoRng,
info: &Info,
dealer: &PublicKey,
mask_commitments: &Map<PublicKey, G1>,
strategy: &impl Strategy,
) -> bool {
if self.poly.degree_exact() != info.degree() {
return false;
}
if let Some(previous) = info.previous.as_ref() {
let Some(expected) = previous
.players()
.index(dealer)
.and_then(|idx| previous.public().partial_public(idx).ok())
else {
return false;
};
if *self.poly.constant() != expected {
return false;
}
}
let len = info.players.len() - 1;
let (r, z, m, x) = {
let mut r = Vec::with_capacity(len);
let mut z = Vec::with_capacity(len);
let mut m = Vec::with_capacity(len);
let mut x = Vec::with_capacity(len);
for p in &info.players {
if self.masked_shares.get_value(p).is_none() {
return false;
}
if p == dealer {
continue;
}
r.push(SmallScalar::random(&mut *rng));
let z_i = self
.masked_shares
.get_value(p)
.expect("presence checked above");
z.push(z_i.clone());
let Some(m_i) = mask_commitments.get_value(p) else {
return false;
};
m.push(*m_i);
x.push(info.player_scalar(p).expect("player scalar must exist"));
}
(r, z, m, x)
};
let lhs: G1 = {
let r_z = r.iter().zip(z).fold(Scalar::zero(), |mut acc, (r_i, z_i)| {
acc += &(Scalar::from(r_i.clone()) * &z_i);
acc
});
let r_m = G1::msm(&m, &r, strategy);
G1::generator() * &r_z - &r_m
};
let rhs: G1 = self.poly.lin_comb_eval(
r.into_iter()
.zip(x)
.map(|(r_i, x_i)| (Scalar::from(r_i), Cow::Owned(x_i))),
strategy,
);
lhs == rhs
}
}
#[cfg(any(feature = "arbitrary", test))]
mod test_plan {
use super::*;
use commonware_math::{algebra::Random, poly::Poly};
use commonware_parallel::Sequential;
use commonware_utils::{N3f1, TestRng};
use std::collections::{BTreeMap, BTreeSet};
pub const MAX_PARTICIPANTS: u32 = 7;
#[derive(Debug)]
pub struct Plan {
num_dealers: u32,
num_players: u32,
star: u32,
bad_signatures: BTreeSet<u32>,
bad_shares: BTreeSet<(u32, u32)>,
bad_commitments: BTreeSet<u32>,
missing_shares: BTreeSet<(u32, u32)>,
shift_degrees: BTreeMap<u32, i32>,
drop_dealers: BTreeSet<u32>,
reshare: bool,
replace_shares: BTreeSet<u32>,
fake_masks: BTreeSet<u32>,
}
impl Plan {
pub const fn new(num_dealers: u32, num_players: u32, star: u32) -> Self {
Self {
num_dealers,
num_players,
star,
bad_signatures: BTreeSet::new(),
bad_shares: BTreeSet::new(),
bad_commitments: BTreeSet::new(),
missing_shares: BTreeSet::new(),
shift_degrees: BTreeMap::new(),
drop_dealers: BTreeSet::new(),
reshare: false,
replace_shares: BTreeSet::new(),
fake_masks: BTreeSet::new(),
}
}
pub fn bad_signature(mut self, dealer: u32) -> Self {
self.bad_signatures.insert(dealer);
self
}
pub fn bad_share(mut self, dealer: u32, player: u32) -> Self {
self.bad_shares.insert((dealer, player));
self
}
pub fn bad_commitment(mut self, dealer: u32) -> Self {
self.bad_commitments.insert(dealer);
self
}
pub fn missing_share(mut self, dealer: u32, player: u32) -> Self {
self.missing_shares.insert((dealer, player));
self
}
pub fn shift_degree(mut self, dealer: u32, shift: i32) -> Self {
self.shift_degrees.insert(dealer, shift);
self
}
pub fn drop_dealer(mut self, dealer: u32) -> Self {
self.drop_dealers.insert(dealer);
self
}
pub const fn reshare(mut self) -> Self {
self.reshare = true;
self
}
pub fn replace_share(mut self, dealer: u32) -> Self {
self.replace_shares.insert(dealer);
self
}
pub fn fake_mask(mut self, dealer: u32) -> Self {
self.fake_masks.insert(dealer);
self
}
pub fn validate(&self) -> anyhow::Result<()> {
anyhow::ensure!(self.num_dealers >= 1, "need at least 1 dealer");
anyhow::ensure!(self.num_players >= 1, "need at least 1 player");
anyhow::ensure!(
self.star < self.num_players,
"star must be a valid player index"
);
for &d in &self.bad_signatures {
anyhow::ensure!(d < self.num_dealers, "bad_signature dealer out of range");
}
for &(d, p) in &self.bad_shares {
anyhow::ensure!(d < self.num_dealers, "bad_share dealer out of range");
anyhow::ensure!(p < self.num_players, "bad_share player out of range");
}
for &d in &self.bad_commitments {
anyhow::ensure!(d < self.num_dealers, "bad_commitment dealer out of range");
}
for &(d, p) in &self.missing_shares {
anyhow::ensure!(d < self.num_dealers, "missing_share dealer out of range");
anyhow::ensure!(p < self.num_players, "missing_share player out of range");
}
for &d in self.shift_degrees.keys() {
anyhow::ensure!(d < self.num_dealers, "shift_degree dealer out of range");
}
for &d in &self.drop_dealers {
anyhow::ensure!(d < self.num_dealers, "drop_dealer dealer out of range");
}
for &d in &self.replace_shares {
anyhow::ensure!(d < self.num_dealers, "replace_share dealer out of range");
anyhow::ensure!(self.reshare, "replace_share requires reshare");
}
for &d in &self.fake_masks {
anyhow::ensure!(d < self.num_dealers, "fake_mask dealer out of range");
}
Ok(())
}
fn is_bad_dealer(&self, dealer: u32) -> bool {
self.bad_commitments.contains(&dealer)
|| self.shift_degree_effective(dealer)
|| self.replace_shares.contains(&dealer)
|| self.fake_masks.contains(&dealer)
|| self.bad_shares.iter().any(|&(d, _)| d == dealer)
|| self.missing_shares.iter().any(|&(d, _)| d == dealer)
}
fn shift_degree_effective(&self, dealer: u32) -> bool {
let Some(&shift) = self.shift_degrees.get(&dealer) else {
return false;
};
let degree = N3f1::quorum(self.num_players).saturating_sub(1);
let new_degree = (degree as i32 + shift).max(0) as u32;
new_degree != degree
}
fn honest_dealer_count(&self) -> u32 {
(0..self.num_dealers)
.filter(|d| {
!self.drop_dealers.contains(d)
&& !self.bad_signatures.contains(d)
&& !self.is_bad_dealer(*d)
})
.count() as u32
}
fn expect_failure(&self) -> bool {
let required = N3f1::quorum(self.num_dealers);
let previous_quorum = if self.reshare {
N3f1::quorum(self.num_dealers)
} else {
0
};
let required = required.max(previous_quorum);
self.honest_dealer_count() < required
}
fn make_info(
round: u64,
previous: Option<Output<PublicKey>>,
dealer_keys: &[PrivateKey],
player_keys: &[PrivateKey],
) -> Info {
let dealer_set: Set<PublicKey> = dealer_keys
.iter()
.map(|k| k.public())
.try_collect()
.unwrap();
let player_set: Set<PublicKey> = player_keys
.iter()
.map(|k| k.public())
.try_collect()
.unwrap();
Info::new::<N3f1>(b"test", round, previous, dealer_set, player_set).unwrap()
}
pub fn run_fresh(
rng: &mut impl CryptoRng,
setup: &Setup,
dealer_keys: &[PrivateKey],
player_keys: &[PrivateKey],
strategy: &impl Strategy,
) -> anyhow::Result<(Output<PublicKey>, Vec<Share>)> {
let info = Self::make_info(0, None, dealer_keys, player_keys);
let mut logs = BTreeMap::new();
for dk in dealer_keys {
let signed = deal(rng, setup, &info, dk, None, strategy)
.map_err(|e| anyhow::anyhow!("{e:?}"))?;
let (pk, log) = signed
.identify(&info)
.ok_or_else(|| anyhow::anyhow!("identify failed"))?;
logs.insert(pk, log);
}
let output = observe(rng, setup, &info, logs.clone(), &Sequential)
.map_err(|e| anyhow::anyhow!("{e:?}"))?;
let mut shares = Vec::new();
for pk in player_keys {
let (_, share) = play(rng, setup, &info, logs.clone(), pk, &Sequential)
.map_err(|e| anyhow::anyhow!("{e:?}"))?;
shares.push(share);
}
Ok((output, shares))
}
pub fn run(self, setup: &Setup, seed: u64, strategy: &impl Strategy) -> anyhow::Result<()> {
self.validate()?;
let expect_failure = self.expect_failure();
let mut rng = TestRng::new(seed);
let dealer_keys: Vec<PrivateKey> = (0..self.num_dealers)
.map(|_| PrivateKey::random(&mut rng))
.collect();
let player_keys: Vec<PrivateKey> = (0..self.num_players)
.map(|_| PrivateKey::random(&mut rng))
.collect();
let (previous, previous_shares) = if self.reshare {
let (output, shares) =
Self::run_fresh(&mut rng, setup, &dealer_keys, &dealer_keys, strategy)?;
(Some(output), Some(shares))
} else {
(None, None)
};
let info = Self::make_info(
if self.reshare { 1 } else { 0 },
previous,
&dealer_keys,
&player_keys,
);
let mut signed_logs: Vec<(u32, SignedDealerLog)> = Vec::new();
for (i, dk) in dealer_keys.iter().enumerate() {
let i = i as u32;
if self.drop_dealers.contains(&i) {
continue;
}
let share = if self.reshare {
let shares = previous_shares.as_ref().unwrap();
if self.replace_shares.contains(&i) {
Some(Share::new(
shares[i as usize].index,
Private::new(Scalar::random(&mut rng)),
))
} else {
Some(shares[i as usize].clone())
}
} else {
None
};
let mut signed = if let Some(&shift) = self.shift_degrees.get(&i) {
let current_degree = info.degree();
let new_degree = (current_degree as i32 + shift).max(0) as u32;
if new_degree != current_degree {
let constant = info
.unwrap_or_random_share(
&mut rng,
share.as_ref().map(|s| s.private.clone().expose_unwrap()),
)
.expect("share should be available");
let poly = Poly::new_with_constant(&mut rng, new_degree, constant);
let nonce = Summary::random(&mut rng);
let (masks, commitments) = dk.vrf_batch_checked(
&mut rng,
setup,
Transcript::resume(*info.summary())
.fork(b"dealer vrf")
.commit(dk.public().encode()),
&nonce,
info.players.iter().cloned(),
strategy,
);
let dealing = Dealing::reckon(&info, nonce, poly, masks)
.expect("reckon should succeed");
SignedDealerLog::sign(
dk,
&info,
DealerLog {
dealing,
commitments,
},
)
} else {
deal(&mut rng, setup, &info, dk, share, strategy)
.map_err(|e| anyhow::anyhow!("{e:?}"))?
}
} else {
deal(&mut rng, setup, &info, dk, share, strategy)
.map_err(|e| anyhow::anyhow!("{e:?}"))?
};
if self.bad_signatures.contains(&i) {
let mut sig_bytes = signed.signature.encode_mut();
sig_bytes[0] ^= 0xFF;
signed.signature = commonware_codec::ReadExt::read(&mut sig_bytes)
.expect("signature should decode");
}
signed_logs.push((i, signed));
}
let mut logs = BTreeMap::new();
for (i, signed) in signed_logs {
if let Some((pk, mut log)) = signed.identify(&info) {
for &(d, p) in &self.bad_shares {
if d == i {
let player_pk = &player_keys[p as usize].public();
if let Some(share_val) =
log.dealing.masked_shares.get_value_mut(player_pk)
{
*share_val += &Scalar::random(&mut rng);
}
}
}
if self.bad_commitments.contains(&i) {
let corruption: Poly<Scalar> =
Poly::new(&mut rng, log.dealing.poly.degree());
log.dealing.poly += &Poly::commit(corruption);
}
for &(d, p) in &self.missing_shares {
if d == i {
let player_pk = player_keys[p as usize].public();
let new_shares: Map<PublicKey, Scalar> = log
.dealing
.masked_shares
.iter_pairs()
.filter(|(pk, _)| **pk != player_pk)
.map(|(pk, v)| (pk.clone(), v.clone()))
.try_collect()
.unwrap();
log.dealing.masked_shares = new_shares;
}
}
if self.fake_masks.contains(&i) {
for p in &info.players {
let delta = Scalar::random(&mut rng);
if let Some(share_val) = log.dealing.masked_shares.get_value_mut(p) {
*share_val += δ
}
log.commitments.perturb(p, &(G1::generator() * &delta));
}
}
logs.insert(pk, log);
}
}
let observe_result = observe(&mut rng, setup, &info, logs.clone(), &Sequential);
let star_key = &player_keys[self.star as usize];
let play_result = play(&mut rng, setup, &info, logs, star_key, &Sequential);
if expect_failure {
assert!(
observe_result.is_err() || play_result.is_err(),
"expected DkgFailed but both succeeded"
);
return Ok(());
}
let observe_output = observe_result.map_err(|e| anyhow::anyhow!("{e:?}"))?;
let (play_output, share) = play_result.map_err(|e| anyhow::anyhow!("{e:?}"))?;
assert_eq!(
observe_output, play_output,
"observe and play should produce the same public output"
);
let expected = play_output
.public()
.partial_public(share.index)
.map_err(|e| anyhow::anyhow!("{e:?}"))?;
let actual = share.public::<MinPk>();
assert_eq!(expected, actual, "share should match public polynomial");
if self.reshare {
let prev_public = info.previous.as_ref().unwrap().public().public();
let new_public = play_output.public().public();
assert_eq!(
prev_public, new_public,
"reshare should preserve group public key"
);
}
Ok(())
}
}
#[cfg(any(feature = "arbitrary", test))]
impl<'a> arbitrary::Arbitrary<'a> for Plan {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
const PLAIN_PRODUCT_CAP: u32 = 21;
const RESHARE_DEALER_CAP: u32 = 3;
const RESHARE_PRODUCT_CAP: u32 = 8;
let reshare = u.ratio(1, 4)?;
let (dealer_cap, product_cap) = if reshare {
(RESHARE_DEALER_CAP, RESHARE_PRODUCT_CAP)
} else {
(MAX_PARTICIPANTS, PLAIN_PRODUCT_CAP)
};
let num_dealers = u.int_in_range(1..=dealer_cap)?;
let max_players = (product_cap / num_dealers).clamp(1, MAX_PARTICIPANTS);
let num_players = u.int_in_range(1..=max_players)?;
let star = u.int_in_range(0..=num_players - 1)?;
let mut plan = Self::new(num_dealers, num_players, star);
for d in 0..num_dealers {
if u.ratio(1, 8)? {
plan.bad_signatures.insert(d);
}
if u.ratio(1, 8)? {
plan.bad_commitments.insert(d);
}
if u.ratio(1, 8)? {
plan.drop_dealers.insert(d);
}
if u.ratio(1, 10)? {
let shift = u.int_in_range(-2..=2i32)?;
if shift != 0 {
plan.shift_degrees.insert(d, shift);
}
}
for p in 0..num_players {
if u.ratio(1, 12)? {
plan.bad_shares.insert((d, p));
}
if u.ratio(1, 12)? {
plan.missing_shares.insert((d, p));
}
}
}
for d in 0..num_dealers {
if u.ratio(1, 8)? {
plan.fake_masks.insert(d);
}
}
if reshare {
plan = plan.reshare();
for d in 0..num_dealers {
if u.ratio(1, 6)? {
plan.replace_shares.insert(d);
}
}
}
plan.validate()
.map_err(|_| arbitrary::Error::IncorrectFormat)?;
Ok(plan)
}
}
}
#[cfg(feature = "arbitrary")]
pub use test_plan::{Plan as FuzzPlan, MAX_PARTICIPANTS as FUZZ_PLAN_MAX_PARTICIPANTS};
#[cfg(test)]
mod tests {
use super::{test_plan::Plan, *};
use commonware_invariants::minifuzz;
use commonware_macros::test_group;
use commonware_math::algebra::Random;
use commonware_parallel::Sequential;
use commonware_utils::{N3f1, N5f1};
use rstest::rstest;
use std::{sync::LazyLock, time::Duration};
const TEST_NAMESPACE: &[u8] = b"test";
static TEST_SETUP: LazyLock<Setup> = LazyLock::new(|| Setup::new(NonZeroU32::new(7).unwrap()));
#[test]
fn single_dealer_single_player() {
Plan::new(1, 1, 0)
.run(&TEST_SETUP, 42, &Sequential)
.expect("plan should succeed");
}
#[test_group("slow")]
#[test]
fn multiple_dealers_multiple_players() {
Plan::new(4, 7, 3)
.run(&TEST_SETUP, 42, &Sequential)
.expect("plan should succeed");
}
#[test_group("slow")]
#[test]
fn many_dealers() {
Plan::new(10, 5, 0)
.run(&TEST_SETUP, 42, &Sequential)
.expect("plan should succeed");
}
#[test_group("slow")]
#[test]
fn bad_signature_filtered() {
Plan::new(4, 7, 3)
.bad_signature(0)
.run(&TEST_SETUP, 42, &Sequential)
.expect("plan should succeed with 1 bad sig");
}
#[test_group("slow")]
#[test]
fn output_dealers_excludes_filtered_dealers() {
let mut rng = commonware_utils::test_rng();
let dealer_keys: Vec<PrivateKey> = (0..4).map(|_| PrivateKey::random(&mut rng)).collect();
let player_keys: Vec<PrivateKey> = (0..4).map(|_| PrivateKey::random(&mut rng)).collect();
let dealer_set: Set<PublicKey> = dealer_keys
.iter()
.map(|k| k.public())
.try_collect()
.unwrap();
let player_set: Set<PublicKey> = player_keys
.iter()
.map(|k| k.public())
.try_collect()
.unwrap();
let info =
Info::new::<N3f1>(TEST_NAMESPACE, 0, None, dealer_set.clone(), player_set).unwrap();
let mut logs = std::collections::BTreeMap::new();
for (i, dk) in dealer_keys.iter().enumerate() {
let mut signed = deal(&mut rng, &TEST_SETUP, &info, dk, None, &Sequential)
.expect("honest deal should succeed");
if i == 0 {
let mut sig_bytes = signed.signature.encode_mut();
sig_bytes[0] ^= 0xFF;
signed.signature = commonware_codec::ReadExt::read(&mut sig_bytes)
.expect("signature should decode");
}
if let Some((pk, log)) = signed.identify(&info) {
logs.insert(pk, log);
}
}
assert_eq!(logs.len(), 3, "one dealer should be filtered by identify");
let selected_dealers: Set<PublicKey> = logs.keys().cloned().try_collect().unwrap();
let output = observe(&mut rng, &TEST_SETUP, &info, logs, &Sequential)
.expect("round should still succeed with three valid dealers");
assert_eq!(
output.dealers(),
&selected_dealers,
"output dealers should match the validated subset that finalized the round"
);
assert_ne!(
output.dealers(),
&dealer_set,
"output dealers should not include the filtered dealer"
);
}
#[test_group("slow")]
#[test]
fn bad_signature_too_many() {
Plan::new(4, 7, 3)
.bad_signature(0)
.bad_signature(1)
.bad_signature(2)
.run(&TEST_SETUP, 42, &Sequential)
.expect("plan should handle expected failure");
}
#[test_group("slow")]
#[test]
fn bad_share_filtered() {
Plan::new(4, 7, 3)
.bad_share(0, 1)
.run(&TEST_SETUP, 42, &Sequential)
.expect("plan should succeed with 1 bad share");
}
#[test_group("slow")]
#[test]
fn bad_share_too_many() {
Plan::new(4, 7, 3)
.bad_share(0, 1)
.bad_share(1, 2)
.bad_share(2, 3)
.run(&TEST_SETUP, 42, &Sequential)
.expect("plan should handle expected failure");
}
#[test_group("slow")]
#[test]
fn bad_commitment_filtered() {
Plan::new(4, 7, 3)
.bad_commitment(0)
.run(&TEST_SETUP, 42, &Sequential)
.expect("plan should succeed with 1 bad commitment");
}
#[test_group("slow")]
#[test]
fn missing_share_filtered() {
Plan::new(4, 7, 3)
.missing_share(0, 1)
.run(&TEST_SETUP, 42, &Sequential)
.expect("plan should succeed with 1 missing share");
}
#[test_group("slow")]
#[test]
fn shift_degree_filtered() {
Plan::new(4, 7, 3)
.shift_degree(0, 1)
.run(&TEST_SETUP, 42, &Sequential)
.expect("plan should succeed with 1 wrong degree dealer filtered");
}
#[test_group("slow")]
#[test]
fn insufficient_dealers() {
Plan::new(4, 7, 3)
.drop_dealer(0)
.drop_dealer(1)
.drop_dealer(2)
.run(&TEST_SETUP, 42, &Sequential)
.expect("plan should handle expected failure");
}
#[test_group("slow")]
#[test]
fn reshare_happy_path() {
Plan::new(4, 7, 3)
.reshare()
.run(&TEST_SETUP, 42, &Sequential)
.expect("reshare should succeed");
}
#[test_group("slow")]
#[test]
fn reshare_replace_share_filtered() {
Plan::new(4, 7, 3)
.reshare()
.replace_share(0)
.run(&TEST_SETUP, 42, &Sequential)
.expect("reshare should succeed with 1 replaced share");
}
#[test_group("slow")]
#[test]
fn reshare_replace_share_fails() {
Plan::new(4, 7, 3)
.reshare()
.replace_share(0)
.replace_share(1)
.replace_share(2)
.run(&TEST_SETUP, 42, &Sequential)
.expect("plan should handle expected failure");
}
#[test]
#[ignore]
fn fake_mask_filtered() {
Plan::new(4, 7, 3)
.fake_mask(0)
.run(&TEST_SETUP, 42, &Sequential)
.expect("plan should succeed with 1 fake mask dealer filtered");
}
#[test]
fn unknown_dealer() {
let mut rng = commonware_utils::test_rng();
let dealer_keys: Vec<PrivateKey> = (0..4).map(|_| PrivateKey::random(&mut rng)).collect();
let player_keys: Vec<PrivateKey> = (0..4).map(|_| PrivateKey::random(&mut rng)).collect();
let dealer_set: Set<PublicKey> = dealer_keys
.iter()
.map(|k| k.public())
.try_collect()
.unwrap();
let player_set: Set<PublicKey> = player_keys
.iter()
.map(|k| k.public())
.try_collect()
.unwrap();
let info = Info::new::<N3f1>(TEST_NAMESPACE, 0, None, dealer_set, player_set).unwrap();
let outsider = PrivateKey::random(&mut rng);
let result = deal(&mut rng, &TEST_SETUP, &info, &outsider, None, &Sequential);
assert!(matches!(result, Err(Error::UnknownDealer(_))));
}
#[test_group("slow")]
#[test]
fn unknown_player() {
let mut rng = commonware_utils::test_rng();
let dealer_keys: Vec<PrivateKey> = (0..4).map(|_| PrivateKey::random(&mut rng)).collect();
let player_keys: Vec<PrivateKey> = (0..4).map(|_| PrivateKey::random(&mut rng)).collect();
let dealer_set: Set<PublicKey> = dealer_keys
.iter()
.map(|k| k.public())
.try_collect()
.unwrap();
let player_set: Set<PublicKey> = player_keys
.iter()
.map(|k| k.public())
.try_collect()
.unwrap();
let info = Info::new::<N3f1>(TEST_NAMESPACE, 0, None, dealer_set, player_set).unwrap();
let mut logs = std::collections::BTreeMap::new();
for dk in &dealer_keys {
let signed = deal(&mut rng, &TEST_SETUP, &info, dk, None, &Sequential).unwrap();
let (pk, log) = signed.identify(&info).unwrap();
logs.insert(pk, log);
}
let outsider = PrivateKey::random(&mut rng);
let result = play(
&mut rng,
&TEST_SETUP,
&info,
logs,
&outsider,
&commonware_parallel::Sequential,
);
assert!(matches!(result, Err(Error::UnknownPlayer)));
}
#[test_group("slow")]
#[test]
fn missing_dealer_share_in_reshare() {
let mut rng = commonware_utils::test_rng();
let dealer_keys: Vec<PrivateKey> = (0..4).map(|_| PrivateKey::random(&mut rng)).collect();
let player_keys: Vec<PrivateKey> = (0..4).map(|_| PrivateKey::random(&mut rng)).collect();
let (output, _shares) = Plan::run_fresh(
&mut rng,
&TEST_SETUP,
&dealer_keys,
&dealer_keys,
&Sequential,
)
.unwrap();
let dealer_set: Set<PublicKey> = dealer_keys
.iter()
.map(|k| k.public())
.try_collect()
.unwrap();
let player_set: Set<PublicKey> = player_keys
.iter()
.map(|k| k.public())
.try_collect()
.unwrap();
let info =
Info::new::<N3f1>(TEST_NAMESPACE, 1, Some(output), dealer_set, player_set).unwrap();
let result = deal(
&mut rng,
&TEST_SETUP,
&info,
&dealer_keys[0],
None,
&Sequential,
);
assert!(matches!(result, Err(Error::MissingDealerShare)));
}
#[test_group("slow")]
#[test]
fn signed_dealer_log_roundtrip() {
use commonware_parallel::Sequential;
let mut rng = commonware_utils::test_rng();
let dealer_keys: Vec<PrivateKey> = (0..4).map(|_| PrivateKey::random(&mut rng)).collect();
let player_keys: Vec<PrivateKey> = (0..7).map(|_| PrivateKey::random(&mut rng)).collect();
let dealer_set: Set<PublicKey> = dealer_keys
.iter()
.map(|k| k.public())
.try_collect()
.unwrap();
let player_set: Set<PublicKey> = player_keys
.iter()
.map(|k| k.public())
.try_collect()
.unwrap();
let info = Info::new::<N3f1>(TEST_NAMESPACE, 0, None, dealer_set, player_set).unwrap();
let signed = deal(
&mut rng,
&TEST_SETUP,
&info,
&dealer_keys[0],
None,
&Sequential,
)
.unwrap();
let encoded = signed.encode();
let max_players = NonZeroU32::new(7).unwrap();
let cfg = (max_players, ModeVersion::v0());
let decoded = SignedDealerLog::read_cfg(&mut encoded.as_ref(), &cfg).unwrap();
let (pk, log) = decoded
.identify(&info)
.expect("signature should verify after roundtrip");
assert_eq!(pk, dealer_keys[0].public());
let mut logs = std::collections::BTreeMap::new();
logs.insert(pk, log);
for dk in &dealer_keys[1..] {
let signed = deal(&mut rng, &TEST_SETUP, &info, dk, None, &Sequential).unwrap();
let (pk, log) = signed.identify(&info).unwrap();
logs.insert(pk, log);
}
observe(&mut rng, &TEST_SETUP, &info, logs, &Sequential).expect("observe should succeed");
}
#[test_group("slow")]
#[test]
fn output_roundtrip() {
let mut rng = commonware_utils::test_rng();
let dealer_keys: Vec<PrivateKey> = (0..4).map(|_| PrivateKey::random(&mut rng)).collect();
let player_keys: Vec<PrivateKey> = (0..7).map(|_| PrivateKey::random(&mut rng)).collect();
let (output, _shares) = Plan::run_fresh(
&mut rng,
&TEST_SETUP,
&dealer_keys,
&player_keys,
&Sequential,
)
.unwrap();
let encoded = output.encode();
let max_players = NonZeroU32::new(7).unwrap();
let cfg = (max_players, ModeVersion::v0());
let decoded: Output<PublicKey> = Read::read_cfg(&mut encoded.as_ref(), &cfg).unwrap();
assert_eq!(output, decoded);
assert_eq!(output.public(), decoded.public());
assert_eq!(output.dealers(), decoded.dealers());
assert_eq!(output.players(), decoded.players());
}
#[test]
fn unsupported_num_players_is_reported() {
let mut rng = commonware_utils::test_rng();
let small_setup = Setup::new(NonZeroU32::new(3).unwrap());
let dealer_keys: Vec<PrivateKey> = (0..4).map(|_| PrivateKey::random(&mut rng)).collect();
let player_keys: Vec<PrivateKey> = (0..4).map(|_| PrivateKey::random(&mut rng)).collect();
let dealer_set: Set<PublicKey> = dealer_keys
.iter()
.map(|k| k.public())
.try_collect()
.unwrap();
let player_set: Set<PublicKey> = player_keys
.iter()
.map(|k| k.public())
.try_collect()
.unwrap();
let info = Info::new::<N3f1>(TEST_NAMESPACE, 0, None, dealer_set, player_set).unwrap();
let deal_result = deal(
&mut rng,
&small_setup,
&info,
&dealer_keys[0],
None,
&Sequential,
);
assert!(matches!(
deal_result,
Err(Error::UnsupportedNumPlayers {
max: 3,
num_players: 4
})
));
let observe_result = observe(&mut rng, &small_setup, &info, BTreeMap::new(), &Sequential);
assert!(matches!(
observe_result,
Err(Error::UnsupportedNumPlayers {
max: 3,
num_players: 4
})
));
let play_result = play(
&mut rng,
&small_setup,
&info,
BTreeMap::new(),
&dealer_keys[0],
&Sequential,
);
assert!(matches!(
play_result,
Err(Error::UnsupportedNumPlayers {
max: 3,
num_players: 4
})
));
}
#[test]
fn missing_self_share_in_overlap_round_is_rejected() {
let mut rng = commonware_utils::test_rng();
let me = PrivateKey::random(&mut rng);
let me_pub = me.public();
let dealer_set: Set<PublicKey> = std::iter::once(me_pub.clone()).try_collect().unwrap();
let player_set: Set<PublicKey> = std::iter::once(me_pub.clone()).try_collect().unwrap();
let info = Info::new::<N3f1>(TEST_NAMESPACE, 0, None, dealer_set, player_set).unwrap();
let signed = deal(&mut rng, &TEST_SETUP, &info, &me, None, &Sequential)
.expect("honest deal should succeed");
let (pk, mut log) = signed.identify(&info).expect("valid signature");
assert_eq!(pk, me_pub);
assert!(
log.dealing.masked_shares.get_value(&me_pub).is_some(),
"honest dealing should include the dealer's self-row when it overlaps with players",
);
let new_shares: Map<PublicKey, Scalar> = log
.dealing
.masked_shares
.iter_pairs()
.filter(|(p, _)| **p != me_pub)
.map(|(p, v)| (p.clone(), v.clone()))
.try_collect()
.unwrap();
log.dealing.masked_shares = new_shares;
let mut logs = BTreeMap::new();
logs.insert(pk, log);
let observe_result = observe(&mut rng, &TEST_SETUP, &info, logs.clone(), &Sequential);
assert!(
matches!(observe_result, Err(Error::DkgFailed)),
"observe should reject a dealing missing the self-row, got {observe_result:?}",
);
let play_result = play(&mut rng, &TEST_SETUP, &info, logs, &me, &Sequential);
assert!(
matches!(play_result, Err(Error::DkgFailed)),
"play should reject a dealing missing the self-row, got {:?}",
play_result.as_ref().map(|_| "Ok"),
);
}
#[test]
fn info_rejects_empty_participants() {
let result = Info::new::<N3f1>(TEST_NAMESPACE, 0, None, Set::default(), Set::default());
assert!(matches!(result, Err(Error::NumDealers(0))));
}
#[test_group("slow")]
#[test]
fn info_rejects_reshare_dealer_outside_previous_players() {
let mut rng = commonware_utils::test_rng();
let previous_players: Vec<PrivateKey> =
(0..4).map(|_| PrivateKey::random(&mut rng)).collect();
let previous_dealers: Set<PublicKey> = previous_players
.iter()
.map(|dealer| dealer.public())
.try_collect()
.unwrap();
let previous_player_set = previous_dealers.clone();
let previous = Info::new::<N3f1>(
TEST_NAMESPACE,
0,
None,
previous_dealers,
previous_player_set,
)
.unwrap();
let mut logs = BTreeMap::new();
for dealer in &previous_players {
let signed = deal(&mut rng, &TEST_SETUP, &previous, dealer, None, &Sequential).unwrap();
let (pk, log) = signed.identify(&previous).unwrap();
logs.insert(pk, log);
}
let previous_output = observe(&mut rng, &TEST_SETUP, &previous, logs, &Sequential).unwrap();
let outsider = PrivateKey::random(&mut rng);
let dealers: Set<PublicKey> = std::iter::once(outsider.public()).try_collect().unwrap();
let players: Set<PublicKey> = std::iter::once(PrivateKey::random(&mut rng).public())
.try_collect()
.unwrap();
let result = Info::new::<N3f1>(TEST_NAMESPACE, 1, Some(previous_output), dealers, players);
assert!(matches!(result, Err(Error::UnknownDealer(_))));
}
#[test_group("slow")]
#[test]
fn info_rejects_reshare_with_too_few_dealers() {
let mut rng = commonware_utils::test_rng();
let previous_players: Vec<PrivateKey> =
(0..4).map(|_| PrivateKey::random(&mut rng)).collect();
let previous_dealers: Set<PublicKey> = previous_players
.iter()
.map(|dealer| dealer.public())
.try_collect()
.unwrap();
let previous_player_set = previous_dealers.clone();
let previous = Info::new::<N3f1>(
TEST_NAMESPACE,
0,
None,
previous_dealers,
previous_player_set,
)
.unwrap();
let mut logs = BTreeMap::new();
for dealer in &previous_players {
let signed = deal(&mut rng, &TEST_SETUP, &previous, dealer, None, &Sequential).unwrap();
let (pk, log) = signed.identify(&previous).unwrap();
logs.insert(pk, log);
}
let previous_output = observe(&mut rng, &TEST_SETUP, &previous, logs, &Sequential).unwrap();
let dealers: Set<PublicKey> = std::iter::once(previous_players[0].public())
.try_collect()
.unwrap();
let players: Set<PublicKey> = std::iter::once(PrivateKey::random(&mut rng).public())
.try_collect()
.unwrap();
let result = Info::new::<N3f1>(TEST_NAMESPACE, 1, Some(previous_output), dealers, players);
assert!(matches!(result, Err(Error::NumDealers(1))));
}
#[test]
fn identify_is_bound_to_round_number() {
let mut rng = commonware_utils::test_rng();
let dealer = PrivateKey::random(&mut rng);
let player = PrivateKey::random(&mut rng);
let dealers: Set<PublicKey> = std::iter::once(dealer.public()).try_collect().unwrap();
let players: Set<PublicKey> = std::iter::once(player.public()).try_collect().unwrap();
let info =
Info::new::<N3f1>(TEST_NAMESPACE, 0, None, dealers.clone(), players.clone()).unwrap();
let next_round = Info::new::<N3f1>(TEST_NAMESPACE, 1, None, dealers, players).unwrap();
let signed = deal(&mut rng, &TEST_SETUP, &info, &dealer, None, &Sequential).unwrap();
assert!(signed.clone().identify(&info).is_some());
assert!(signed.identify(&next_round).is_none());
}
#[test]
fn identify_is_bound_to_round_summary() {
let mut rng = commonware_utils::test_rng();
let dealers: Vec<PrivateKey> = (0..4).map(|_| PrivateKey::random(&mut rng)).collect();
let players: Vec<PrivateKey> = (0..2).map(|_| PrivateKey::random(&mut rng)).collect();
let dealer_set: Set<PublicKey> = dealers
.iter()
.map(|dealer| dealer.public())
.try_collect()
.unwrap();
let player_set: Set<PublicKey> = players
.iter()
.map(|player| player.public())
.try_collect()
.unwrap();
let info = Info::new::<N3f1>(
TEST_NAMESPACE,
0,
None,
dealer_set.clone(),
player_set.clone(),
)
.unwrap();
let other_namespace =
Info::new::<N3f1>(b"other", 0, None, dealer_set.clone(), player_set.clone()).unwrap();
let other_faults =
Info::new::<N5f1>(TEST_NAMESPACE, 0, None, dealer_set, player_set).unwrap();
let signed = deal(&mut rng, &TEST_SETUP, &info, &dealers[0], None, &Sequential).unwrap();
assert!(signed.clone().identify(&info).is_some());
assert!(signed.clone().identify(&other_namespace).is_none());
assert!(signed.identify(&other_faults).is_none());
}
#[rstest]
#[case()]
#[case()]
#[case()]
#[case()]
#[test_group("slow")]
fn fuzz_plan() {
minifuzz::Builder::default()
.with_min_iterations(0)
.with_search_time(Duration::from_secs(180))
.test(|u| {
let plan: Plan = u.arbitrary()?;
let seed: u64 = u.arbitrary()?;
plan.run(&TEST_SETUP, seed, &Sequential)
.expect("plan should not panic");
Ok(())
});
}
}