use alloc::vec;
use alloc::vec::Vec;
use hkdf::Hkdf;
use sha2::{Digest, Sha256};
use x25519_dalek::{PublicKey, StaticSecret};
use crate::curve::{CurvePoint, CurveScalar};
use crate::dkg::BadSubShareEvidence;
use crate::error::Error;
use crate::reshare::{DealerCommitment, SubShare};
pub const NOISE_PATTERN: &str = "Noise_K_25519_ChaChaPoly_BLAKE2s";
pub const X25519_DERIVE_INFO: &[u8] = b"frostito/sealed/x25519/v1";
pub const TRANSCRIPT_DOMAIN: &[u8] = b"frostito/sealed/transcript/v1";
pub const PROLOGUE_DOMAIN: &[u8] = b"frostito/sealed/prologue/v1";
pub const SEALED_DIGEST_DOMAIN: &[u8] = b"frostito/sealed/ciphertext/v1";
pub use crate::dkg::COMMITMENT_DIGEST_DOMAIN;
pub fn x25519_secret_from_seed(seed: &[u8; 32]) -> [u8; 32] {
let hk = Hkdf::<Sha256>::new(None, seed);
let mut out = [0u8; 32];
hk.expand(X25519_DERIVE_INFO, &mut out)
.expect("32 bytes is a valid HKDF-SHA256 length");
out
}
pub fn x25519_public_from_seed(seed: &[u8; 32]) -> [u8; 32] {
let sk = StaticSecret::from(x25519_secret_from_seed(seed));
PublicKey::from(&sk).to_bytes()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SealedRoster {
participants: Vec<(u32, [u8; 32])>,
session_id: [u8; 32],
}
impl SealedRoster {
pub fn new(
participants: &[(u32, [u8; 32])],
session_id: [u8; 32],
) -> Result<Self, Error> {
if participants.is_empty() {
return Err(Error::EmptyContributions);
}
let mut sorted = participants.to_vec();
sorted.sort_by_key(|(i, _)| *i);
if sorted[0].0 == 0 {
return Err(Error::InvalidIndex);
}
for w in sorted.windows(2) {
if w[0].0 == w[1].0 {
return Err(Error::DuplicateIndex(w[0].0));
}
}
Ok(Self {
participants: sorted,
session_id,
})
}
#[inline]
pub fn participants(&self) -> &[(u32, [u8; 32])] {
&self.participants
}
#[inline]
pub fn session_id(&self) -> &[u8; 32] {
&self.session_id
}
pub fn public_key(&self, index: u32) -> Result<&[u8; 32], Error> {
self.participants
.iter()
.find(|(i, _)| *i == index)
.map(|(_, k)| k)
.ok_or(Error::UnknownParticipant(index))
}
pub fn transcript(&self) -> [u8; 32] {
let mut h = Sha256::new();
h.update(TRANSCRIPT_DOMAIN);
h.update((self.participants.len() as u64).to_le_bytes());
for (index, key) in &self.participants {
h.update(index.to_le_bytes());
h.update(key);
}
h.update(self.session_id);
h.finalize().into()
}
pub fn prologue(&self, round: u8) -> Vec<u8> {
let mut out = Vec::with_capacity(PROLOGUE_DOMAIN.len() + 65);
out.extend_from_slice(PROLOGUE_DOMAIN);
out.extend_from_slice(&self.transcript());
out.extend_from_slice(&self.session_id);
out.push(round);
out
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SealedSubShare {
pub dealer_index: u32,
pub recipient_index: u32,
pub ciphertext: Vec<u8>,
}
pub use crate::dkg::commitment_digest;
pub fn sealed_ciphertext_digest(ciphertext: &[u8]) -> [u8; 32] {
let mut h = Sha256::new();
h.update(SEALED_DIGEST_DOMAIN);
h.update((ciphertext.len() as u64).to_le_bytes());
h.update(ciphertext);
h.finalize().into()
}
fn noise_seal(
local_private: &[u8; 32],
remote_public: &[u8; 32],
prologue: &[u8],
plaintext: &[u8],
) -> Option<Vec<u8>> {
let mut noise = snow::Builder::new(NOISE_PATTERN.parse().ok()?)
.prologue(prologue)
.local_private_key(local_private)
.remote_public_key(remote_public)
.build_initiator()
.ok()?;
let mut out = vec![0u8; plaintext.len() + 128];
let n = noise.write_message(plaintext, &mut out).ok()?;
out.truncate(n);
Some(out)
}
fn noise_open(
local_private: &[u8; 32],
remote_public: &[u8; 32],
prologue: &[u8],
message: &[u8],
) -> Option<Vec<u8>> {
let mut noise = snow::Builder::new(NOISE_PATTERN.parse().ok()?)
.prologue(prologue)
.local_private_key(local_private)
.remote_public_key(remote_public)
.build_responder()
.ok()?;
let mut out = vec![0u8; message.len() + 128];
let n = noise.read_message(message, &mut out).ok()?;
out.truncate(n);
Some(out)
}
pub fn seal_subshare<P: CurvePoint>(
dealer_x25519_secret: &[u8; 32],
roster: &SealedRoster,
round: u8,
subshare: &SubShare<P::Scalar>,
commitment: &DealerCommitment<P>,
) -> Result<SealedSubShare, Error> {
if subshare.dealer_index != commitment.dealer_index {
return Err(Error::InvalidIndex);
}
let recipient = roster.public_key(subshare.player_index)?;
roster.public_key(subshare.dealer_index)?;
let mut plaintext = Vec::with_capacity(72);
plaintext.extend_from_slice(&subshare.encode_plaintext());
plaintext.extend_from_slice(&commitment_digest(commitment));
let ciphertext = noise_seal(
dealer_x25519_secret,
recipient,
&roster.prologue(round),
&plaintext,
)
.ok_or(Error::SealedOpenFailed(subshare.dealer_index))?;
Ok(SealedSubShare {
dealer_index: subshare.dealer_index,
recipient_index: subshare.player_index,
ciphertext,
})
}
pub fn open_subshare<P: CurvePoint>(
recipient_x25519_secret: &[u8; 32],
recipient_index: u32,
roster: &SealedRoster,
round: u8,
sealed: &SealedSubShare,
commitment: &DealerCommitment<P>,
) -> Result<SubShare<P::Scalar>, Error> {
let dealer = roster.public_key(sealed.dealer_index)?;
if sealed.recipient_index != recipient_index {
return Err(Error::SealedOpenFailed(sealed.dealer_index));
}
let plaintext = noise_open(
recipient_x25519_secret,
dealer,
&roster.prologue(round),
&sealed.ciphertext,
)
.ok_or(Error::SealedOpenFailed(sealed.dealer_index))?;
if plaintext.len() != 72 {
return Err(Error::SealedOpenFailed(sealed.dealer_index));
}
let subshare_bytes: [u8; 40] = plaintext[..40].try_into().unwrap();
let subshare = SubShare::<P::Scalar>::decode_plaintext(&subshare_bytes)?;
if subshare.dealer_index != sealed.dealer_index
|| subshare.player_index != sealed.recipient_index
{
return Err(Error::SealedOpenFailed(sealed.dealer_index));
}
if subshare.dealer_index != commitment.dealer_index
|| plaintext[40..] != commitment_digest(commitment)
{
return Err(Error::InvalidSubShare(sealed.dealer_index));
}
if !commitment.verify_subshare(recipient_index, subshare.value()) {
return Err(Error::InvalidSubShare(sealed.dealer_index));
}
Ok(subshare)
}
pub fn open_subshare_agreed<P: CurvePoint>(
recipient_x25519_secret: &[u8; 32],
recipient_index: u32,
roster: &SealedRoster,
round: u8,
sealed: &SealedSubShare,
agreed: &crate::dkg::AgreedRound1<P>,
) -> Result<SubShare<P::Scalar>, Error> {
open_subshare_agreed_with_evidence::<P>(
recipient_x25519_secret,
recipient_index,
roster,
round,
sealed,
agreed,
)
.map_err(OpenFailure::into_error)
}
#[derive(Debug)]
pub enum OpenFailure {
Local(Error),
BadSubShare { evidence: BadSubShareEvidence },
}
impl OpenFailure {
pub fn into_error(self) -> Error {
match self {
Self::Local(e) => e,
Self::BadSubShare { evidence } => Error::InvalidSubShare(evidence.dealer_index),
}
}
}
impl From<Error> for OpenFailure {
fn from(e: Error) -> Self {
Self::Local(e)
}
}
impl core::fmt::Display for OpenFailure {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Local(e) => write!(f, "{}", e),
Self::BadSubShare { evidence } => write!(
f,
"sub-share from dealer {} fails the Feldman check against the agreed commitment",
evidence.dealer_index
),
}
}
}
impl std::error::Error for OpenFailure {}
#[allow(clippy::result_large_err)]
pub fn open_subshare_agreed_with_evidence<P: CurvePoint>(
recipient_x25519_secret: &[u8; 32],
recipient_index: u32,
roster: &SealedRoster,
round: u8,
sealed: &SealedSubShare,
agreed: &crate::dkg::AgreedRound1<P>,
) -> Result<SubShare<P::Scalar>, OpenFailure> {
let commitment = agreed.commitment(sealed.dealer_index)?;
let dealer = roster.public_key(sealed.dealer_index)?;
if sealed.recipient_index != recipient_index {
return Err(Error::SealedOpenFailed(sealed.dealer_index).into());
}
let plaintext = noise_open(
recipient_x25519_secret,
dealer,
&roster.prologue(round),
&sealed.ciphertext,
)
.ok_or(Error::SealedOpenFailed(sealed.dealer_index))?;
if plaintext.len() != 72 {
return Err(Error::SealedOpenFailed(sealed.dealer_index).into());
}
let subshare_bytes: [u8; 40] = plaintext[..40].try_into().unwrap();
let subshare = SubShare::<P::Scalar>::decode_plaintext(&subshare_bytes)?;
if subshare.dealer_index != sealed.dealer_index
|| subshare.player_index != sealed.recipient_index
{
return Err(Error::SealedOpenFailed(sealed.dealer_index).into());
}
if !commitment.verify_subshare(recipient_index, subshare.value()) {
return Err(OpenFailure::BadSubShare {
evidence: BadSubShareEvidence {
dealer_index: sealed.dealer_index,
recipient_index,
session_id: *roster.session_id(),
round,
subshare: subshare.value().to_bytes(),
agreed_digest: commitment_digest(commitment),
sealed_digest: sealed_ciphertext_digest(&sealed.ciphertext),
},
});
}
if plaintext[40..] != commitment_digest(commitment) {
return Err(Error::InvalidSubShare(sealed.dealer_index).into());
}
Ok(subshare)
}
pub fn seal_round2<P: CurvePoint>(
dealer: &crate::dkg::Dealer<P>,
dealer_x25519_secret: &[u8; 32],
roster: &SealedRoster,
round: u8,
) -> Result<Vec<SealedSubShare>, Error> {
let commitment = dealer.commitment();
let mut out = Vec::with_capacity(roster.participants().len());
for (index, _) in roster.participants() {
let subshare = dealer.generate_subshare(*index).expect("index is 1-indexed by construction");
out.push(seal_subshare::<P>(
dealer_x25519_secret,
roster,
round,
&subshare,
commitment,
)?);
}
Ok(out)
}
#[cfg(all(test, feature = "ristretto255"))]
mod tests {
use super::*;
use crate::curve::CurveScalar;
use crate::dkg::Dealer;
use curve25519_dalek::ristretto::RistrettoPoint;
use rand::rngs::OsRng;
type Point = RistrettoPoint;
const SEED_1: [u8; 32] = [7u8; 32];
const SEED_2: [u8; 32] = [9u8; 32];
const SEED_3: [u8; 32] = [11u8; 32];
const SESSION: [u8; 32] = [0x33u8; 32];
const ROUND: u8 = 2;
fn roster(session: [u8; 32]) -> SealedRoster {
SealedRoster::new(
&[
(1, x25519_public_from_seed(&SEED_1)),
(2, x25519_public_from_seed(&SEED_2)),
(3, x25519_public_from_seed(&SEED_3)),
],
session,
)
.unwrap()
}
#[test]
fn the_x25519_key_is_not_the_identity_seed() {
assert_ne!(x25519_secret_from_seed(&SEED_1), SEED_1);
}
#[test]
fn the_roster_is_order_independent_and_unambiguous() {
let a = (1u32, [1u8; 32]);
let b = (2u32, [2u8; 32]);
assert_eq!(
SealedRoster::new(&[a, b], SESSION).unwrap().transcript(),
SealedRoster::new(&[b, a], SESSION).unwrap().transcript()
);
assert_eq!(
SealedRoster::new(&[a, a], SESSION),
Err(Error::DuplicateIndex(1))
);
assert_eq!(
SealedRoster::new(&[(0u32, [0u8; 32])], SESSION),
Err(Error::InvalidIndex)
);
assert_eq!(SealedRoster::new(&[], SESSION), Err(Error::EmptyContributions));
}
#[test]
fn a_sealed_subshare_round_trips_and_hides_the_scalar() {
let mut rng = OsRng;
let r = roster(SESSION);
let dealer: Dealer<Point> = Dealer::new(1, 2, &mut rng).expect("index is 1-indexed by construction");
let subshare = dealer.generate_subshare(2).expect("index is 1-indexed by construction");
let plaintext = subshare.encode_plaintext();
let sealed = seal_subshare::<Point>(
&x25519_secret_from_seed(&SEED_1),
&r,
ROUND,
&subshare,
dealer.commitment(),
)
.unwrap();
assert!(
!sealed
.ciphertext
.windows(32)
.any(|w| w == &plaintext[8..40]),
"the sub-share scalar must not appear on the wire"
);
let opened = open_subshare::<Point>(
&x25519_secret_from_seed(&SEED_2),
2,
&r,
ROUND,
&sealed,
dealer.commitment(),
)
.unwrap();
assert_eq!(opened.value(), subshare.value());
assert_eq!(opened.dealer_index, 1);
assert_eq!(opened.player_index, 2);
}
#[test]
fn a_cheating_dealer_leaves_evidence_anyone_can_recheck() {
use crate::dkg::{
commitment_digest, Complaint, ComplaintEvidence, ComplaintTally, ComplaintVerdict,
DkgState,
};
use crate::reshare::SubShare;
use curve25519_dalek::scalar::Scalar;
let mut rng = OsRng;
let r = roster(SESSION);
let (t, n, epoch) = (2u32, 3u32, 8u64);
let dealers: Vec<Dealer<Point>> = (1..=n)
.map(|i| Dealer::new(i, t, &mut rng).unwrap())
.collect();
let mut st = DkgState::<Point>::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 junk = SubShare::<Scalar>::new(1, 2, Scalar::random(&mut rng)).unwrap();
let sealed = seal_subshare::<Point>(
&x25519_secret_from_seed(&SEED_1),
&r,
ROUND,
&junk,
dealers[0].commitment(),
)
.unwrap();
assert_eq!(
open_subshare_agreed::<Point>(
&x25519_secret_from_seed(&SEED_2),
2,
&r,
ROUND,
&sealed,
&agreed,
)
.unwrap_err(),
Error::InvalidSubShare(1)
);
let err = open_subshare_agreed_with_evidence::<Point>(
&x25519_secret_from_seed(&SEED_2),
2,
&r,
ROUND,
&sealed,
&agreed,
)
.unwrap_err();
let OpenFailure::BadSubShare { evidence } = err else {
panic!("a Feldman failure is the accusable case");
};
assert_eq!(evidence.dealer_index, 1);
assert_eq!(evidence.recipient_index, 2);
assert_eq!(evidence.session_id, SESSION);
assert_eq!(evidence.round, ROUND);
assert_eq!(evidence.subshare, junk.value().to_bytes());
assert_eq!(
evidence.agreed_digest,
commitment_digest(agreed.commitment(1).unwrap())
);
assert_eq!(
evidence.sealed_digest,
sealed_ciphertext_digest(&sealed.ciphertext)
);
assert!(
alloc::format!("{:?}", evidence).contains("[REDACTED]"),
"the scalar is redacted from Debug"
);
let accuser_secret = Scalar::random(&mut rng);
let accuser_pk: Point =
<Point as CurvePoint>::generator().mul_scalar(&accuser_secret);
let complaint = Complaint::<Point>::sign(
epoch,
SESSION,
ROUND,
2,
ComplaintEvidence::BadSubShare { evidence },
&accuser_secret,
&mut rng,
)
.unwrap();
assert_eq!(
complaint
.verify(epoch, &SESSION, &accuser_pk, Some(&agreed))
.unwrap(),
ComplaintVerdict::Upheld
);
let mut tally = ComplaintTally::new(t);
tally.record(2, 1, ComplaintVerdict::Upheld).unwrap();
assert!(!tally.reached(1));
tally.record(3, 1, ComplaintVerdict::Upheld).unwrap();
assert!(tally.reached(1), "t distinct accusers, and only then");
}
#[test]
fn the_agreed_set_decides_which_commitment_a_subshare_is_checked_against() {
use crate::dkg::DkgState;
let mut rng = OsRng;
let r = roster(SESSION);
let (t, n, epoch) = (2u32, 3u32, 4u64);
let evil_a: Dealer<Point> = Dealer::new(1, t, &mut rng).unwrap();
let evil_b: Dealer<Point> = Dealer::new(1, t, &mut rng).unwrap();
let honest: Vec<Dealer<Point>> = (2..=n)
.map(|i| Dealer::new(i, t, &mut rng).unwrap())
.collect();
let sub_a = evil_a.generate_subshare(2).unwrap();
let sealed_a = seal_subshare::<Point>(
&x25519_secret_from_seed(&SEED_1),
&r,
ROUND,
&sub_a,
evil_a.commitment(),
)
.unwrap();
open_subshare::<Point>(
&x25519_secret_from_seed(&SEED_2),
2,
&r,
ROUND,
&sealed_a,
evil_a.commitment(),
)
.expect("each pair is internally consistent — that is the finding");
let mut st = DkgState::<Point>::new(epoch, t, n);
st.submit_commitment(evil_b.round1_package(epoch, &mut rng))
.unwrap();
for d in &honest {
st.submit_commitment(d.round1_package(epoch, &mut rng))
.unwrap();
}
let agreed = st.agreed_round1().unwrap();
assert_eq!(
open_subshare_agreed::<Point>(
&x25519_secret_from_seed(&SEED_2),
2,
&r,
ROUND,
&sealed_a,
&agreed,
)
.unwrap_err(),
Error::InvalidSubShare(1),
"a sub-share against a commitment outside the agreed set is refused"
);
let stranger: Dealer<Point> = Dealer::new(3, t, &mut rng).unwrap();
let sub_s = stranger.generate_subshare(2).unwrap();
let sealed_s = seal_subshare::<Point>(
&x25519_secret_from_seed(&SEED_3),
&r,
ROUND,
&sub_s,
stranger.commitment(),
)
.unwrap();
let mut thin = DkgState::<Point>::new(epoch, t, 1);
thin.submit_commitment(evil_b.round1_package(epoch, &mut rng))
.unwrap();
assert_eq!(
open_subshare_agreed::<Point>(
&x25519_secret_from_seed(&SEED_2),
2,
&r,
ROUND,
&sealed_s,
&thin.agreed_round1().unwrap(),
)
.unwrap_err(),
Error::UnexpectedDealer(3)
);
}
#[test]
fn the_wrong_recipient_cannot_open_it() {
let mut rng = OsRng;
let r = roster(SESSION);
let dealer: Dealer<Point> = Dealer::new(1, 2, &mut rng).expect("index is 1-indexed by construction");
let sealed = seal_subshare::<Point>(
&x25519_secret_from_seed(&SEED_1),
&r,
ROUND,
&dealer.generate_subshare(2).expect("index is 1-indexed by construction"),
dealer.commitment(),
)
.unwrap();
assert_eq!(
open_subshare::<Point>(
&x25519_secret_from_seed(&SEED_3),
3,
&r,
ROUND,
&sealed,
dealer.commitment(),
)
.unwrap_err(),
Error::SealedOpenFailed(1)
);
let mut relabelled = sealed.clone();
relabelled.recipient_index = 3;
assert_eq!(
open_subshare::<Point>(
&x25519_secret_from_seed(&SEED_3),
3,
&r,
ROUND,
&relabelled,
dealer.commitment(),
)
.unwrap_err(),
Error::SealedOpenFailed(1)
);
}
#[test]
fn a_package_cannot_be_reattributed_to_another_sender() {
let mut rng = OsRng;
let r = roster(SESSION);
let dealer1: Dealer<Point> = Dealer::new(1, 2, &mut rng).expect("index is 1-indexed by construction");
let dealer3: Dealer<Point> = Dealer::new(3, 2, &mut rng).expect("index is 1-indexed by construction");
let sealed = seal_subshare::<Point>(
&x25519_secret_from_seed(&SEED_1),
&r,
ROUND,
&dealer1.generate_subshare(2).expect("index is 1-indexed by construction"),
dealer1.commitment(),
)
.unwrap();
let mut claimed = sealed.clone();
claimed.dealer_index = 3;
assert_eq!(
open_subshare::<Point>(
&x25519_secret_from_seed(&SEED_2),
2,
&r,
ROUND,
&claimed,
dealer3.commitment(),
)
.unwrap_err(),
Error::SealedOpenFailed(3)
);
let forged = seal_subshare::<Point>(
&x25519_secret_from_seed(&[42u8; 32]),
&r,
ROUND,
&dealer3.generate_subshare(2).expect("index is 1-indexed by construction"),
dealer3.commitment(),
)
.unwrap();
assert_eq!(
open_subshare::<Point>(
&x25519_secret_from_seed(&SEED_2),
2,
&r,
ROUND,
&forged,
dealer3.commitment(),
)
.unwrap_err(),
Error::SealedOpenFailed(3)
);
}
#[test]
fn a_commitment_and_subshare_from_different_dealings_do_not_pair() {
let mut rng = OsRng;
let r = roster(SESSION);
let dealer: Dealer<Point> = Dealer::new(1, 2, &mut rng).expect("index is 1-indexed by construction");
let other: Dealer<Point> = Dealer::new(1, 2, &mut rng).expect("index is 1-indexed by construction");
let sealed = seal_subshare::<Point>(
&x25519_secret_from_seed(&SEED_1),
&r,
ROUND,
&dealer.generate_subshare(2).expect("index is 1-indexed by construction"),
dealer.commitment(),
)
.unwrap();
assert_eq!(
open_subshare::<Point>(
&x25519_secret_from_seed(&SEED_2),
2,
&r,
ROUND,
&sealed,
other.commitment(),
)
.unwrap_err(),
Error::InvalidSubShare(1),
"a sub-share must not verify against a commitment it did not travel with"
);
assert_eq!(
seal_subshare::<Point>(
&x25519_secret_from_seed(&SEED_1),
&r,
ROUND,
&dealer.generate_subshare(2).expect("index is 1-indexed by construction"),
Dealer::<Point>::new(2, 2, &mut rng).unwrap().commitment(),
)
.unwrap_err(),
Error::InvalidIndex
);
}
#[test]
fn a_package_from_another_ceremony_does_not_open() {
let mut rng = OsRng;
let r = roster(SESSION);
let dealer: Dealer<Point> = Dealer::new(1, 2, &mut rng).expect("index is 1-indexed by construction");
let sealed = seal_subshare::<Point>(
&x25519_secret_from_seed(&SEED_1),
&r,
ROUND,
&dealer.generate_subshare(2).expect("index is 1-indexed by construction"),
dealer.commitment(),
)
.unwrap();
let sk2 = x25519_secret_from_seed(&SEED_2);
let other_session = roster([0x44u8; 32]);
assert_eq!(
open_subshare::<Point>(&sk2, 2, &other_session, ROUND, &sealed, dealer.commitment())
.unwrap_err(),
Error::SealedOpenFailed(1)
);
assert_eq!(
open_subshare::<Point>(&sk2, 2, &r, ROUND + 1, &sealed, dealer.commitment())
.unwrap_err(),
Error::SealedOpenFailed(1)
);
let smaller = SealedRoster::new(
&[
(1, x25519_public_from_seed(&SEED_1)),
(2, x25519_public_from_seed(&SEED_2)),
],
SESSION,
)
.unwrap();
assert_eq!(
open_subshare::<Point>(&sk2, 2, &smaller, ROUND, &sealed, dealer.commitment())
.unwrap_err(),
Error::SealedOpenFailed(1)
);
}
#[test]
fn tampering_is_detected() {
let mut rng = OsRng;
let r = roster(SESSION);
let dealer: Dealer<Point> = Dealer::new(1, 2, &mut rng).expect("index is 1-indexed by construction");
let mut sealed = seal_subshare::<Point>(
&x25519_secret_from_seed(&SEED_1),
&r,
ROUND,
&dealer.generate_subshare(2).expect("index is 1-indexed by construction"),
dealer.commitment(),
)
.unwrap();
let last = sealed.ciphertext.len() - 1;
sealed.ciphertext[last] ^= 1;
assert_eq!(
open_subshare::<Point>(
&x25519_secret_from_seed(&SEED_2),
2,
&r,
ROUND,
&sealed,
dealer.commitment(),
)
.unwrap_err(),
Error::SealedOpenFailed(1)
);
}
#[test]
fn each_package_is_fresh() {
let mut rng = OsRng;
let r = roster(SESSION);
let dealer: Dealer<Point> = Dealer::new(1, 2, &mut rng).expect("index is 1-indexed by construction");
let sub = dealer.generate_subshare(2).expect("index is 1-indexed by construction");
let sk = x25519_secret_from_seed(&SEED_1);
let one = seal_subshare::<Point>(&sk, &r, ROUND, &sub, dealer.commitment()).unwrap();
let two = seal_subshare::<Point>(&sk, &r, ROUND, &sub, dealer.commitment()).unwrap();
assert_ne!(one.ciphertext, two.ciphertext);
}
#[test]
fn a_sealed_round2_completes_the_dkg() {
let mut rng = OsRng;
let n = 3u32;
let t = 2u32;
let seeds = [SEED_1, SEED_2, SEED_3];
let r = roster(SESSION);
let dealers: Vec<Dealer<Point>> = (1..=n).map(|i| Dealer::new(i, t, &mut rng).expect("index is 1-indexed by construction")).collect();
let wire: Vec<Vec<SealedSubShare>> = dealers
.iter()
.enumerate()
.map(|(i, d)| {
seal_round2::<Point>(d, &x25519_secret_from_seed(&seeds[i]), &r, ROUND).unwrap()
})
.collect();
let mut shares = Vec::new();
for j in 1..=n {
let sk = x25519_secret_from_seed(&seeds[(j - 1) as usize]);
let mut agg: crate::dkg::Aggregator<Point> =
crate::dkg::Aggregator::all_dealers(j, n).unwrap();
for (i, packages) in wire.iter().enumerate() {
let mut opened = 0;
for pkg in packages {
if pkg.recipient_index != j {
continue;
}
let sub = open_subshare::<Point>(
&sk,
j,
&r,
ROUND,
pkg,
dealers[i].commitment(),
)
.unwrap();
agg.add_subshare(sub, dealers[i].commitment()).unwrap();
opened += 1;
}
assert_eq!(opened, 1);
}
shares.push(agg.finalize().unwrap());
}
let group_key = {
let mut k = <Point as CurvePoint>::identity();
for d in &dealers {
k = k.add(d.commitment().share_commitment());
}
k
};
let lag = crate::compute_lagrange_coefficients::<
<Point as CurvePoint>::Scalar,
>(&[1, 2, 3])
.unwrap();
let mut secret = <<Point as CurvePoint>::Scalar as CurveScalar>::zero();
for (i, s) in shares.iter().enumerate() {
secret = secret.add(&lag[i].mul(s));
}
assert_eq!(<Point as CurvePoint>::generator().mul_scalar(&secret), group_key);
}
}