Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DKG: clean interfaces #672

Merged
merged 9 commits into from
Oct 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 123 additions & 57 deletions fastcrypto-tbls/src/dkg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
use fastcrypto::error::{FastCryptoError, FastCryptoResult};
use fastcrypto::groups::{FiatShamirChallenge, GroupElement, MultiScalarMul};
use fastcrypto::traits::AllowedRng;
use itertools::Itertools;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};

/// Generics below use `G: GroupElement' for the group of the VSS public key, and `EG: GroupElement'
/// for the group of the ECIES public key.
Expand Down Expand Up @@ -64,6 +65,7 @@
pub complaints: Vec<Complaint<EG>>,
}

#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProcessedMessage<G: GroupElement, EG: GroupElement> {
message: Message<G, EG>,
shares: Vec<Share<G::ScalarType>>, //possibly empty
Expand All @@ -73,13 +75,49 @@
/// Mapping from node id to the shares received from that sender.
pub type SharesMap<S> = HashMap<PartyId, Vec<Share<S>>>;

#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]

Check warning on line 78 in fastcrypto-tbls/src/dkg.rs

View check run for this annotation

Codecov / codecov/patch

fastcrypto-tbls/src/dkg.rs#L78

Added line #L78 was not covered by tests
pub struct UsedProcessedMessages<G: GroupElement, EG: GroupElement>(
pub Vec<ProcessedMessage<G, EG>>,
);

impl<G: GroupElement, EG: GroupElement> From<&[ProcessedMessage<G, EG>]>
for UsedProcessedMessages<G, EG>
{
fn from(msgs: &[ProcessedMessage<G, EG>]) -> Self {
let filtered = msgs
.iter()
.unique_by(|&m| m.message.sender)
.cloned()
.collect::<Vec<_>>();
Self(filtered)
}
}

pub struct VerifiedProcessedMessages<G: GroupElement, EG: GroupElement>(
pub Vec<ProcessedMessage<G, EG>>,
);

impl<G: GroupElement, EG: GroupElement> VerifiedProcessedMessages<G, EG> {
fn filter_from(msgs: &UsedProcessedMessages<G, EG>, to_exclude: &[PartyId]) -> Self {
let filtered = msgs
.0
.iter()
.filter(|m| !to_exclude.contains(&m.message.sender))
.cloned()
.collect::<Vec<_>>();
Self(filtered)
}
}

/// [Output] is the final output of the DKG protocol in case it runs
/// successfully. It can be used later with [ThresholdBls], see examples in tests.
///
/// If shares is None, the object can only be used for verifying (partial and full) signatures.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Output<G: GroupElement, EG: GroupElement> {
pub nodes: Nodes<EG>,
pub vss_pk: Poly<G>,
pub shares: Vec<Share<G::ScalarType>>,
pub shares: Option<Vec<Share<G::ScalarType>>>, // None if some shares are missing.
benr-ml marked this conversation as resolved.
Show resolved Hide resolved
}

/// A dealer in the DKG ceremony.
Expand Down Expand Up @@ -157,21 +195,28 @@
}

fn sanity_check_message(&self, msg: &Message<G, EG>) -> FastCryptoResult<()> {
self.nodes.node_id_to_node(msg.sender)?;
self.nodes
.node_id_to_node(msg.sender)
.map_err(|_| FastCryptoError::InvalidMessage)?;
if self.t != msg.vss_pk.degree() + 1 {
return Err(FastCryptoError::InvalidInput);
return Err(FastCryptoError::InvalidMessage);

Check warning on line 202 in fastcrypto-tbls/src/dkg.rs

View check run for this annotation

Codecov / codecov/patch

fastcrypto-tbls/src/dkg.rs#L202

Added line #L202 was not covered by tests
}
if self.nodes.num_nodes() != msg.encrypted_shares.len() {
return Err(FastCryptoError::InvalidInput);
return Err(FastCryptoError::InvalidMessage);

Check warning on line 205 in fastcrypto-tbls/src/dkg.rs

View check run for this annotation

Codecov / codecov/patch

fastcrypto-tbls/src/dkg.rs#L205

Added line #L205 was not covered by tests
}
msg.encrypted_shares
.verify_knowledge(&self.random_oracle.extend(&format!("encs {}", msg.sender)))?;
.verify_knowledge(&self.random_oracle.extend(&format!("encs {}", msg.sender)))
.map_err(|_| FastCryptoError::InvalidMessage)?;
Ok(())
}

/// 5. Process a message and create the second message to be broadcasted.
/// The second message contains the list of complaints on invalid shares. In addition, it
/// returns a set of valid shares (so far).
///
/// Returns error InvalidMessage if the message is invalid and should be ignored (note that we
/// could count it as part of the f+1 messages we wait for, but it's also safe to ignore it
/// and just wait for f+1 valid messages).
pub fn process_message<R: AllowedRng>(
&self,
message: Message<G, EG>,
Expand Down Expand Up @@ -242,71 +287,64 @@
})
}

/// 6. Merge results from multiple process_message calls so only one message needs to be sent.
/// Returns InputTooShort if the threshold t is not met.
/// 6. Merge results from multiple ProcessedMessages so only one message needs to be sent.
/// Returns NotEnoughInputs if the threshold t is not met.
pub fn merge(
&self,
processed_messages: &[ProcessedMessage<G, EG>],
) -> FastCryptoResult<(SharesMap<G::ScalarType>, Confirmation<EG>)> {
// Enforce unique senders
let processed_messages = processed_messages
.iter()
.map(|m| (m.message.sender, m))
.collect::<HashMap<_, _>>();
) -> FastCryptoResult<(Confirmation<EG>, UsedProcessedMessages<G, EG>)> {
let filtered_messages = UsedProcessedMessages::from(processed_messages);
// Verify we have enough messages
let total_weight = processed_messages
.keys()
.map(|sender| {
let total_weight = filtered_messages
.0
.iter()
.map(|m| {
self.nodes
.node_id_to_node(*sender)
.node_id_to_node(m.message.sender)
.expect("checked in process_message")
.weight as u32
})
.sum::<u32>();
if total_weight < self.t {
return Err(FastCryptoError::InputTooShort(self.t as usize));
return Err(FastCryptoError::NotEnoughInputs);
}

let mut shares = HashMap::new();
let mut conf = Confirmation {
sender: self.id,
complaints: Vec::new(),
};
for m in processed_messages.values() {
shares.insert(m.message.sender, m.shares.clone());
for m in &filtered_messages.0 {
if m.complaint.is_some() {
let complaint = m.complaint.clone().unwrap();
let complaint = m.complaint.clone().expect("checked above");
conf.complaints.push(complaint);
}
}
Ok((shares, conf))
Ok((conf, filtered_messages))
}

// TODO: Handle the case of not having enough valid shares gracefully (e.g.,
// process_confirmations without my complaint).

/// 7. Process all confirmations, check all complaints, and update the local set of
/// valid shares accordingly.
///
/// minimal_threshold is the minimal number of second round messages we expect. Its value is
/// application dependent but in most cases it should be at least t+f to guarantee that at
/// least t honest nodes have valid shares.
/// Returns InputTooShort if the threshold minimal_threshold is not met.
pub fn process_confirmations<R: AllowedRng>(
///
/// Returns NotEnoughInputs if the threshold minimal_threshold is not met.
pub(crate) fn process_confirmations<R: AllowedRng>(
&self,
messages: &[Message<G, EG>],
messages: &UsedProcessedMessages<G, EG>,
confirmations: &[Confirmation<EG>],
shares: SharesMap<G::ScalarType>,
minimal_threshold: u32,
rng: &mut R,
) -> Result<SharesMap<G::ScalarType>, FastCryptoError> {
) -> FastCryptoResult<VerifiedProcessedMessages<G, EG>> {
if minimal_threshold < self.t {
return Err(FastCryptoError::InvalidInput);
}
// Ignore confirmations with invalid sender
let confirmations = confirmations
.iter()
.filter(|c| self.nodes.node_id_to_node(c.sender).is_ok())
.unique_by(|m| m.sender)
.collect::<Vec<_>>();
// Verify we have enough confirmations
let total_weight = confirmations
Expand All @@ -319,23 +357,25 @@
})
.sum::<u32>();
if total_weight < minimal_threshold {
return Err(FastCryptoError::InputTooShort(minimal_threshold as usize));
return Err(FastCryptoError::NotEnoughInputs);

Check warning on line 360 in fastcrypto-tbls/src/dkg.rs

View check run for this annotation

Codecov / codecov/patch

fastcrypto-tbls/src/dkg.rs#L360

Added line #L360 was not covered by tests
}

// Two hash maps for faster access in the main loop below.
let id_to_pk: HashMap<PartyId, &ecies::PublicKey<EG>> =
self.nodes.iter().map(|n| (n.id, &n.pk)).collect();
let id_to_m1: HashMap<PartyId, &Message<G, EG>> =
messages.iter().map(|m| (m.sender, m)).collect();
let id_to_pk = self
.nodes
.iter()
.map(|n| (n.id, &n.pk))
.collect::<HashMap<_, _>>();
let id_to_m1 = messages
.0
.iter()
.map(|m| (m.message.sender, &m.message))
.collect::<HashMap<_, _>>();

let mut shares = shares;
let mut to_exclude = HashSet::new();
'outer: for m2 in confirmations {
'inner: for complaint in &m2.complaints[..] {
'inner: for complaint in &m2.complaints {
let accused = complaint.accused_sender;
// Ignore senders that are already not relevant, or invalid complaints.
if !shares.contains_key(&accused) {
continue 'inner;
}
let accuser = m2.sender;
let accuser_pk = id_to_pk
.get(&accuser)
Expand All @@ -347,7 +387,7 @@
.expect("checked above that is not None")
.encrypted_shares
.get_encryption(accuser as usize)
.expect("checked above that there are enough encryptions");
.expect("checked earlier that there are enough encryptions");
Self::check_delegated_key_and_share(
&complaint.proof,
accuser_pk,
Expand All @@ -365,29 +405,34 @@
// Ignore accused from now on, and continue processing complaints from the
// current accuser.
true => {
shares.remove(&accused);
to_exclude.insert(accused);
continue 'inner;
}
// Ignore the accuser from now on, including its other complaints (not critical
// for security, just saves some work).
false => {
shares.remove(&accuser);
to_exclude.insert(accuser);

Check warning on line 414 in fastcrypto-tbls/src/dkg.rs

View check run for this annotation

Codecov / codecov/patch

fastcrypto-tbls/src/dkg.rs#L414

Added line #L414 was not covered by tests
continue 'outer;
}
}
}
}

Ok(shares)
let verified_messages = VerifiedProcessedMessages::filter_from(
messages,
&to_exclude.into_iter().collect::<Vec<_>>(),
);

Ok(verified_messages)
}

/// 8. Aggregate the valid shares (as returned from the previous step) and the public key.
pub fn aggregate(
&self,
first_messages: &[Message<G, EG>],
shares: SharesMap<G::ScalarType>,
) -> Output<G, EG> {
let id_to_m1: HashMap<_, _> = first_messages.iter().map(|m| (m.sender, m)).collect();
pub(crate) fn aggregate(&self, messages: &VerifiedProcessedMessages<G, EG>) -> Output<G, EG> {
let id_to_m1 = messages
.0
.iter()
.map(|m| (m.message.sender, &m.message))
.collect::<HashMap<_, _>>();
let mut vss_pk = PublicPoly::<G>::zero();
let my_share_ids = self.nodes.share_ids_of(self.id);

Expand All @@ -404,28 +449,49 @@
})
.collect::<HashMap<_, _>>();

for (from_sender, shares_from_sender) in shares {
for m in &messages.0 {
vss_pk.add(
&id_to_m1
.get(&from_sender)
.get(&m.message.sender)
.expect("shares only includes shares from valid first messages")
.vss_pk,
);
for share in shares_from_sender {
for share in &m.shares {
final_shares
.get_mut(&share.index)
.expect("created above")
.value += share.value;
}
}

// If I didn't receive a valid share for one of the verified messages (i.e., my complaint
// was not processed), then I don't have a valid share for the final key.
let shares = if messages.0.iter().all(|m| m.complaint.is_none()) {
Some(final_shares.values().cloned().collect())
} else {
None

Check warning on line 472 in fastcrypto-tbls/src/dkg.rs

View check run for this annotation

Codecov / codecov/patch

fastcrypto-tbls/src/dkg.rs#L472

Added line #L472 was not covered by tests
};

Output {
nodes: self.nodes.clone(),
vss_pk,
shares: final_shares.values().cloned().collect(),
shares,
}
}

/// Execute the previous two steps together.
benr-ml marked this conversation as resolved.
Show resolved Hide resolved
pub fn complete<R: AllowedRng>(
&self,
messages: &UsedProcessedMessages<G, EG>,
confirmations: &[Confirmation<EG>],
minimal_threshold: u32,
rng: &mut R,
) -> FastCryptoResult<Output<G, EG>> {
let verified_messages =
self.process_confirmations(messages, confirmations, minimal_threshold, rng)?;
Ok(self.aggregate(&verified_messages))
}

Check warning on line 493 in fastcrypto-tbls/src/dkg.rs

View check run for this annotation

Codecov / codecov/patch

fastcrypto-tbls/src/dkg.rs#L483-L493

Added lines #L483 - L493 were not covered by tests

fn decrypt_and_get_share(
sk: &ecies::PrivateKey<EG>,
encrypted_shares: &ecies::Encryption<EG>,
Expand Down
1 change: 0 additions & 1 deletion fastcrypto-tbls/src/dl_verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,5 @@ pub fn verify_equal_exponents<R: AllowedRng>(
}

pub fn get_random_scalars<S: Scalar, R: AllowedRng>(n: u32, rng: &mut R) -> Vec<S> {
// TODO: can use 40 bits instead of 64 ("& 0x000F_FFFF_FFFF_FFFF" below)
(0..n).map(|_| S::from(rng.next_u64())).collect::<Vec<_>>()
}
1 change: 1 addition & 0 deletions fastcrypto-tbls/src/ecies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use typenum::consts::{U16, U32};
/// APIs that use a random oracle must receive one as an argument. That RO must be unique and thus
/// the caller should initialize/derive it using a unique prefix.

// TODO: Use ZeroizeOnDrop.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PrivateKey<G: GroupElement>(G::ScalarType);

Expand Down
Loading
Loading