ec00918524
* dkg: move crate and un-nest * all: update paths to common/dkg * crypto: Cargo.toml metadata
627 lines
21 KiB
Rust
627 lines
21 KiB
Rust
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||
// SPDX-License-Identifier: Apache-2.0
|
||
|
||
use crate::bte::keys::{DecryptionKey, PublicKey};
|
||
use crate::bte::{evaluate_f, Params, CHUNK_SIZE, G2_GENERATOR_PREPARED, NUM_CHUNKS, PAIRING_BASE};
|
||
use crate::error::DkgError;
|
||
use crate::utils::{combine_g1_chunks, combine_scalar_chunks, deserialize_g1, deserialize_g2};
|
||
use crate::{Chunk, ChunkedShare, Share};
|
||
use bls12_381::{G1Affine, G1Projective, G2Prepared, G2Projective, Gt, Scalar};
|
||
use ff::Field;
|
||
use group::{Curve, Group, GroupEncoding};
|
||
use rand_core::RngCore;
|
||
use std::collections::HashMap;
|
||
use std::ops::Neg;
|
||
use zeroize::Zeroize;
|
||
|
||
#[derive(Debug)]
|
||
#[cfg_attr(test, derive(Clone, PartialEq, Eq))]
|
||
pub struct Ciphertexts {
|
||
pub rr: [G1Projective; NUM_CHUNKS],
|
||
pub ss: [G1Projective; NUM_CHUNKS],
|
||
pub zz: [G2Projective; NUM_CHUNKS],
|
||
pub ciphertext_chunks: Vec<[G1Projective; NUM_CHUNKS]>,
|
||
}
|
||
|
||
impl Ciphertexts {
|
||
pub fn verify_integrity(&self, params: &Params) -> bool {
|
||
// if this checks fails it means the ciphertext is undefined as values
|
||
// in `r`, `s` and `z` are meaningless since technically this ciphertext
|
||
// has been created for 0 parties
|
||
if self.ciphertext_chunks.is_empty() {
|
||
return false;
|
||
}
|
||
|
||
let g1_neg = G1Affine::generator().neg();
|
||
let f = evaluate_f(params);
|
||
|
||
// we have to use `f` in up to `NUM_CHUNKS` pairings (if everything is valid),
|
||
// so perform some precomputation on it
|
||
let f_prepared = G2Prepared::from(f.to_affine());
|
||
|
||
// for each triple (R_i, S_i, Z_i) check whether e(g1, Z_i) == e(R_j, f) • e(S_i, h),
|
||
// which is equivalent to checking whether e(R_j, f) • e(S_i, h) • e(g1, Z_i)^-1 == id
|
||
// and due to bilinear property whether e(R_j, f) • e(S_i, h) • e(g1^-1, Z_i) == id
|
||
for i in 0..self.rr.len() {
|
||
let miller = bls12_381::multi_miller_loop(&[
|
||
(&self.rr[i].to_affine(), &f_prepared),
|
||
(&self.ss[i].to_affine(), ¶ms._h_prepared),
|
||
(&g1_neg, &G2Prepared::from(self.zz[i].to_affine())),
|
||
]);
|
||
let res = miller.final_exponentiation();
|
||
if !bool::from(res.is_identity()) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
true
|
||
}
|
||
|
||
pub fn combine_rs(&self) -> G1Projective {
|
||
combine_g1_chunks(&self.rr)
|
||
}
|
||
|
||
// required for the purposes of the proof of secret sharing
|
||
pub fn combine_ciphertexts(&self) -> Vec<G1Projective> {
|
||
self.ciphertext_chunks
|
||
.iter()
|
||
.map(|share_ciphertext| combine_g1_chunks(share_ciphertext))
|
||
.collect()
|
||
}
|
||
|
||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||
let num_receivers = self.ciphertext_chunks.len();
|
||
|
||
let mut bytes = Vec::with_capacity(NUM_CHUNKS * ((num_receivers + 2) * 48 + 96) + 4);
|
||
for r_i in &self.rr {
|
||
bytes.extend_from_slice(r_i.to_bytes().as_ref())
|
||
}
|
||
for s_i in &self.ss {
|
||
bytes.extend_from_slice(s_i.to_bytes().as_ref())
|
||
}
|
||
for z_i in &self.zz {
|
||
bytes.extend_from_slice(z_i.to_bytes().as_ref())
|
||
}
|
||
|
||
bytes.extend_from_slice(&(num_receivers as u32).to_be_bytes());
|
||
for c_i in &self.ciphertext_chunks {
|
||
for c_ij in c_i {
|
||
bytes.extend_from_slice(c_ij.to_bytes().as_ref())
|
||
}
|
||
}
|
||
|
||
bytes
|
||
}
|
||
|
||
pub(crate) fn try_from_bytes(bytes: &[u8]) -> Result<Self, DkgError> {
|
||
// at the very minimum we must have enough bytes for a single receiver
|
||
if bytes.len() < NUM_CHUNKS * (3 * 48 + 96) + 4 {
|
||
return Err(DkgError::new_deserialization_failure(
|
||
"Ciphertexts",
|
||
"insufficient number of bytes provided",
|
||
));
|
||
}
|
||
|
||
let mut rr = Vec::with_capacity(NUM_CHUNKS);
|
||
let mut ss = Vec::with_capacity(NUM_CHUNKS);
|
||
let mut zz = Vec::with_capacity(NUM_CHUNKS);
|
||
|
||
let mut i = 0;
|
||
for _ in 0..NUM_CHUNKS {
|
||
rr.push(deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| {
|
||
DkgError::new_deserialization_failure("Ciphertexts.r", "invalid curve point")
|
||
})?);
|
||
i += 48;
|
||
}
|
||
for _ in 0..NUM_CHUNKS {
|
||
ss.push(deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| {
|
||
DkgError::new_deserialization_failure("Ciphertexts.s", "invalid curve point")
|
||
})?);
|
||
i += 48;
|
||
}
|
||
for _ in 0..NUM_CHUNKS {
|
||
zz.push(deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| {
|
||
DkgError::new_deserialization_failure("Ciphertexts.z", "invalid curve point")
|
||
})?);
|
||
i += 96;
|
||
}
|
||
|
||
let num_receivers = u32::from_be_bytes(bytes[i..i + 4].try_into().unwrap()) as usize;
|
||
i += 4;
|
||
|
||
if bytes[i..].len() != num_receivers * NUM_CHUNKS * 48 {
|
||
return Err(DkgError::new_deserialization_failure(
|
||
"Ciphertexts",
|
||
"invalid number of bytes provided",
|
||
));
|
||
}
|
||
|
||
let mut ciphertext_chunks = Vec::with_capacity(num_receivers);
|
||
|
||
for _ in 0..num_receivers {
|
||
let mut ci = Vec::with_capacity(NUM_CHUNKS);
|
||
for _ in 0..NUM_CHUNKS {
|
||
ci.push(deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| {
|
||
DkgError::new_deserialization_failure(
|
||
"Ciphertexts.ciphertext_chunks",
|
||
"invalid curve point",
|
||
)
|
||
})?);
|
||
i += 48;
|
||
}
|
||
|
||
// this unwrap is fine as we have exactly NUM_CHUNKS elements in each vector
|
||
ciphertext_chunks.push(ci.try_into().unwrap())
|
||
}
|
||
|
||
// and the same is true here, the unwraps are fine as we have exactly NUM_CHUNKS elements in each as required
|
||
Ok(Ciphertexts {
|
||
rr: rr.try_into().unwrap(),
|
||
ss: ss.try_into().unwrap(),
|
||
zz: zz.try_into().unwrap(),
|
||
ciphertext_chunks,
|
||
})
|
||
}
|
||
}
|
||
|
||
#[derive(Zeroize)]
|
||
#[zeroize(drop)]
|
||
/// Randomness generated during ciphertext generation that is required for proofs of knowledge.
|
||
/// It must be handled with extreme care as its misuse might help malicious parties to recover
|
||
/// the underlying plaintext.
|
||
pub struct HazmatRandomness {
|
||
r: [Scalar; NUM_CHUNKS],
|
||
s: [Scalar; NUM_CHUNKS],
|
||
}
|
||
|
||
impl HazmatRandomness {
|
||
pub fn r(&self) -> &[Scalar; NUM_CHUNKS] {
|
||
&self.r
|
||
}
|
||
|
||
pub fn s(&self) -> &[Scalar; NUM_CHUNKS] {
|
||
&self.s
|
||
}
|
||
|
||
pub fn combine_rs(&self) -> Scalar {
|
||
combine_scalar_chunks(&self.r)
|
||
}
|
||
}
|
||
|
||
pub fn encrypt_shares(
|
||
shares: &[(&Share, &PublicKey)],
|
||
params: &Params,
|
||
mut rng: impl RngCore,
|
||
) -> (Ciphertexts, HazmatRandomness) {
|
||
let g1 = G1Projective::generator();
|
||
|
||
let mut rand_rs = Vec::with_capacity(NUM_CHUNKS);
|
||
let mut rand_ss = Vec::with_capacity(NUM_CHUNKS);
|
||
let mut rr = Vec::with_capacity(NUM_CHUNKS);
|
||
let mut ss = Vec::with_capacity(NUM_CHUNKS);
|
||
|
||
// generate relevant re-usable pseudorandom data
|
||
for _ in 0..NUM_CHUNKS {
|
||
let rand_r = Scalar::random(&mut rng);
|
||
let rand_s = Scalar::random(&mut rng);
|
||
|
||
// g1^r
|
||
let rr_i = g1 * rand_r;
|
||
// g1^s
|
||
let ss_i = g1 * rand_s;
|
||
|
||
rand_rs.push(rand_r);
|
||
rand_ss.push(rand_s);
|
||
|
||
rr.push(rr_i);
|
||
ss.push(ss_i);
|
||
}
|
||
|
||
// produce per-chunk ciphertexts
|
||
let mut cc = Vec::with_capacity(shares.len());
|
||
|
||
for (share, pk) in shares {
|
||
let m = share.to_chunks();
|
||
|
||
let mut ci = Vec::with_capacity(NUM_CHUNKS);
|
||
|
||
for (j, chunk) in m.chunks.iter().enumerate() {
|
||
// can't really have a more efficient implementation until https://github.com/zkcrypto/bls12_381/pull/70 is merged...
|
||
let c = pk.0 * rand_rs[j] + g1 * Scalar::from(*chunk as u64);
|
||
ci.push(c)
|
||
}
|
||
|
||
// the conversion must succeed since we must have EXACTLY `NUM_CHUNKS` elements
|
||
cc.push(ci.try_into().unwrap())
|
||
}
|
||
|
||
// convert into arrays, note that the unwraps are fine as we have exactly `NUM_CHUNKS` elements in each vector
|
||
let rr = rr.try_into().unwrap();
|
||
let ss = ss.try_into().unwrap();
|
||
|
||
let f = evaluate_f(params);
|
||
|
||
let mut zz = Vec::with_capacity(NUM_CHUNKS);
|
||
for i in 0..NUM_CHUNKS {
|
||
zz.push(f * rand_rs[i] + params.h * rand_ss[i]);
|
||
}
|
||
|
||
// the conversions here must also succeed since the other vecs also have `NUM_CHUNKS` elements
|
||
(
|
||
Ciphertexts {
|
||
rr,
|
||
ss,
|
||
zz: zz.try_into().unwrap(),
|
||
ciphertext_chunks: cc,
|
||
},
|
||
HazmatRandomness {
|
||
r: rand_rs.try_into().unwrap(),
|
||
s: rand_ss.try_into().unwrap(),
|
||
},
|
||
)
|
||
}
|
||
|
||
pub fn decrypt_share(
|
||
dk: &DecryptionKey,
|
||
// in the case of multiple receivers, specifies which index of ciphertext chunks should be used
|
||
i: usize,
|
||
ciphertext: &Ciphertexts,
|
||
lookup_table: Option<&BabyStepGiantStepLookup>,
|
||
) -> Result<Share, DkgError> {
|
||
let mut plaintext = ChunkedShare::default();
|
||
|
||
if i >= ciphertext.ciphertext_chunks.len() {
|
||
return Err(DkgError::UnavailableCiphertext(i));
|
||
}
|
||
|
||
let b_neg = dk
|
||
.dh
|
||
.iter()
|
||
.fold(dk.b, |acc, d_i| acc + d_i)
|
||
.neg()
|
||
.to_affine();
|
||
|
||
let e_neg = dk.e.neg().to_affine();
|
||
|
||
for j in 0..NUM_CHUNKS {
|
||
let rr_j = &ciphertext.rr[j];
|
||
let ss_j = &ciphertext.ss[j];
|
||
let zz_j = ciphertext.zz[j].to_affine();
|
||
let cc_ij = &ciphertext.ciphertext_chunks[i][j];
|
||
|
||
let miller = bls12_381::multi_miller_loop(&[
|
||
(&cc_ij.to_affine(), &G2_GENERATOR_PREPARED),
|
||
(&rr_j.to_affine(), &G2Prepared::from(b_neg)),
|
||
(&dk.a.to_affine(), &G2Prepared::from(zz_j)),
|
||
(&ss_j.to_affine(), &G2Prepared::from(e_neg)),
|
||
]);
|
||
let m = miller.final_exponentiation();
|
||
|
||
plaintext.chunks[j] = baby_step_giant_step(&m, &PAIRING_BASE, lookup_table)?;
|
||
}
|
||
|
||
plaintext.try_into()
|
||
}
|
||
|
||
pub struct BabyStepGiantStepLookup {
|
||
base: Gt,
|
||
m: Chunk,
|
||
lookup: HashMap<[u8; 576], Chunk>,
|
||
}
|
||
|
||
impl BabyStepGiantStepLookup {
|
||
pub fn precompute(base: &Gt) -> Self {
|
||
let mut lookup = HashMap::new();
|
||
let mut g = Gt::identity();
|
||
|
||
// 1. m ← Ceiling(√n)
|
||
let m = (CHUNK_SIZE as f32).sqrt().ceil() as Chunk;
|
||
|
||
// 2. For all j where 0 ≤ j < m:
|
||
for j in 0..m {
|
||
// Compute α^j and store the pair (j, α^j) in a table.
|
||
lookup.insert(g.to_uncompressed(), j);
|
||
g += base;
|
||
}
|
||
|
||
BabyStepGiantStepLookup {
|
||
base: *base,
|
||
m,
|
||
lookup,
|
||
}
|
||
}
|
||
|
||
pub fn try_solve(&self, target: &Gt) -> Result<Chunk, DkgError> {
|
||
// 3. Compute α^{−m}
|
||
let m_neg = Scalar::from(self.m as u64).neg();
|
||
let alpha_m = self.base * m_neg;
|
||
|
||
// 4. γ ← β. (set γ = β)
|
||
let mut gamma = *target;
|
||
|
||
// 5. For all i where 0 ≤ i < m:
|
||
for i in 0..self.m {
|
||
// 1. Check to see if γ is the second component (αj) of any pair in the table.
|
||
if let Some(j) = self.lookup.get(&gamma.to_uncompressed()) {
|
||
// 2. If so, return im + j.
|
||
return Ok(i * self.m + j);
|
||
} else {
|
||
// 3. If not, γ ← γ • α^{−m}.
|
||
gamma += alpha_m;
|
||
}
|
||
}
|
||
|
||
Err(DkgError::UnsolvableDiscreteLog)
|
||
}
|
||
}
|
||
|
||
impl Default for BabyStepGiantStepLookup {
|
||
fn default() -> Self {
|
||
BabyStepGiantStepLookup::precompute(&PAIRING_BASE)
|
||
}
|
||
}
|
||
|
||
/// Attempts to solve the discrete log problem g^m, where g is in the Gt group and
|
||
/// m should be within the [0, CHUNK_MAX] range.
|
||
///
|
||
/// The implementation follows the following algorithm: https://en.wikipedia.org/wiki/Baby-step_giant-step#The_algorithm
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `target`: the result of the exponentiation, M in M = g^m,
|
||
/// * `base`: the base used for exponentiation, g in M = g^m
|
||
/// * `lookup_table`: precomputed table containing (j, α^j) pairs
|
||
pub fn baby_step_giant_step(
|
||
target: &Gt,
|
||
base: &Gt,
|
||
lookup_table: Option<&BabyStepGiantStepLookup>,
|
||
) -> Result<Chunk, DkgError> {
|
||
if let Some(lookup_table) = lookup_table {
|
||
// compute expected m to make sure the provided lookup is valid
|
||
let m = (CHUNK_SIZE as f32).sqrt().ceil() as Chunk;
|
||
|
||
if &lookup_table.base != base || lookup_table.lookup.len() != m as usize {
|
||
return Err(DkgError::MismatchedLookupTable);
|
||
}
|
||
|
||
lookup_table.try_solve(target)
|
||
} else {
|
||
BabyStepGiantStepLookup::precompute(base).try_solve(target)
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::bte::{keygen, setup, DEFAULT_BSGS_TABLE};
|
||
use rand_core::SeedableRng;
|
||
|
||
fn verify_hazmat_rand(ciphertext: &Ciphertexts, randomness: &HazmatRandomness) {
|
||
let g1 = G1Projective::generator();
|
||
|
||
for i in 0..ciphertext.rr.len() {
|
||
assert_eq!(ciphertext.rr[i], g1 * randomness.r[i]);
|
||
assert_eq!(ciphertext.ss[i], g1 * randomness.s[i]);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn baby_giant_100_without_table() {
|
||
let dummy_seed = [1u8; 32];
|
||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||
|
||
for i in 0u64..100 {
|
||
let base = Gt::random(&mut rng);
|
||
let x = (rng.next_u64() + i) % CHUNK_SIZE as u64;
|
||
let target = base * Scalar::from(x);
|
||
|
||
assert_eq!(
|
||
baby_step_giant_step(&target, &base, None).unwrap(),
|
||
x as Chunk
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn baby_giant_100_with_table() {
|
||
let dummy_seed = [1u8; 32];
|
||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||
let base = Gt::random(&mut rng);
|
||
let lookup_table = BabyStepGiantStepLookup::precompute(&base);
|
||
let table = Some(&lookup_table);
|
||
|
||
for i in 0u64..100 {
|
||
let x = (rng.next_u64() + i) % CHUNK_SIZE as u64;
|
||
let target = base * Scalar::from(x);
|
||
|
||
assert_eq!(
|
||
baby_step_giant_step(&target, &base, table).unwrap(),
|
||
x as Chunk
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
#[ignore] // expensive test
|
||
fn share_decryption_20() {
|
||
let dummy_seed = [1u8; 32];
|
||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||
let params = setup();
|
||
|
||
let (decryption_key1, public_key1) = keygen(¶ms, &mut rng);
|
||
let (decryption_key2, public_key2) = keygen(¶ms, &mut rng);
|
||
|
||
let lookup_table = &DEFAULT_BSGS_TABLE;
|
||
|
||
for _ in 0..10 {
|
||
let m1 = Share::random(&mut rng);
|
||
let m2 = Share::random(&mut rng);
|
||
let shares = &[(&m1, &public_key1.key), (&m2, &public_key2.key)];
|
||
|
||
let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng);
|
||
verify_hazmat_rand(&ciphertext, &hazmat);
|
||
|
||
let recovered1 =
|
||
decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap();
|
||
let recovered2 =
|
||
decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap();
|
||
assert_eq!(m1, recovered1);
|
||
assert_eq!(m2, recovered2);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
#[ignore] // expensive test
|
||
fn share_encryption_under_nonzero_epoch() {
|
||
let dummy_seed = [1u8; 32];
|
||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||
let params = setup();
|
||
|
||
let (decryption_key1, public_key1) = keygen(¶ms, &mut rng);
|
||
let (decryption_key2, public_key2) = keygen(¶ms, &mut rng);
|
||
|
||
let lookup_table = &DEFAULT_BSGS_TABLE;
|
||
|
||
for _ in 0..10 {
|
||
let m1 = Share::random(&mut rng);
|
||
let m2 = Share::random(&mut rng);
|
||
let shares = &[(&m1, &public_key1.key), (&m2, &public_key2.key)];
|
||
|
||
let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng);
|
||
verify_hazmat_rand(&ciphertext, &hazmat);
|
||
|
||
let recovered1 =
|
||
decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap();
|
||
let recovered2 =
|
||
decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap();
|
||
assert_eq!(m1, recovered1);
|
||
assert_eq!(m2, recovered2);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
#[ignore] // expensive test
|
||
fn ciphertext_integrity_check_passes_for_valid_data() {
|
||
let params = setup();
|
||
|
||
let dummy_seed = [1u8; 32];
|
||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||
|
||
let (_, public_key) = keygen(¶ms, &mut rng);
|
||
|
||
let share = Share::random(&mut rng);
|
||
let (ciphertext, _) = encrypt_shares(&[(&share, &public_key.key)], ¶ms, &mut rng);
|
||
assert!(ciphertext.verify_integrity(¶ms))
|
||
}
|
||
|
||
#[test]
|
||
#[ignore] // expensive test
|
||
fn ciphertext_integrity_check_passes_fails_for_malformed_data() {
|
||
let params = setup();
|
||
|
||
let dummy_seed = [1u8; 32];
|
||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||
|
||
let (_, public_key) = keygen(¶ms, &mut rng);
|
||
|
||
let share = Share::random(&mut rng);
|
||
let (ciphertext, _) = encrypt_shares(&[(&share, &public_key.key)], ¶ms, &mut rng);
|
||
|
||
let mut bad_cipher1 = ciphertext.clone();
|
||
bad_cipher1.rr[4] = G1Projective::generator();
|
||
assert!(!bad_cipher1.verify_integrity(¶ms));
|
||
|
||
let mut bad_cipher2 = ciphertext.clone();
|
||
bad_cipher2.ss[4] = G1Projective::generator();
|
||
assert!(!bad_cipher2.verify_integrity(¶ms));
|
||
|
||
let mut bad_cipher3 = ciphertext;
|
||
bad_cipher3.zz[4] = G2Projective::generator();
|
||
assert!(!bad_cipher3.verify_integrity(¶ms));
|
||
}
|
||
|
||
#[test]
|
||
fn ciphertext_combining() {
|
||
let params = setup();
|
||
|
||
let dummy_seed = [1u8; 32];
|
||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||
|
||
let nodes = 3;
|
||
|
||
let mut shares = Vec::new();
|
||
let mut public_keys = Vec::new();
|
||
for _ in 0..nodes {
|
||
shares.push(Share::random(&mut rng));
|
||
let (_, pk) = keygen(¶ms, &mut rng);
|
||
public_keys.push(*pk.public_key());
|
||
}
|
||
|
||
let refs = shares.iter().zip(public_keys.iter()).collect::<Vec<_>>();
|
||
let (ciphertext, hazmat) = encrypt_shares(&refs, ¶ms, &mut rng);
|
||
|
||
let combined_r = combine_scalar_chunks(hazmat.r());
|
||
let combined_rr = ciphertext.combine_rs();
|
||
let combined_ciphertexts = ciphertext.combine_ciphertexts();
|
||
|
||
let g1 = G1Projective::generator();
|
||
for i in 0..nodes {
|
||
let expected = public_keys[i].0 * combined_r + g1 * shares[i].0;
|
||
assert_eq!(expected, combined_ciphertexts[i]);
|
||
assert_eq!(combined_rr, g1 * combined_r);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn ciphertexts_roundtrip() {
|
||
fn random_ciphertexts(mut rng: impl RngCore, num_receivers: usize) -> Ciphertexts {
|
||
Ciphertexts {
|
||
rr: (0..NUM_CHUNKS)
|
||
.map(|_| G1Projective::random(&mut rng))
|
||
.collect::<Vec<_>>()
|
||
.try_into()
|
||
.unwrap(),
|
||
ss: (0..NUM_CHUNKS)
|
||
.map(|_| G1Projective::random(&mut rng))
|
||
.collect::<Vec<_>>()
|
||
.try_into()
|
||
.unwrap(),
|
||
zz: (0..NUM_CHUNKS)
|
||
.map(|_| G2Projective::random(&mut rng))
|
||
.collect::<Vec<_>>()
|
||
.try_into()
|
||
.unwrap(),
|
||
ciphertext_chunks: (0..num_receivers)
|
||
.map(|_| {
|
||
(0..NUM_CHUNKS)
|
||
.map(|_| G1Projective::random(&mut rng))
|
||
.collect::<Vec<_>>()
|
||
.try_into()
|
||
.unwrap()
|
||
})
|
||
.collect(),
|
||
}
|
||
}
|
||
|
||
let dummy_seed = [1u8; 32];
|
||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||
|
||
let good_ciphertexts = vec![
|
||
random_ciphertexts(&mut rng, 1),
|
||
random_ciphertexts(&mut rng, 2),
|
||
random_ciphertexts(&mut rng, 10),
|
||
];
|
||
|
||
for ciphertexts in &good_ciphertexts {
|
||
let bytes = ciphertexts.to_bytes();
|
||
let recovered = Ciphertexts::try_from_bytes(&bytes).unwrap();
|
||
assert_eq!(ciphertexts, &recovered);
|
||
}
|
||
|
||
// ciphertext for 0 receivers is invalid by default
|
||
let ciphertexts = random_ciphertexts(&mut rng, 0);
|
||
let bytes = ciphertexts.to_bytes();
|
||
assert!(Ciphertexts::try_from_bytes(&bytes).is_err());
|
||
}
|
||
}
|