use alloc::collections::{BTreeMap, BTreeSet};
use alloc::vec;
use alloc::vec::Vec;
use core::marker::PhantomData;
use crate::curve::{CurvePoint, CurveScalar};
use crate::error::Error;
use crate::reshare::{DealerCommitment, SubShare};
pub const DKG_POK_DOMAIN: &[u8] = b"frostito/dkg-pok/v1";
#[derive(Clone, Debug, PartialEq)]
pub struct ProofOfKnowledge<P: CurvePoint> {
pub r: P,
pub z: P::Scalar,
}
impl<P: CurvePoint> ProofOfKnowledge<P> {
fn challenge(dealer_index: u32, epoch: u64, constant_commitment: &P, r: &P) -> P::Scalar {
use sha2::{Digest, Sha512};
let mut h = Sha512::new();
h.update(DKG_POK_DOMAIN);
h.update(dealer_index.to_le_bytes());
h.update(epoch.to_le_bytes());
h.update(constant_commitment.compress());
h.update(r.compress());
let hash: [u8; 64] = h.finalize().into();
P::Scalar::from_bytes_wide(&hash)
}
pub fn prove<R: rand_core::RngCore + rand_core::CryptoRng>(
dealer_index: u32,
epoch: u64,
a_0: &P::Scalar,
rng: &mut R,
) -> Self {
let k = P::Scalar::random(rng);
let r = P::generator().mul_scalar(&k);
let constant_commitment = P::generator().mul_scalar(a_0);
let e = Self::challenge(dealer_index, epoch, &constant_commitment, &r);
Self {
z: k.add(&e.mul(a_0)),
r,
}
}
pub fn verify(&self, dealer_index: u32, epoch: u64, constant_commitment: &P) -> bool {
let e = Self::challenge(dealer_index, epoch, constant_commitment, &self.r);
P::generator().mul_scalar(&self.z) == self.r.add(&constant_commitment.mul_scalar(&e))
}
}
#[derive(Clone, Debug)]
pub struct Round1Package<P: CurvePoint> {
pub commitment: DealerCommitment<P>,
pub proof_of_knowledge: ProofOfKnowledge<P>,
}
impl<P: CurvePoint> Round1Package<P> {
#[inline]
pub fn dealer_index(&self) -> u32 {
self.commitment.dealer_index
}
pub fn verify(&self, epoch: u64) -> Result<(), Error> {
let idx = self.commitment.dealer_index;
if self
.proof_of_knowledge
.verify(idx, epoch, self.commitment.share_commitment())
{
Ok(())
} else {
Err(Error::InvalidProofOfKnowledge(idx))
}
}
}
pub struct Dealer<P: CurvePoint> {
index: u32,
polynomial: Vec<P::Scalar>,
commitment: DealerCommitment<P>,
}
impl<P: CurvePoint> Drop for Dealer<P> {
fn drop(&mut self) {
for coeff in &mut self.polynomial {
coeff.zeroize();
}
}
}
impl<P: CurvePoint> Dealer<P> {
pub fn new<R: rand_core::RngCore + rand_core::CryptoRng>(
index: u32,
threshold: u32,
rng: &mut R,
) -> Result<Self, Error> {
if index == 0 {
return Err(Error::InvalidIndex);
}
if threshold == 0 {
return Err(Error::ThresholdMismatch { expected: 1, got: 0 });
}
let mut polynomial = Vec::with_capacity(threshold as usize);
for _ in 0..threshold {
polynomial.push(P::Scalar::random(rng));
}
let commitment = DealerCommitment::from_polynomial(index, &polynomial)?;
Ok(Self {
index,
polynomial,
commitment,
})
}
#[inline]
pub fn index(&self) -> u32 {
self.index
}
#[inline]
pub fn commitment(&self) -> &DealerCommitment<P> {
&self.commitment
}
pub fn round1_package<R: rand_core::RngCore + rand_core::CryptoRng>(
&self,
epoch: u64,
rng: &mut R,
) -> Round1Package<P> {
Round1Package {
commitment: self.commitment.clone(),
proof_of_knowledge: ProofOfKnowledge::prove::<R>(
self.index,
epoch,
&self.polynomial[0],
rng,
),
}
}
pub fn generate_subshare(&self, player_index: u32) -> Result<SubShare<P::Scalar>, Error> {
if player_index == 0 {
return Err(Error::InvalidIndex);
}
let j = P::Scalar::from_u32(player_index);
let mut result = P::Scalar::zero();
for coeff in self.polynomial.iter().rev() {
result = result.mul(&j);
result = result.add(coeff);
}
SubShare::new(self.index, player_index, result)
}
pub fn generate_subshares(
&self,
num_players: u32,
) -> Result<Vec<SubShare<P::Scalar>>, Error> {
(1..=num_players).map(|j| self.generate_subshare(j)).collect()
}
}
impl<P: CurvePoint> core::fmt::Debug for Dealer<P> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("dkg::Dealer")
.field("index", &self.index)
.field("polynomial", &"[REDACTED]")
.field("commitment", &self.commitment)
.finish()
}
}
pub struct Aggregator<P: CurvePoint> {
player_index: u32,
dealer_set: Vec<u32>,
subshares: Vec<(u32, P::Scalar)>,
constant_commitments: Vec<P>,
_marker: PhantomData<P>,
}
impl<P: CurvePoint> Aggregator<P> {
pub fn new(player_index: u32, dealer_set: &[u32]) -> Result<Self, Error> {
if player_index == 0 {
return Err(Error::InvalidIndex);
}
if dealer_set.is_empty() {
return Err(Error::EmptyContributions);
}
let mut sorted = dealer_set.to_vec();
sorted.sort_unstable();
for w in sorted.windows(2) {
if w[0] == w[1] {
return Err(Error::DuplicateIndex(w[0]));
}
}
if sorted[0] == 0 {
return Err(Error::InvalidIndex);
}
Ok(Self {
player_index,
dealer_set: sorted,
subshares: Vec::new(),
constant_commitments: Vec::new(),
_marker: PhantomData,
})
}
pub fn all_dealers(player_index: u32, n: u32) -> Result<Self, Error> {
let set: alloc::vec::Vec<u32> = (1..=n).collect();
Self::new(player_index, &set)
}
pub fn from_agreed(
player_index: u32,
agreed: &AgreedRound1<P>,
) -> Result<Self, Error> {
Self::new(player_index, &agreed.dealer_set())
}
#[inline]
pub fn player_index(&self) -> u32 {
self.player_index
}
#[inline]
pub fn dealer_set(&self) -> &[u32] {
&self.dealer_set
}
#[inline]
pub fn count(&self) -> usize {
self.subshares.len()
}
#[inline]
pub fn is_complete(&self) -> bool {
self.subshares.len() == self.dealer_set.len()
}
pub fn missing_dealers(&self) -> Vec<u32> {
self.dealer_set
.iter()
.copied()
.filter(|d| !self.subshares.iter().any(|(i, _)| i == d))
.collect()
}
pub fn add_subshare(
&mut self,
subshare: SubShare<P::Scalar>,
commitment: &DealerCommitment<P>,
) -> Result<bool, Error> {
if subshare.player_index != self.player_index {
return Err(Error::InvalidIndex);
}
if subshare.dealer_index != commitment.dealer_index {
return Err(Error::InvalidIndex);
}
if subshare.dealer_index == 0 {
return Err(Error::InvalidIndex);
}
if self.dealer_set.binary_search(&subshare.dealer_index).is_err() {
return Err(Error::UnexpectedDealer(subshare.dealer_index));
}
if self
.subshares
.iter()
.any(|(idx, _)| *idx == subshare.dealer_index)
{
return Ok(false);
}
if !commitment.verify_subshare(self.player_index, subshare.value()) {
return Err(Error::InvalidSubShare(subshare.dealer_index));
}
self.subshares
.push((subshare.dealer_index, subshare.value().clone()));
self.constant_commitments
.push(commitment.share_commitment().clone());
Ok(true)
}
pub fn derive_group_key(&self) -> Result<P, Error> {
self.require_complete()?;
let mut key = P::identity();
for c0 in &self.constant_commitments {
key = key.add(c0);
}
Ok(key)
}
fn require_complete(&self) -> Result<(), Error> {
if !self.is_complete() {
return Err(Error::InsufficientContributions {
got: self.subshares.len(),
need: self.dealer_set.len(),
});
}
Ok(())
}
pub fn finalize(&self) -> Result<P::Scalar, Error> {
self.require_complete()?;
let mut share = P::Scalar::zero();
for (_, value) in &self.subshares {
share = share.add(value);
}
Ok(share)
}
}
impl<P: CurvePoint> core::fmt::Debug for Aggregator<P> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("dkg::Aggregator")
.field("player_index", &self.player_index)
.field("dealer_set", &self.dealer_set)
.field("count", &self.subshares.len())
.finish()
}
}
#[derive(Clone, Debug)]
pub struct DkgState<P: CurvePoint> {
pub epoch: u64,
pub threshold: u32,
pub num_participants: u32,
pub commitments: Vec<Option<DealerCommitment<P>>>,
disqualified: Vec<u32>,
}
impl<P: CurvePoint> DkgState<P> {
pub fn new(epoch: u64, threshold: u32, num_participants: u32) -> Self {
Self {
epoch,
threshold,
num_participants,
commitments: vec![None; num_participants as usize],
disqualified: Vec::new(),
}
}
pub fn submit_commitment(&mut self, package: Round1Package<P>) -> Result<bool, Error> {
package.verify(self.epoch)?;
let commitment = package.commitment;
if self.disqualified.contains(&commitment.dealer_index) {
return Err(Error::UnexpectedDealer(commitment.dealer_index));
}
let idx = commitment
.dealer_index
.checked_sub(1)
.ok_or(Error::InvalidIndex)? as usize;
if idx >= self.commitments.len() {
return Err(Error::InvalidIndex);
}
if commitment.threshold() != self.threshold {
return Err(Error::InvalidCommitment);
}
if self.commitments[idx].is_some() {
return Ok(false);
}
self.commitments[idx] = Some(commitment);
Ok(true)
}
pub fn commitment_count(&self) -> usize {
self.commitments.iter().filter(|c| c.is_some()).count()
}
pub fn is_complete(&self) -> bool {
self.commitment_count() + self.disqualified.len() == self.num_participants as usize
}
#[inline]
pub fn disqualified(&self) -> &[u32] {
&self.disqualified
}
pub fn qualified_dealers(&self) -> Vec<u32> {
self.commitments
.iter()
.flatten()
.map(|c| c.dealer_index)
.collect()
}
pub fn disqualify(&mut self, dealer_index: u32) -> Result<(), Error> {
let idx = dealer_index.checked_sub(1).ok_or(Error::InvalidIndex)? as usize;
if idx >= self.commitments.len() {
return Err(Error::InvalidIndex);
}
self.commitments[idx] = None;
if !self.disqualified.contains(&dealer_index) {
self.disqualified.push(dealer_index);
self.disqualified.sort_unstable();
}
let remaining = self.num_participants as usize - self.disqualified.len();
if remaining < self.threshold as usize {
return Err(Error::DkgAborted {
qualified: remaining,
need: self.threshold as usize,
});
}
Ok(())
}
pub fn derive_group_key(&self) -> Result<P, Error> {
if !self.is_complete() {
return Err(Error::InsufficientContributions {
got: self.commitment_count(),
need: self.num_participants as usize - self.disqualified.len(),
});
}
let mut key = P::identity();
for commitment in self.commitments.iter().flatten() {
key = key.add(commitment.share_commitment());
}
Ok(key)
}
pub fn derive_verification_share(&self, player_index: u32) -> Result<P, Error> {
if player_index == 0 {
return Err(Error::InvalidIndex);
}
if !self.is_complete() {
return Err(Error::InsufficientContributions {
got: self.commitment_count(),
need: self.num_participants as usize - self.disqualified.len(),
});
}
let mut vshare = P::identity();
for commitment in self.commitments.iter().flatten() {
vshare = vshare.add(&commitment.evaluate_at(player_index).expect("index is 1-indexed by construction"));
}
Ok(vshare)
}
pub fn derive_all_verification_shares(
&self,
) -> Result<alloc::collections::BTreeMap<u32, P>, Error> {
let mut map = alloc::collections::BTreeMap::new();
for j in 1..=self.num_participants {
map.insert(j, self.derive_verification_share(j)?);
}
Ok(map)
}
pub fn get_commitments(&self) -> Vec<&DealerCommitment<P>> {
self.commitments.iter().filter_map(|c| c.as_ref()).collect()
}
}
pub const COMPLAINT_SIG_DOMAIN: &[u8] = b"frostito/dkg-complaint/v1";
pub const COMMITMENT_DIGEST_DOMAIN: &[u8] = b"frostito/sealed/commitment/v1";
pub fn commitment_digest<P: CurvePoint>(commitment: &DealerCommitment<P>) -> [u8; 32] {
use sha2::{Digest, Sha512};
let mut h = Sha512::new();
h.update(COMMITMENT_DIGEST_DOMAIN);
h.update(commitment.to_bytes());
let full: [u8; 64] = h.finalize().into();
let mut out = [0u8; 32];
out.copy_from_slice(&full[..32]);
out
}
#[derive(Clone, PartialEq, Eq, zeroize::Zeroize, zeroize::ZeroizeOnDrop)]
pub struct BadSubShareEvidence {
pub dealer_index: u32,
pub recipient_index: u32,
pub session_id: [u8; 32],
pub round: u8,
pub subshare: [u8; 32],
pub agreed_digest: [u8; 32],
pub sealed_digest: [u8; 32],
}
impl core::fmt::Debug for BadSubShareEvidence {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("BadSubShareEvidence")
.field("dealer_index", &self.dealer_index)
.field("recipient_index", &self.recipient_index)
.field("round", &self.round)
.field("subshare", &"[REDACTED]")
.finish_non_exhaustive()
}
}
impl BadSubShareEvidence {
fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(109);
out.extend_from_slice(&self.dealer_index.to_le_bytes());
out.extend_from_slice(&self.recipient_index.to_le_bytes());
out.extend_from_slice(&self.session_id);
out.push(self.round);
out.extend_from_slice(&self.subshare);
out.extend_from_slice(&self.agreed_digest);
out.extend_from_slice(&self.sealed_digest);
out
}
}
#[derive(Clone, Debug)]
pub enum ComplaintEvidence<P: CurvePoint> {
ForgedProofOfKnowledge { package: Round1Package<P> },
BadSubShare { evidence: BadSubShareEvidence },
}
impl<P: CurvePoint> ComplaintEvidence<P> {
pub fn accused_index(&self) -> u32 {
match self {
Self::ForgedProofOfKnowledge { package } => package.dealer_index(),
Self::BadSubShare { evidence } => evidence.dealer_index,
}
}
fn encode_into(&self, out: &mut Vec<u8>) {
fn field(out: &mut Vec<u8>, bytes: &[u8]) {
out.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
out.extend_from_slice(bytes);
}
match self {
Self::ForgedProofOfKnowledge { package } => {
out.push(1);
field(out, &package.commitment.to_bytes());
field(out, package.proof_of_knowledge.r.compress().as_ref());
field(out, &package.proof_of_knowledge.z.to_bytes());
}
Self::BadSubShare { evidence } => {
out.push(2);
field(out, &evidence.encode());
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ComplaintVerdict {
Upheld,
Unfounded,
}
#[derive(Clone, Debug)]
pub struct Complaint<P: CurvePoint> {
pub epoch: u64,
pub session_id: [u8; 32],
pub round: u8,
pub accuser_index: u32,
pub accused_index: u32,
pub evidence: ComplaintEvidence<P>,
pub r: P,
pub s: P::Scalar,
}
impl<P: CurvePoint> Complaint<P> {
fn body(
epoch: u64,
session_id: &[u8; 32],
round: u8,
accuser_index: u32,
accused_index: u32,
evidence: &ComplaintEvidence<P>,
) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&(COMPLAINT_SIG_DOMAIN.len() as u64).to_le_bytes());
out.extend_from_slice(COMPLAINT_SIG_DOMAIN);
out.extend_from_slice(&epoch.to_le_bytes());
out.extend_from_slice(session_id);
out.push(round);
out.extend_from_slice(&accuser_index.to_le_bytes());
out.extend_from_slice(&accused_index.to_le_bytes());
evidence.encode_into(&mut out);
out
}
fn challenge(r: &P, accuser_pubkey: &P, body: &[u8]) -> P::Scalar {
use sha2::{Digest, Sha512};
let mut h = Sha512::new();
h.update(COMPLAINT_SIG_DOMAIN);
h.update(r.compress());
h.update(accuser_pubkey.compress());
h.update((body.len() as u64).to_le_bytes());
h.update(body);
let hash: [u8; 64] = h.finalize().into();
P::Scalar::from_bytes_wide(&hash)
}
pub fn sign<R: rand_core::RngCore + rand_core::CryptoRng>(
epoch: u64,
session_id: [u8; 32],
round: u8,
accuser_index: u32,
evidence: ComplaintEvidence<P>,
accuser_identity_secret: &P::Scalar,
rng: &mut R,
) -> Result<Self, Error> {
if accuser_index == 0 {
return Err(Error::InvalidIndex);
}
let accused_index = evidence.accused_index();
let body = Self::body(
epoch,
&session_id,
round,
accuser_index,
accused_index,
&evidence,
);
let accuser_pubkey = P::generator().mul_scalar(accuser_identity_secret);
let k = P::Scalar::random(rng);
let r = P::generator().mul_scalar(&k);
let e = Self::challenge(&r, &accuser_pubkey, &body);
let s = k.add(&e.mul(accuser_identity_secret));
Ok(Self {
epoch,
session_id,
round,
accuser_index,
accused_index,
evidence,
r,
s,
})
}
pub fn verify(
&self,
epoch: u64,
session_id: &[u8; 32],
accuser_identity_pubkey: &P,
agreed: Option<&AgreedRound1<P>>,
) -> Result<ComplaintVerdict, Error> {
if self.epoch != epoch || &self.session_id != session_id {
return Err(Error::InvalidComplaint);
}
if self.accuser_index == 0
|| self.accused_index == 0
|| self.accused_index != self.evidence.accused_index()
{
return Err(Error::InvalidComplaint);
}
let body = Self::body(
self.epoch,
&self.session_id,
self.round,
self.accuser_index,
self.accused_index,
&self.evidence,
);
let e = Self::challenge(&self.r, accuser_identity_pubkey, &body);
if P::generator().mul_scalar(&self.s)
!= self.r.add(&accuser_identity_pubkey.mul_scalar(&e))
{
return Err(Error::InvalidComplaint);
}
self.adjudicate(agreed)
}
pub fn adjudicate(
&self,
agreed: Option<&AgreedRound1<P>>,
) -> Result<ComplaintVerdict, Error> {
match &self.evidence {
ComplaintEvidence::ForgedProofOfKnowledge { package } => {
if package.dealer_index() != self.accused_index {
return Ok(ComplaintVerdict::Unfounded);
}
Ok(match package.verify(self.epoch) {
Ok(()) => ComplaintVerdict::Unfounded,
Err(_) => ComplaintVerdict::Upheld,
})
}
ComplaintEvidence::BadSubShare { evidence } => {
if evidence.dealer_index != self.accused_index
|| evidence.recipient_index != self.accuser_index
|| evidence.session_id != self.session_id
|| evidence.round != self.round
{
return Err(Error::InvalidComplaint);
}
let agreed = agreed.ok_or(Error::InvalidComplaint)?;
if agreed.epoch() != self.epoch {
return Err(Error::InvalidComplaint);
}
let commitment = agreed.commitment(evidence.dealer_index)?;
if commitment_digest(commitment) != evidence.agreed_digest {
return Err(Error::InvalidComplaint);
}
let scalar = P::Scalar::from_canonical_bytes(&evidence.subshare)
.ok_or(Error::InvalidComplaint)?;
Ok(
if commitment.verify_subshare(evidence.recipient_index, &scalar) {
ComplaintVerdict::Unfounded
} else {
ComplaintVerdict::Upheld
},
)
}
}
}
}
#[derive(Clone, Debug)]
pub struct ComplaintTally {
threshold: u32,
upheld: BTreeMap<u32, BTreeSet<u32>>,
flagged: BTreeSet<u32>,
}
impl ComplaintTally {
pub fn new(threshold: u32) -> Self {
Self {
threshold,
upheld: BTreeMap::new(),
flagged: BTreeSet::new(),
}
}
#[inline]
pub fn threshold(&self) -> u32 {
self.threshold
}
pub fn record(
&mut self,
accuser: u32,
accused: u32,
verdict: ComplaintVerdict,
) -> Result<(), Error> {
if accuser == 0 || accused == 0 || accuser == accused {
return Err(Error::InvalidIndex);
}
match verdict {
ComplaintVerdict::Upheld => {
self.upheld.entry(accused).or_default().insert(accuser);
}
ComplaintVerdict::Unfounded => {
self.flagged.insert(accuser);
}
}
Ok(())
}
pub fn count(&self, accused: u32) -> usize {
self.upheld.get(&accused).map_or(0, |s| s.len())
}
pub fn accusers(&self, accused: u32) -> Vec<u32> {
self.upheld
.get(&accused)
.map(|s| s.iter().copied().collect())
.unwrap_or_default()
}
pub fn reached(&self, accused: u32) -> bool {
self.count(accused) >= self.threshold as usize
}
pub fn disqualifiable(&self) -> Vec<u32> {
self.upheld
.keys()
.copied()
.filter(|d| self.reached(*d))
.collect()
}
pub fn below_threshold(&self) -> Vec<(u32, Vec<u32>)> {
self.upheld
.iter()
.filter(|(d, _)| !self.reached(**d))
.map(|(d, a)| (*d, a.iter().copied().collect()))
.collect()
}
pub fn flagged_accusers(&self) -> Vec<u32> {
self.flagged.iter().copied().collect()
}
}
pub const ECHO_DIGEST_DOMAIN: &[u8] = b"frostito/dkg-round1-echo/v1";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EchoDigest(pub [u8; 32]);
impl EchoDigest {
#[inline]
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl core::fmt::Display for EchoDigest {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
for b in &self.0 {
write!(f, "{:02x}", b)?;
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct AgreedRound1<P: CurvePoint> {
epoch: u64,
threshold: u32,
digest: EchoDigest,
commitments: Vec<DealerCommitment<P>>,
}
impl<P: CurvePoint> AgreedRound1<P> {
#[inline]
pub fn digest(&self) -> EchoDigest {
self.digest
}
#[inline]
pub fn epoch(&self) -> u64 {
self.epoch
}
#[inline]
pub fn threshold(&self) -> u32 {
self.threshold
}
pub fn dealer_set(&self) -> Vec<u32> {
self.commitments.iter().map(|c| c.dealer_index).collect()
}
#[inline]
pub fn commitments(&self) -> &[DealerCommitment<P>] {
&self.commitments
}
pub fn commitment(&self, dealer_index: u32) -> Result<&DealerCommitment<P>, Error> {
self.commitments
.iter()
.find(|c| c.dealer_index == dealer_index)
.ok_or(Error::UnexpectedDealer(dealer_index))
}
pub fn confirm(&self, peer: &EchoDigest) -> Result<(), Error> {
let mut diff = 0u8;
for (a, b) in self.digest.0.iter().zip(peer.0.iter()) {
diff |= a ^ b;
}
if diff == 0 {
Ok(())
} else {
Err(Error::EchoMismatch)
}
}
pub fn confirm_all(&self, peers: &[EchoDigest], expected: usize) -> Result<(), Error> {
if peers.len() < expected {
return Err(Error::InsufficientContributions {
got: peers.len(),
need: expected,
});
}
for p in peers {
self.confirm(p)?;
}
Ok(())
}
}
impl<P: CurvePoint> DkgState<P> {
pub fn agreed_round1(&self) -> Result<AgreedRound1<P>, Error> {
if !self.is_complete() {
return Err(Error::InsufficientContributions {
got: self.commitment_count(),
need: self.num_participants as usize - self.disqualified.len(),
});
}
let mut commitments: Vec<DealerCommitment<P>> =
self.commitments.iter().flatten().cloned().collect();
commitments.sort_by_key(|c| c.dealer_index);
let digest = round1_echo_digest::<P>(
self.epoch,
self.threshold,
self.num_participants,
&commitments,
);
Ok(AgreedRound1 {
epoch: self.epoch,
threshold: self.threshold,
digest,
commitments,
})
}
}
pub fn round1_echo_digest<P: CurvePoint>(
epoch: u64,
threshold: u32,
num_participants: u32,
commitments: &[DealerCommitment<P>],
) -> EchoDigest {
use sha2::{Digest, Sha512};
let mut sorted: Vec<&DealerCommitment<P>> = commitments.iter().collect();
sorted.sort_by_key(|c| c.dealer_index);
let mut h = Sha512::new();
h.update(ECHO_DIGEST_DOMAIN);
h.update(epoch.to_le_bytes());
h.update(threshold.to_le_bytes());
h.update(num_participants.to_le_bytes());
h.update((sorted.len() as u32).to_le_bytes());
for c in sorted {
let bytes = c.to_bytes();
h.update(c.dealer_index.to_le_bytes());
h.update((bytes.len() as u64).to_le_bytes());
h.update(&bytes);
}
let full: [u8; 64] = h.finalize().into();
let mut out = [0u8; 32];
out.copy_from_slice(&full[..32]);
EchoDigest(out)
}
#[cfg(all(test, feature = "ristretto255"))]
mod tests {
use super::*;
use crate::{compute_lagrange_coefficients, SecretShare};
fn reconstructs<P: CurvePoint>(shares: &[(u32, P::Scalar)], group_key: &P) -> bool {
let indices: Vec<u32> = shares.iter().map(|(i, _)| *i).collect();
let lambdas: Vec<P::Scalar> =
compute_lagrange_coefficients(&indices).expect("distinct nonzero indices");
let secret = shares
.iter()
.zip(lambdas.iter())
.fold(P::Scalar::zero(), |acc, ((_, s), l)| acc.add(&l.mul(s)));
P::generator().mul_scalar(&secret) == *group_key
}
use curve25519_dalek::{ristretto::RistrettoPoint, scalar::Scalar};
use rand::rngs::OsRng;
const CSESSION: [u8; 32] = [0x5Au8; 32];
#[test]
fn a_forged_proof_of_knowledge_is_publicly_adjudicable() {
let mut rng = OsRng;
let epoch = 3u64;
let honest: Dealer<RistrettoPoint> = Dealer::new(2, 2, &mut rng).unwrap();
let other: Dealer<RistrettoPoint> = Dealer::new(2, 2, &mut rng).unwrap();
let mut forged = honest.round1_package(epoch, &mut rng);
forged.proof_of_knowledge = other.round1_package(epoch, &mut rng).proof_of_knowledge;
assert!(forged.verify(epoch).is_err());
let accuser_secret = Scalar::random(&mut rng);
let accuser_pk: RistrettoPoint =
<RistrettoPoint as CurvePoint>::generator().mul_scalar(&accuser_secret);
let complaint = Complaint::<RistrettoPoint>::sign(
epoch,
CSESSION,
1,
1,
ComplaintEvidence::ForgedProofOfKnowledge { package: forged },
&accuser_secret,
&mut rng,
)
.unwrap();
assert_eq!(complaint.accused_index, 2);
assert_eq!(
complaint
.verify(epoch, &CSESSION, &accuser_pk, None)
.unwrap(),
ComplaintVerdict::Upheld,
);
let unfounded = Complaint::<RistrettoPoint>::sign(
epoch,
CSESSION,
1,
1,
ComplaintEvidence::ForgedProofOfKnowledge {
package: honest.round1_package(epoch, &mut rng),
},
&accuser_secret,
&mut rng,
)
.unwrap();
assert_eq!(
unfounded
.verify(epoch, &CSESSION, &accuser_pk, None)
.unwrap(),
ComplaintVerdict::Unfounded,
"the accuser, not the accused, is the problem here"
);
}
fn agreed_of(
dealers: &[Dealer<RistrettoPoint>],
t: u32,
n: u32,
epoch: u64,
rng: &mut OsRng,
) -> AgreedRound1<RistrettoPoint> {
let mut state = DkgState::<RistrettoPoint>::new(epoch, t, n);
for d in dealers {
state.submit_commitment(d.round1_package(epoch, rng)).unwrap();
}
state.agreed_round1().unwrap()
}
fn bad_subshare_evidence(
dealer_index: u32,
recipient_index: u32,
round: u8,
scalar: &Scalar,
agreed: &AgreedRound1<RistrettoPoint>,
) -> BadSubShareEvidence {
BadSubShareEvidence {
dealer_index,
recipient_index,
session_id: CSESSION,
round,
subshare: <Scalar as CurveScalar>::to_bytes(scalar),
agreed_digest: commitment_digest(agreed.commitment(dealer_index).unwrap()),
sealed_digest: [9u8; 32],
}
}
#[test]
fn a_bad_subshare_complaint_is_checked_against_the_agreed_commitment() {
let mut rng = OsRng;
let epoch = 11u64;
let dealers: Vec<Dealer<RistrettoPoint>> = (1..=2)
.map(|i| Dealer::new(i, 2, &mut rng).unwrap())
.collect();
let agreed = agreed_of(&dealers, 2, 2, epoch, &mut rng);
let accuser_secret = Scalar::random(&mut rng);
let accuser_pk: RistrettoPoint =
<RistrettoPoint as CurvePoint>::generator().mul_scalar(&accuser_secret);
let bogus = Scalar::random(&mut rng);
let complaint = Complaint::<RistrettoPoint>::sign(
epoch,
CSESSION,
2,
1,
ComplaintEvidence::BadSubShare {
evidence: bad_subshare_evidence(2, 1, 2, &bogus, &agreed),
},
&accuser_secret,
&mut rng,
)
.unwrap();
assert_eq!(
complaint
.verify(epoch, &CSESSION, &accuser_pk, Some(&agreed))
.unwrap(),
ComplaintVerdict::Upheld
);
let good = dealers[1].generate_subshare(1).unwrap();
let honest_claim = Complaint::<RistrettoPoint>::sign(
epoch,
CSESSION,
2,
1,
ComplaintEvidence::BadSubShare {
evidence: bad_subshare_evidence(2, 1, 2, good.value(), &agreed),
},
&accuser_secret,
&mut rng,
)
.unwrap();
assert_eq!(
honest_claim
.verify(epoch, &CSESSION, &accuser_pk, Some(&agreed))
.unwrap(),
ComplaintVerdict::Unfounded,
"an accuser complaining about a valid share names itself"
);
assert_eq!(
complaint.verify(epoch, &CSESSION, &accuser_pk, None),
Err(Error::InvalidComplaint),
"a round-2 complaint cannot be adjudicated without the agreed set"
);
let other: Dealer<RistrettoPoint> = Dealer::new(2, 2, &mut rng).unwrap();
let mut ev = bad_subshare_evidence(2, 1, 2, &bogus, &agreed);
ev.agreed_digest = commitment_digest(other.commitment());
let substituted = Complaint::<RistrettoPoint>::sign(
epoch,
CSESSION,
2,
1,
ComplaintEvidence::BadSubShare { evidence: ev },
&accuser_secret,
&mut rng,
)
.unwrap();
assert_eq!(
substituted.verify(epoch, &CSESSION, &accuser_pk, Some(&agreed)),
Err(Error::InvalidComplaint),
"the evidence is checked against the verifier's commitment, not the accuser's"
);
assert_eq!(
complaint.verify(epoch + 1, &CSESSION, &accuser_pk, Some(&agreed)),
Err(Error::InvalidComplaint)
);
assert_eq!(
complaint.verify(epoch, &[0u8; 32], &accuser_pk, Some(&agreed)),
Err(Error::InvalidComplaint)
);
let impostor: RistrettoPoint =
<RistrettoPoint as CurvePoint>::generator().mul_scalar(&Scalar::random(&mut rng));
assert_eq!(
complaint.verify(epoch, &CSESSION, &impostor, Some(&agreed)),
Err(Error::InvalidComplaint)
);
}
#[test]
fn a_fabricated_complaint_is_upheld_too_so_one_complaint_decides_nothing() {
let mut rng = OsRng;
let (n, t, epoch) = (3u32, 2u32, 42u64);
let dealers: Vec<Dealer<RistrettoPoint>> = (1..=n)
.map(|i| Dealer::new(i, t, &mut rng).unwrap())
.collect();
let agreed = agreed_of(&dealers, t, n, epoch, &mut rng);
for j in 1..=n {
let s = dealers[2].generate_subshare(j).unwrap();
assert!(agreed
.commitment(3)
.unwrap()
.verify_subshare(j, s.value()));
}
let liar_secret = Scalar::random(&mut rng);
let liar_pk: RistrettoPoint =
<RistrettoPoint as CurvePoint>::generator().mul_scalar(&liar_secret);
let fabricated = Complaint::<RistrettoPoint>::sign(
epoch,
CSESSION,
2,
1,
ComplaintEvidence::BadSubShare {
evidence: bad_subshare_evidence(3, 1, 2, &Scalar::random(&mut rng), &agreed),
},
&liar_secret,
&mut rng,
)
.unwrap();
let verdict = fabricated
.verify(epoch, &CSESSION, &liar_pk, Some(&agreed))
.unwrap();
assert_eq!(
verdict,
ComplaintVerdict::Upheld,
"Upheld and Unfounded cannot tell an honest recipient from a lying one"
);
let mut tally = ComplaintTally::new(t);
tally.record(1, 3, verdict).unwrap();
assert_eq!(tally.count(3), 1);
assert!(
!tally.reached(3),
"one accuser is below t: an honest dealer survives a lone liar"
);
assert!(tally.disqualifiable().is_empty());
assert_eq!(tally.below_threshold(), alloc::vec![(3, alloc::vec![1])]);
tally.record(2, 3, ComplaintVerdict::Upheld).unwrap();
assert!(tally.reached(3));
assert_eq!(tally.disqualifiable(), alloc::vec![3]);
}
#[test]
fn the_tally_counts_distinct_accusers_and_gates_on_the_threshold() {
let mut tally = ComplaintTally::new(3);
assert_eq!(tally.threshold(), 3);
for _ in 0..5 {
tally.record(1, 4, ComplaintVerdict::Upheld).unwrap();
}
assert_eq!(tally.count(4), 1);
assert!(!tally.reached(4));
tally.record(2, 4, ComplaintVerdict::Upheld).unwrap();
assert!(!tally.reached(4), "two of three is not a quorum");
tally.record(5, 4, ComplaintVerdict::Upheld).unwrap();
assert!(tally.reached(4));
assert_eq!(tally.accusers(4), alloc::vec![1, 2, 5]);
assert_eq!(tally.disqualifiable(), alloc::vec![4]);
assert!(tally.below_threshold().is_empty());
tally.record(6, 7, ComplaintVerdict::Unfounded).unwrap();
assert_eq!(tally.count(7), 0);
assert_eq!(tally.flagged_accusers(), alloc::vec![6]);
assert_eq!(
tally.record(2, 2, ComplaintVerdict::Upheld),
Err(Error::InvalidIndex)
);
assert_eq!(
tally.record(0, 2, ComplaintVerdict::Upheld),
Err(Error::InvalidIndex)
);
assert_eq!(
tally.record(2, 0, ComplaintVerdict::Upheld),
Err(Error::InvalidIndex)
);
}
#[test]
fn a_complaint_cannot_be_relabelled() {
let mut rng = OsRng;
let epoch = 2u64;
let dealers: Vec<Dealer<RistrettoPoint>> = (1..=2)
.map(|i| Dealer::new(i, 2, &mut rng).unwrap())
.collect();
let agreed = agreed_of(&dealers, 2, 2, epoch, &mut rng);
let accuser_secret = Scalar::random(&mut rng);
let accuser_pk: RistrettoPoint =
<RistrettoPoint as CurvePoint>::generator().mul_scalar(&accuser_secret);
let bogus = Scalar::random(&mut rng);
let mut complaint = Complaint::<RistrettoPoint>::sign(
epoch,
CSESSION,
2,
1,
ComplaintEvidence::BadSubShare {
evidence: bad_subshare_evidence(2, 1, 2, &bogus, &agreed),
},
&accuser_secret,
&mut rng,
)
.unwrap();
complaint.accused_index = 3;
assert_eq!(
complaint.verify(epoch, &CSESSION, &accuser_pk, Some(&agreed)),
Err(Error::InvalidComplaint),
"the named dealer must agree with the evidence"
);
complaint.accused_index = 2;
complaint.accuser_index = 9;
assert_eq!(
complaint.verify(epoch, &CSESSION, &accuser_pk, Some(&agreed)),
Err(Error::InvalidComplaint),
"the accuser is inside the signed body"
);
let mut drifted = Complaint::<RistrettoPoint>::sign(
epoch,
CSESSION,
2,
1,
ComplaintEvidence::BadSubShare {
evidence: bad_subshare_evidence(2, 1, 2, &bogus, &agreed),
},
&accuser_secret,
&mut rng,
)
.unwrap();
if let ComplaintEvidence::BadSubShare { evidence } = &mut drifted.evidence {
evidence.round = 7;
}
assert_eq!(
drifted.verify(epoch, &CSESSION, &accuser_pk, Some(&agreed)),
Err(Error::InvalidComplaint),
"the evidence's binding is not decorative"
);
}
#[test]
fn honest_participants_echo_the_same_round1_digest() {
let mut rng = OsRng;
let (n, t, epoch) = (4u32, 3u32, 77u64);
let dealers: Vec<Dealer<RistrettoPoint>> = (1..=n)
.map(|i| Dealer::new(i, t, &mut rng).unwrap())
.collect();
let packages: Vec<_> = dealers
.iter()
.map(|d| d.round1_package(epoch, &mut rng))
.collect();
let mut alice = DkgState::<RistrettoPoint>::new(epoch, t, n);
let mut bob = DkgState::<RistrettoPoint>::new(epoch, t, n);
for p in &packages {
alice.submit_commitment(p.clone()).unwrap();
}
for p in packages.iter().rev() {
bob.submit_commitment(p.clone()).unwrap();
}
let a = alice.agreed_round1().unwrap();
let b = bob.agreed_round1().unwrap();
assert_eq!(a.digest(), b.digest(), "delivery order must not matter");
a.confirm(&b.digest()).unwrap();
a.confirm_all(&[a.digest(), b.digest()], 2).unwrap();
assert_eq!(a.dealer_set(), (1..=n).collect::<Vec<_>>());
let mut other_epoch = DkgState::<RistrettoPoint>::new(epoch + 1, t, n);
for d in &dealers {
other_epoch
.submit_commitment(d.round1_package(epoch + 1, &mut rng))
.unwrap();
}
assert_ne!(
a.digest(),
other_epoch.agreed_round1().unwrap().digest(),
"the digest must bind the ceremony, not just the commitments"
);
}
#[test]
fn an_equivocating_dealer_is_caught_by_the_echo_round() {
let mut rng = OsRng;
let (n, t, epoch) = (3u32, 2u32, 9u64);
let honest: Vec<Dealer<RistrettoPoint>> = (2..=n)
.map(|i| Dealer::new(i, t, &mut rng).unwrap())
.collect();
let honest_packages: Vec<_> = honest
.iter()
.map(|d| d.round1_package(epoch, &mut rng))
.collect();
let evil_a: Dealer<RistrettoPoint> = Dealer::new(1, t, &mut rng).unwrap();
let evil_b: Dealer<RistrettoPoint> = Dealer::new(1, t, &mut rng).unwrap();
let pkg_a = evil_a.round1_package(epoch, &mut rng);
let pkg_b = evil_b.round1_package(epoch, &mut rng);
assert_ne!(pkg_a.commitment.to_bytes(), pkg_b.commitment.to_bytes());
pkg_a.verify(epoch).unwrap();
pkg_b.verify(epoch).unwrap();
let mut alice = DkgState::<RistrettoPoint>::new(epoch, t, n);
let mut bob = DkgState::<RistrettoPoint>::new(epoch, t, n);
alice.submit_commitment(pkg_a).unwrap();
bob.submit_commitment(pkg_b).unwrap();
for p in &honest_packages {
alice.submit_commitment(p.clone()).unwrap();
bob.submit_commitment(p.clone()).unwrap();
}
let a = alice.agreed_round1().unwrap();
let b = bob.agreed_round1().unwrap();
assert_ne!(
alice.derive_group_key().unwrap(),
bob.derive_group_key().unwrap(),
"the equivocation really does split the group key"
);
assert_eq!(a.confirm(&b.digest()), Err(Error::EchoMismatch));
assert_eq!(b.confirm(&a.digest()), Err(Error::EchoMismatch));
assert_eq!(
a.confirm_all(&[a.digest(), b.digest()], 2),
Err(Error::EchoMismatch)
);
}
#[test]
fn a_partial_round1_set_has_no_echo_digest() {
let mut rng = OsRng;
let (n, t, epoch) = (3u32, 2u32, 1u64);
let mut st = DkgState::<RistrettoPoint>::new(epoch, t, n);
let d: Dealer<RistrettoPoint> = Dealer::new(1, t, &mut rng).unwrap();
st.submit_commitment(d.round1_package(epoch, &mut rng)).unwrap();
assert_eq!(
st.agreed_round1().unwrap_err(),
Error::InsufficientContributions { got: 1, need: 3 }
);
}
#[test]
fn the_agreed_set_drives_the_aggregator_and_the_lookup() {
let mut rng = OsRng;
let (n, t, epoch) = (3u32, 2u32, 5u64);
let dealers: Vec<Dealer<RistrettoPoint>> = (1..=n)
.map(|i| Dealer::new(i, t, &mut rng).unwrap())
.collect();
let mut st = DkgState::<RistrettoPoint>::new(epoch, t, n);
for d in &dealers {
st.submit_commitment(d.round1_package(epoch, &mut rng)).unwrap();
}
let agreed = st.agreed_round1().unwrap();
let mut agg = Aggregator::<RistrettoPoint>::from_agreed(2, &agreed).unwrap();
assert_eq!(agg.dealer_set(), &[1, 2, 3]);
for d in &dealers {
let sub = d.generate_subshare(2).unwrap();
let c = agreed.commitment(d.index()).unwrap();
assert!(agg.add_subshare(sub, c).unwrap());
}
assert!(agg.is_complete());
assert_eq!(agg.derive_group_key().unwrap(), st.derive_group_key().unwrap());
assert_eq!(
agreed.commitment(9).unwrap_err(),
Error::UnexpectedDealer(9)
);
}
#[test]
fn test_basic_dkg() {
let mut rng = OsRng;
let n = 5u32;
let t = 3u32;
let dealers: Vec<Dealer<RistrettoPoint>> =
(1..=n).map(|i| Dealer::new(i, t, &mut rng).expect("index is 1-indexed by construction")).collect();
let commitments: Vec<&DealerCommitment<RistrettoPoint>> =
dealers.iter().map(|d| d.commitment()).collect();
let mut shares = Vec::new();
for j in 1..=n {
let mut agg: Aggregator<RistrettoPoint> = Aggregator::all_dealers(j, n).unwrap();
for dealer in &dealers {
let subshare = dealer.generate_subshare(j).expect("index is 1-indexed by construction");
agg.add_subshare(subshare, commitments[(dealer.index() - 1) as usize])
.unwrap();
}
let share = agg.finalize().unwrap();
let group_key = agg.derive_group_key().unwrap();
shares.push((share, group_key));
}
let group_key = shares[0].1;
for (_, gk) in &shares {
assert_eq!(*gk, group_key);
}
let picked: Vec<(u32, Scalar)> = shares[0..t as usize]
.iter()
.enumerate()
.map(|(i, (s, _))| ((i + 1) as u32, *s))
.collect();
assert!(
reconstructs::<RistrettoPoint>(&picked, &group_key),
"t DKG shares must reconstruct the group key"
);
}
#[test]
fn test_dkg_state() {
let mut rng = OsRng;
let n = 5u32;
let t = 3u32;
let dealers: Vec<Dealer<RistrettoPoint>> =
(1..=n).map(|i| Dealer::new(i, t, &mut rng).expect("index is 1-indexed by construction")).collect();
let mut state: DkgState<RistrettoPoint> = DkgState::new(1, t, n);
assert!(!state.is_complete());
for dealer in &dealers {
state
.submit_commitment(dealer.round1_package(1, &mut rng))
.unwrap();
}
assert!(state.is_complete());
assert_eq!(state.commitment_count(), n as usize);
let state_key = state.derive_group_key().unwrap();
let mut agg: Aggregator<RistrettoPoint> = Aggregator::all_dealers(1, n).unwrap();
for dealer in &dealers {
let subshare = dealer.generate_subshare(1).expect("index is 1-indexed by construction");
agg.add_subshare(subshare, dealer.commitment()).unwrap();
}
let agg_key = agg.derive_group_key().unwrap();
assert_eq!(state_key, agg_key);
}
#[test]
fn test_dkg_bad_subshare_rejected() {
let mut rng = OsRng;
let n = 3u32;
let t = 2u32;
let dealers: Vec<Dealer<RistrettoPoint>> =
(1..=n).map(|i| Dealer::new(i, t, &mut rng).expect("index is 1-indexed by construction")).collect();
let mut agg: Aggregator<RistrettoPoint> = Aggregator::all_dealers(1, n).unwrap();
let subshare = dealers[0].generate_subshare(1).expect("index is 1-indexed by construction");
assert!(agg.add_subshare(subshare, dealers[0].commitment()).is_ok());
let bad = SubShare::new(2, 1, Scalar::random(&mut rng)).expect("index is 1-indexed by construction");
let result = agg.add_subshare(bad, dealers[1].commitment());
assert!(matches!(result, Err(Error::InvalidSubShare(_))));
}
#[test]
fn test_dkg_duplicate_rejected() {
let mut rng = OsRng;
let dealer: Dealer<RistrettoPoint> = Dealer::new(1, 2, &mut rng).expect("index is 1-indexed by construction");
let mut agg: Aggregator<RistrettoPoint> = Aggregator::new(1, &[1]).unwrap();
let subshare = dealer.generate_subshare(1).expect("index is 1-indexed by construction");
assert!(agg.add_subshare(subshare, dealer.commitment()).unwrap());
let subshare2 = dealer.generate_subshare(1).expect("index is 1-indexed by construction");
assert!(!agg.add_subshare(subshare2, dealer.commitment()).unwrap());
}
#[test]
fn test_dkg_non_consecutive_subset_verifies() {
let mut rng = OsRng;
let n = 7u32;
let t = 4u32;
let dealers: Vec<Dealer<RistrettoPoint>> =
(1..=n).map(|i| Dealer::new(i, t, &mut rng).expect("index is 1-indexed by construction")).collect();
let commitments: Vec<&DealerCommitment<RistrettoPoint>> =
dealers.iter().map(|d| d.commitment()).collect();
let mut secret_shares = Vec::new();
let mut group_key = None;
for j in 1..=n {
let mut agg: Aggregator<RistrettoPoint> = Aggregator::all_dealers(j, n).unwrap();
for dealer in &dealers {
let subshare = dealer.generate_subshare(j).expect("index is 1-indexed by construction");
agg.add_subshare(subshare, commitments[(dealer.index() - 1) as usize])
.unwrap();
}
if group_key.is_none() {
group_key = Some(agg.derive_group_key().unwrap());
}
secret_shares.push(SecretShare::new(j, agg.finalize().unwrap()).expect("index is 1-indexed by construction"));
}
let group_key = group_key.unwrap();
let picked: Vec<(u32, Scalar)> = [0usize, 2, 4, 6]
.iter()
.map(|&i| (secret_shares[i].index, *secret_shares[i].scalar()))
.collect();
assert!(reconstructs::<RistrettoPoint>(&picked, &group_key));
}
#[test]
fn test_dkg_subset_dealer_set_enforced() {
let mut rng = OsRng;
let n = 5u32;
let t = 3u32;
let dealers: Vec<Dealer<RistrettoPoint>> =
(1..=n).map(|i| Dealer::new(i, t, &mut rng).expect("index is 1-indexed by construction")).collect();
let set = [1u32, 3, 4];
let mut agg: Aggregator<RistrettoPoint> = Aggregator::new(2, &set).unwrap();
assert_eq!(
agg.add_subshare(dealers[4].generate_subshare(2).expect("index is 1-indexed by construction"), dealers[4].commitment()),
Err(Error::UnexpectedDealer(5))
);
for &i in &set {
let d = &dealers[(i - 1) as usize];
agg.add_subshare(d.generate_subshare(2).expect("index is 1-indexed by construction"), d.commitment()).unwrap();
}
assert!(agg.is_complete());
let mut expected = RistrettoPoint::identity();
for &i in &set {
expected = expected.add(dealers[(i - 1) as usize].commitment().share_commitment());
}
assert_eq!(agg.derive_group_key().unwrap(), expected);
let mut partial: Aggregator<RistrettoPoint> = Aggregator::new(2, &set).unwrap();
partial
.add_subshare(dealers[0].generate_subshare(2).expect("index is 1-indexed by construction"), dealers[0].commitment())
.unwrap();
assert_eq!(partial.missing_dealers(), vec![3, 4]);
assert!(matches!(
partial.finalize(),
Err(Error::InsufficientContributions { got: 1, need: 3 })
));
}
#[test]
fn pok_verifies_for_an_honest_dealer() {
let mut rng = OsRng;
let dealer: Dealer<RistrettoPoint> = Dealer::new(2, 3, &mut rng).expect("index is 1-indexed by construction");
let pkg = dealer.round1_package(7, &mut rng);
assert!(pkg.verify(7).is_ok());
}
#[test]
fn pok_is_bound_to_the_dealer_index_the_epoch_and_the_commitment() {
let mut rng = OsRng;
let dealer: Dealer<RistrettoPoint> = Dealer::new(2, 3, &mut rng).expect("index is 1-indexed by construction");
let pkg = dealer.round1_package(7, &mut rng);
assert_eq!(pkg.verify(8), Err(Error::InvalidProofOfKnowledge(2)));
let mut stolen = pkg.clone();
stolen.commitment.dealer_index = 3;
assert_eq!(stolen.verify(7), Err(Error::InvalidProofOfKnowledge(3)));
let other: Dealer<RistrettoPoint> = Dealer::new(2, 3, &mut rng).expect("index is 1-indexed by construction");
let mut rogue = pkg.clone();
rogue.commitment = other.commitment().clone();
assert_eq!(rogue.verify(7), Err(Error::InvalidProofOfKnowledge(2)));
}
#[test]
fn dkg_state_rejects_a_dealer_that_cannot_prove_knowledge() {
let mut rng = OsRng;
let honest: Dealer<RistrettoPoint> = Dealer::new(1, 2, &mut rng).expect("index is 1-indexed by construction");
let other: Dealer<RistrettoPoint> = Dealer::new(2, 2, &mut rng).expect("index is 1-indexed by construction");
let mut state: DkgState<RistrettoPoint> = DkgState::new(1, 2, 3);
let mut forged = other.round1_package(1, &mut rng);
forged.commitment = honest.commitment().clone();
forged.commitment.dealer_index = 2;
assert_eq!(
state.submit_commitment(forged),
Err(Error::InvalidProofOfKnowledge(2))
);
assert_eq!(state.commitment_count(), 0, "nothing was recorded");
assert!(state
.submit_commitment(honest.round1_package(1, &mut rng))
.unwrap());
}
#[test]
fn a_complaint_disqualifies_a_dealer_and_moves_the_group_key() {
let mut rng = OsRng;
let n = 3u32;
let t = 2u32;
let dealers: Vec<Dealer<RistrettoPoint>> =
(1..=n).map(|i| Dealer::new(i, t, &mut rng).expect("index is 1-indexed by construction")).collect();
let mut state: DkgState<RistrettoPoint> = DkgState::new(9, t, n);
for d in &dealers {
state.submit_commitment(d.round1_package(9, &mut rng)).unwrap();
}
let key_all = state.derive_group_key().unwrap();
let mut agg: Aggregator<RistrettoPoint> = Aggregator::all_dealers(1, n).unwrap();
let bad = SubShare::new(3, 1, Scalar::random(&mut rng)).expect("index is 1-indexed by construction");
assert_eq!(
agg.add_subshare(bad, dealers[2].commitment()),
Err(Error::InvalidSubShare(3))
);
state.disqualify(3).unwrap();
assert_eq!(state.disqualified(), &[3]);
assert_eq!(state.qualified_dealers(), vec![1, 2]);
assert!(state.is_complete(), "complete over the surviving dealers");
let key_qualified = state.derive_group_key().unwrap();
assert_ne!(key_all, key_qualified);
assert_eq!(
key_qualified,
dealers[0]
.commitment()
.share_commitment()
.add(dealers[1].commitment().share_commitment())
);
assert_eq!(
state.submit_commitment(dealers[2].round1_package(9, &mut rng)),
Err(Error::UnexpectedDealer(3))
);
}
#[test]
fn disqualifying_below_threshold_aborts_the_ceremony() {
let mut rng = OsRng;
let dealers: Vec<Dealer<RistrettoPoint>> =
(1..=3).map(|i| Dealer::new(i, 3, &mut rng).expect("index is 1-indexed by construction")).collect();
let mut state: DkgState<RistrettoPoint> = DkgState::new(1, 3, 3);
for d in &dealers {
state.submit_commitment(d.round1_package(1, &mut rng)).unwrap();
}
assert_eq!(
state.disqualify(2),
Err(Error::DkgAborted {
qualified: 2,
need: 3
})
);
}
}
#[cfg(all(test, feature = "pallas"))]
mod pallas_tests {
use super::*;
use crate::{compute_lagrange_coefficients, SecretShare};
fn reconstructs<P: CurvePoint>(shares: &[(u32, P::Scalar)], group_key: &P) -> bool {
let indices: Vec<u32> = shares.iter().map(|(i, _)| *i).collect();
let lambdas: Vec<P::Scalar> =
compute_lagrange_coefficients(&indices).expect("distinct nonzero indices");
let secret = shares
.iter()
.zip(lambdas.iter())
.fold(P::Scalar::zero(), |acc, ((_, s), l)| acc.add(&l.mul(s)));
P::generator().mul_scalar(&secret) == *group_key
}
use pasta_curves::pallas::Point;
use rand::rngs::OsRng;
#[test]
fn test_pallas_dkg() {
let mut rng = OsRng;
let n = 5u32;
let t = 3u32;
let dealers: Vec<Dealer<Point>> =
(1..=n).map(|i| Dealer::new(i, t, &mut rng).expect("index is 1-indexed by construction")).collect();
let commitments: Vec<&DealerCommitment<Point>> =
dealers.iter().map(|d| d.commitment()).collect();
let mut shares = Vec::new();
let mut group_key = None;
for j in 1..=n {
let mut agg: Aggregator<Point> = Aggregator::all_dealers(j, n).unwrap();
for dealer in &dealers {
let subshare = dealer.generate_subshare(j).expect("index is 1-indexed by construction");
agg.add_subshare(subshare, commitments[(dealer.index() - 1) as usize])
.unwrap();
}
if group_key.is_none() {
group_key = Some(agg.derive_group_key().unwrap());
}
shares.push(SecretShare::new(j, agg.finalize().unwrap()).expect("index is 1-indexed by construction"));
}
let group_key = group_key.unwrap();
let picked: Vec<(u32, _)> = shares[0..t as usize]
.iter()
.map(|s| (s.index, *s.scalar()))
.collect();
assert!(reconstructs::<Point>(&picked, &group_key));
}
}