// Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::bte::proof_chunking::ProofOfChunking; use crate::bte::proof_sharing::ProofOfSecretSharing; use crate::bte::{encrypt_shares, proof_chunking, proof_sharing, Ciphertexts, Params, PublicKey}; use crate::error::DkgError; use crate::interpolation::polynomial::{Polynomial, PublicCoefficients}; use crate::interpolation::{ perform_lagrangian_interpolation_at_origin, perform_lagrangian_interpolation_at_x, }; use crate::utils::deserialize_g2; use crate::{NodeIndex, Share, Threshold}; use bls12_381::{G2Projective, Scalar}; use group::GroupEncoding; use rand_core::RngCore; use std::collections::BTreeMap; use zeroize::Zeroize; #[derive(Clone, Debug)] #[cfg_attr(test, derive(PartialEq, Eq))] pub struct RecoveredVerificationKeys { pub recovered_master: G2Projective, pub recovered_partials: Vec, } impl RecoveredVerificationKeys { pub fn to_bytes(&self) -> Vec { let partials = self.recovered_partials.len(); let mut bytes = Vec::with_capacity(96 + 4 + 96 * partials); bytes.extend_from_slice(self.recovered_master.to_bytes().as_ref()); bytes.extend_from_slice(&((partials as u32).to_be_bytes())); for partial in &self.recovered_partials { bytes.extend_from_slice(partial.to_bytes().as_ref()); } bytes } pub fn try_from_bytes(b: &[u8]) -> Result { if b.len() < 96 + 4 { return Err(DkgError::new_deserialization_failure( "RecoveredVerificationKeys", "insufficient number of bytes provided", )); } let recovered_master = deserialize_g2(&b[..96]).ok_or_else(|| { DkgError::new_deserialization_failure( "RecoveredVerificationKeys.recovered_master", "invalid curve point", ) })?; let partials = u32::from_be_bytes([b[96], b[97], b[98], b[99]]) as usize; let mut recovered_partials = Vec::with_capacity(partials); if b.len() != 96 + 4 + 96 * partials { return Err(DkgError::new_deserialization_failure( "RecoveredVerificationKeys", "insufficient number of bytes provided", )); } let mut i = 96 + 4; for _ in 0..partials { let partial = deserialize_g2(&b[i..i + 96]).ok_or_else(|| { DkgError::new_deserialization_failure( "RecoveredVerificationKeys.recovered_partials", "invalid curve point", ) })?; recovered_partials.push(partial); i += 96; } Ok(RecoveredVerificationKeys { recovered_master, recovered_partials, }) } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct Dealing { pub public_coefficients: PublicCoefficients, pub ciphertexts: Ciphertexts, pub proof_of_chunking: ProofOfChunking, pub proof_of_sharing: ProofOfSecretSharing, } impl Dealing { // I'm not a big fan of this function signature, but I'm not clear on how to improve it while // allowing the dealer to skip decryption of its own share if it was also one of the receivers pub fn create( mut rng: impl RngCore, params: &Params, dealer_index: NodeIndex, threshold: Threshold, // BTreeMap ensures the keys are sorted by their indices receivers: &BTreeMap, prior_resharing_secret: Option, ) -> (Self, Option) { assert!(threshold > 0); let mut polynomial = Polynomial::new_random(&mut rng, threshold - 1); if let Some(prior_secret) = prior_resharing_secret { polynomial.set_constant_coefficient(prior_secret) } let mut shares = receivers .keys() .map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into()) .collect::>(); let remote_share_key_pairs = shares.iter().zip(receivers.values()).collect::>(); let ordered_public_keys = receivers.values().copied().collect::>(); let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, params, &mut rng); // create proofs of knowledge let chunking_instance = proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts); let proof_of_chunking = ProofOfChunking::construct(&mut rng, chunking_instance, hazmat.r(), &shares) .expect("failed to construct proof of chunking"); let combined_ciphertexts = ciphertexts.combine_ciphertexts(); let mut combined_r = hazmat.combine_rs(); let combined_rr = ciphertexts.combine_rs(); let public_coefficients = polynomial.public_coefficients(); let sharing_instance = proof_sharing::Instance::new( receivers, &public_coefficients, &combined_rr, &combined_ciphertexts, ); let proof_of_sharing = ProofOfSecretSharing::construct(&mut rng, sharing_instance, &combined_r, &shares) .expect("failed to construct proof of secret sharing"); combined_r.zeroize(); let dealing = Dealing { public_coefficients, ciphertexts, proof_of_chunking, proof_of_sharing, }; let dealers_key_index = receivers .keys() .position(|node_index| node_index == &dealer_index); if let Some(dealer_key_index) = dealers_key_index { let dealers_share = shares.remove(dealer_key_index); shares.zeroize(); (dealing, Some(dealers_share)) } else { (dealing, None) } } // rather than returning a bool for whether the dealing is valid or not, a Result is returned // instead so that we would have more information regarding a possible failure cause pub fn verify( &self, params: &Params, threshold: Threshold, receivers: &BTreeMap, prior_resharing_public: Option, ) -> Result<(), DkgError> { if threshold == 0 || threshold as usize > receivers.len() { return Err(DkgError::InvalidThreshold { actual: threshold as usize, participating: receivers.len(), }); } if self.ciphertexts.ciphertext_chunks.len() != receivers.len() { return Err(DkgError::WrongCiphertextSize { actual: self.ciphertexts.ciphertext_chunks.len(), expected: receivers.len(), }); } if self.public_coefficients.size() != threshold as usize { return Err(DkgError::WrongPublicCoefficientsSize { actual: self.public_coefficients.size(), expected: threshold as usize, }); } if !self.ciphertexts.verify_integrity(params) { return Err(DkgError::FailedCiphertextIntegrityCheck); } // TODO: perhaps change the underlying arguments in proofs of knowledge to avoid this allocation? let sorted_receivers = receivers.values().copied().collect::>(); let chunking_instance = proof_chunking::Instance::new(&sorted_receivers, &self.ciphertexts); if !self.proof_of_chunking.verify(chunking_instance) { return Err(DkgError::InvalidProofOfChunking); } let combined_randomizer = &self.ciphertexts.combine_rs(); let combined_ciphertexts = &self.ciphertexts.combine_ciphertexts(); let sharing_instance = proof_sharing::Instance::new( receivers, &self.public_coefficients, combined_randomizer, combined_ciphertexts, ); if !self.proof_of_sharing.verify(sharing_instance) { return Err(DkgError::InvalidProofOfSharing); } if let Some(prior_public) = prior_resharing_public { let dealt_public = &self.public_coefficients[0]; if dealt_public != &prior_public { return Err(DkgError::InvalidResharing); } } Ok(()) } // coeff_len || coeff || cc_len || cc || pi_c_len || pi_c || pi_s_len || pi_s pub fn to_bytes(&self) -> Vec { let mut bytes = Vec::new(); let mut coefficients_bytes = self.public_coefficients.to_bytes(); bytes.extend_from_slice(&(coefficients_bytes.len() as u32).to_be_bytes()); bytes.append(&mut coefficients_bytes); let mut ciphertexts_bytes = self.ciphertexts.to_bytes(); bytes.extend_from_slice(&(ciphertexts_bytes.len() as u32).to_be_bytes()); bytes.append(&mut ciphertexts_bytes); let mut proof_sharing_bytes = self.proof_of_sharing.to_bytes(); bytes.extend_from_slice(&(proof_sharing_bytes.len() as u32).to_be_bytes()); bytes.append(&mut proof_sharing_bytes); let mut proof_chunking_bytes = self.proof_of_chunking.to_bytes(); bytes.extend_from_slice(&(proof_chunking_bytes.len() as u32).to_be_bytes()); bytes.append(&mut proof_chunking_bytes); bytes } pub fn try_from_bytes(bytes: &[u8]) -> Result { // can we read the length of serialized public coefficients? if bytes.len() < 4 { return Err(DkgError::new_deserialization_failure( "Dealing", "insufficient number of bytes provided", )); } let mut i = 0; let coefficients_bytes_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; i += 4; let public_coefficients = PublicCoefficients::try_from_bytes(&bytes[i..i + coefficients_bytes_len])?; i += coefficients_bytes_len; let ciphertexts_bytes_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; i += 4; let ciphertexts = Ciphertexts::try_from_bytes(&bytes[i..i + ciphertexts_bytes_len])?; i += ciphertexts_bytes_len; let proof_of_sharing_bytes_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; i += 4; let proof_of_sharing = ProofOfSecretSharing::try_from_bytes(&bytes[i..i + proof_of_sharing_bytes_len])?; i += proof_of_sharing_bytes_len; let proof_of_chunking_bytes_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; i += 4; if bytes[i..].len() != proof_of_chunking_bytes_len { return Err(DkgError::new_deserialization_failure( "Dealing", "invalid number of bytes provided", )); } let proof_of_chunking = ProofOfChunking::try_from_bytes(&bytes[i..])?; Ok(Dealing { public_coefficients, ciphertexts, proof_of_chunking, proof_of_sharing, }) } } #[cfg(feature = "cw-types")] impl<'a> From<&'a Dealing> for nym_contracts_common::dealings::ContractSafeBytes { fn from(dealing: &'a Dealing) -> Self { nym_contracts_common::dealings::ContractSafeBytes(dealing.to_bytes()) } } #[cfg(feature = "cw-types")] impl<'a> TryFrom<&'a nym_contracts_common::dealings::ContractSafeBytes> for Dealing { type Error = DkgError; fn try_from( value: &'a nym_contracts_common::dealings::ContractSafeBytes, ) -> Result { Dealing::try_from_bytes(&value.0) } } /// Attempt to run the `VkCombine` algorithm to obtain the public master verification key, `VK` /// alongside shares of the verification key, `shvk_{1}`, `shvk_{2}`, ... `svhk_{n}`, where n is the number of receivers. /// /// # Arguments /// /// * `dealings`: map of dealer indices to dealings they generated /// * `threshold`: explicit threshold value of the associated dealings /// * `receivers`:map of receiver indices to their public keys // note: this function assumes all dealings have already been verified pub fn try_recover_verification_keys( dealings: &BTreeMap, threshold: Threshold, receivers: &BTreeMap, ) -> Result { if dealings.is_empty() { return Err(DkgError::NoDealingsAvailable); } let threshold = threshold as usize; if dealings.len() < threshold { return Err(DkgError::NotEnoughDealingsAvailable { available: dealings.len(), required: threshold, }); } if !dealings .values() .all(|dealing| dealing.public_coefficients.size() == threshold) { return Err(DkgError::MismatchedDealings); } let dealer_indices = dealings.keys().collect::>(); // Compute A0, ..., A_{t-1} let mut interpolated_coefficients = Vec::with_capacity(threshold); for k in 0..threshold { let mut samples = Vec::with_capacity(dealer_indices.len()); for (dealer_index, dealing) in dealings.iter() { samples.push(( Scalar::from(*dealer_index), *dealing.public_coefficients.nth(k), )) } let interpolated = perform_lagrangian_interpolation_at_origin(&samples)?; interpolated_coefficients.push(interpolated); } let master_verification_key = interpolated_coefficients[0]; let interpolated_coefficients = PublicCoefficients { coefficients: interpolated_coefficients, }; // shvk_j = A0^{j^0} * A1^{j^1} * ... * A_{t-1}^{j^{t-1}} let verification_key_shares = receivers .keys() .map(|receiver_index| interpolated_coefficients.evaluate_at(&Scalar::from(*receiver_index))) .collect(); Ok(RecoveredVerificationKeys { recovered_master: master_verification_key, recovered_partials: verification_key_shares, }) } pub fn verify_verification_keys( master_key: &G2Projective, shares: &[G2Projective], receivers: &BTreeMap, threshold: Threshold, ) -> Result<(), DkgError> { if shares.len() != receivers.len() { return Err(DkgError::NotEnoughReceiversProvided); } if threshold as usize > receivers.len() { return Err(DkgError::InvalidThreshold { actual: threshold as usize, participating: receivers.len(), }); } let indices = receivers.keys().copied().collect::>(); let indices_with_origin = std::iter::once(&0) .chain(receivers.keys()) .collect::>(); let all_shares = std::iter::once(master_key) .chain(shares.iter()) .collect::>(); for (i, share) in shares.iter().enumerate() { let samples = indices_with_origin .iter() .zip(all_shares.iter()) .map(|(&node_index, &share)| (Scalar::from(*node_index), *share)) .take(threshold as usize) .collect::>(); let interpolated = perform_lagrangian_interpolation_at_x(&Scalar::from(indices[i]), &samples)?; if share != &interpolated { return Err(DkgError::MismatchedVerificationKey); } } Ok(()) } #[cfg(test)] mod tests { use super::*; use crate::bte::{decrypt_share, keygen, setup}; use crate::combine_shares; use rand_core::SeedableRng; #[test] fn recovered_verification_keys_serde() { let keys = RecoveredVerificationKeys { recovered_master: Default::default(), recovered_partials: vec![Default::default(), Default::default()], }; let bytes = keys.to_bytes(); let recovered_keys = RecoveredVerificationKeys::try_from_bytes(&bytes).unwrap(); assert_eq!(keys, recovered_keys); } #[test] #[ignore] // expensive test fn recovering_partial_verification_keys() { // START OF SETUP let dummy_seed = [42u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); let threshold = 2; let node_indices = vec![1, 4, 7]; let mut receivers = BTreeMap::new(); let mut full_keys = Vec::new(); for index in &node_indices { let (dk, pk) = keygen(¶ms, &mut rng); receivers.insert(*index, *pk.public_key()); full_keys.push((dk, pk)) } let dealings = node_indices .iter() .map(|&dealer_index| { ( dealer_index, Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0, ) }) .collect::>(); let mut derived_secrets = Vec::new(); for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = dealings .values() .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); derived_secrets.push( combine_shares(shares, &receivers.keys().copied().collect::>()).unwrap(), ) } let master_secret = perform_lagrangian_interpolation_at_origin(&[ (Scalar::from(node_indices[2]), derived_secrets[2]), (Scalar::from(node_indices[1]), derived_secrets[1]), ]) .unwrap(); // END OF SETUP let RecoveredVerificationKeys { recovered_master, recovered_partials, } = try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); let g2 = G2Projective::generator(); assert_eq!(g2 * master_secret, recovered_master); assert_eq!(g2 * derived_secrets[0], recovered_partials[0]); assert_eq!(g2 * derived_secrets[1], recovered_partials[1]); assert_eq!(g2 * derived_secrets[2], recovered_partials[2]); } #[test] #[ignore] // expensive test fn verifying_partial_verification_keys() { let dummy_seed = [42u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); let threshold = 2; let node_indices = vec![1, 4, 7]; let mut receivers = BTreeMap::new(); let mut full_keys = Vec::new(); for index in &node_indices { let (dk, pk) = keygen(¶ms, &mut rng); receivers.insert(*index, *pk.public_key()); full_keys.push((dk, pk)) } let dealings = node_indices .iter() .map(|&dealer_index| { ( dealer_index, Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0, ) }) .collect::>(); let RecoveredVerificationKeys { recovered_master, recovered_partials, } = try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); assert!(verify_verification_keys( &recovered_master, &recovered_partials, &receivers, threshold ) .is_ok()) } #[test] #[ignore] // expensive test fn verifying_partial_verification_keys_with_different_dealers_and_receivers() { let dummy_seed = [42u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); let dealer_indices = [1, 2, 3, 8]; let receiver_indices = [3, 4, 5, 6, 7]; let threshold = 3; let mut receivers = BTreeMap::new(); let mut full_keys = Vec::new(); for index in &receiver_indices { let (dk, pk) = keygen(¶ms, &mut rng); receivers.insert(*index, *pk.public_key()); full_keys.push((dk, pk)) } let dealings = dealer_indices .iter() .map(|&dealer_index| { ( dealer_index, Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0, ) }) .collect::>(); let RecoveredVerificationKeys { recovered_master, recovered_partials, } = try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); let g2 = G2Projective::generator(); let mut derived_secrets = Vec::new(); for (i, (dk, _)) in full_keys.iter().enumerate() { let shares = dealings .values() .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = combine_shares(shares, &dealer_indices).unwrap(); // make sure it matches the associated vk assert_eq!(recovered_partials[i], g2 * recovered_secret); derived_secrets.push(recovered_secret) } assert!(verify_verification_keys( &recovered_master, &recovered_partials, &receivers, threshold ) .is_ok()) } #[test] #[ignore] // expensive test fn dealing_roundtrip() { let dummy_seed = [1u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); let parties = 5; let threshold = ((parties as f32 * 2.) / 3. + 1.) as Threshold; let node_indices = (1..=parties).collect::>(); let mut receivers = BTreeMap::new(); for index in &node_indices { let (_, pk) = keygen(¶ms, &mut rng); receivers.insert(*index, *pk.public_key()); } let (dealing, _) = Dealing::create( &mut rng, ¶ms, node_indices[0], threshold, &receivers, None, ); let bytes = dealing.to_bytes(); let recovered = Dealing::try_from_bytes(&bytes).unwrap(); assert_eq!(dealing, recovered); } }