mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-13 06:29:25 +00:00
Improve eVRF DKG
Updates how we calculcate verification shares, improves performance when extracting multiple sets of keys, and adds more to the test for it.
This commit is contained in:
18
Cargo.toml
18
Cargo.toml
@@ -117,18 +117,32 @@ members = [
|
||||
# to the extensive operations required for Bulletproofs
|
||||
[profile.dev.package]
|
||||
subtle = { opt-level = 3 }
|
||||
curve25519-dalek = { opt-level = 3 }
|
||||
|
||||
ff = { opt-level = 3 }
|
||||
group = { opt-level = 3 }
|
||||
|
||||
crypto-bigint = { opt-level = 3 }
|
||||
secp256k1 = { opt-level = 3 }
|
||||
curve25519-dalek = { opt-level = 3 }
|
||||
dalek-ff-group = { opt-level = 3 }
|
||||
minimal-ed448 = { opt-level = 3 }
|
||||
|
||||
multiexp = { opt-level = 3 }
|
||||
|
||||
monero-serai = { opt-level = 3 }
|
||||
secq256k1 = { opt-level = 3 }
|
||||
embedwards25519 = { opt-level = 3 }
|
||||
generalized-bulletproofs = { opt-level = 3 }
|
||||
generalized-bulletproofs-circuit-abstraction = { opt-level = 3 }
|
||||
ec-divisors = { opt-level = 3 }
|
||||
generalized-bulletproofs-ec-gadgets = { opt-level = 3 }
|
||||
|
||||
dkg = { opt-level = 3 }
|
||||
|
||||
monero-generators = { opt-level = 3 }
|
||||
monero-borromean = { opt-level = 3 }
|
||||
monero-bulletproofs = { opt-level = 3 }
|
||||
monero-mlsag = { opt-level = 3 }
|
||||
monero-clsag = { opt-level = 3 }
|
||||
|
||||
[profile.release]
|
||||
panic = "unwind"
|
||||
|
||||
@@ -192,12 +192,15 @@ fn share_verification_statements<C: Ciphersuite>(
|
||||
}
|
||||
|
||||
/// Struct to perform/verify the DKG with.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug)]
|
||||
pub struct EvrfDkg<C: EvrfCurve> {
|
||||
t: u16,
|
||||
n: u16,
|
||||
evrf_public_keys: Vec<<C::EmbeddedCurve as Ciphersuite>::G>,
|
||||
participations: HashMap<Participant, (HashMap<Participant, C::F>, EvrfVerifyResult<C>)>,
|
||||
group_key: C::G,
|
||||
verification_shares: HashMap<Participant, C::G>,
|
||||
encrypted_secret_shares:
|
||||
HashMap<Participant, HashMap<Participant, ([<C::EmbeddedCurve as Ciphersuite>::G; 2], C::F)>>,
|
||||
}
|
||||
|
||||
impl<C: EvrfCurve> EvrfDkg<C>
|
||||
@@ -255,6 +258,9 @@ where
|
||||
/// `Participation`s are valid and there's at least `t`, an instance of this struct (usable to
|
||||
/// obtain a threshold share of generated key) is returned. If all are valid and there's not at
|
||||
/// least `t`, an error of an empty list is returned after validation.
|
||||
///
|
||||
/// This DKG is unbiased if all `n` people participate. This DKG is biased if only a threshold
|
||||
/// participate.
|
||||
pub fn verify(
|
||||
rng: &mut (impl RngCore + CryptoRng),
|
||||
generators: &Generators<C>,
|
||||
@@ -277,7 +283,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
let mut res = HashMap::new();
|
||||
let mut valid = HashMap::new();
|
||||
let mut faulty = HashSet::new();
|
||||
|
||||
let mut evrf_verifier = generators.batch_verifier();
|
||||
@@ -299,9 +305,9 @@ where
|
||||
};
|
||||
evrf_verifier = verifier_clone;
|
||||
|
||||
res.insert(*i, (participation.encrypted_secret_shares.clone(), data));
|
||||
valid.insert(*i, (participation.encrypted_secret_shares.clone(), data));
|
||||
}
|
||||
debug_assert_eq!(res.len() + faulty.len(), participations.len());
|
||||
debug_assert_eq!(valid.len() + faulty.len(), participations.len());
|
||||
|
||||
// Perform the batch verification of the eVRFs
|
||||
if !generators.verify(evrf_verifier) {
|
||||
@@ -323,20 +329,23 @@ where
|
||||
)
|
||||
.expect("evrf failed basic checks yet prover wasn't prior marked faulty");
|
||||
if !generators.verify(evrf_verifier) {
|
||||
res.remove(i);
|
||||
valid.remove(i);
|
||||
faulty.insert(*i);
|
||||
}
|
||||
}
|
||||
}
|
||||
debug_assert_eq!(res.len() + faulty.len(), participations.len());
|
||||
debug_assert_eq!(valid.len() + faulty.len(), participations.len());
|
||||
|
||||
// Perform the batch verification of the shares
|
||||
let mut sum_encrypted_secret_shares = HashMap::new();
|
||||
let mut sum_masks = HashMap::new();
|
||||
let mut all_encrypted_secret_shares = HashMap::new();
|
||||
{
|
||||
let mut share_verification_statements_actual = HashMap::with_capacity(res.len());
|
||||
let mut share_verification_statements_actual = HashMap::with_capacity(valid.len());
|
||||
if !{
|
||||
let mut g_scalar = C::F::ZERO;
|
||||
let mut pairs = Vec::with_capacity(res.len() * (usize::from(t) + evrf_public_keys.len()));
|
||||
for (i, (encrypted_secret_shares, data)) in &res {
|
||||
let mut pairs = Vec::with_capacity(valid.len() * (usize::from(t) + evrf_public_keys.len()));
|
||||
for (i, (encrypted_secret_shares, data)) in &valid {
|
||||
let (this_g_scalar, mut these_pairs) = share_verification_statements::<C>(
|
||||
&mut *rng,
|
||||
&data.coefficients,
|
||||
@@ -347,11 +356,36 @@ where
|
||||
&data.encryption_commitments,
|
||||
encrypted_secret_shares,
|
||||
);
|
||||
// Queue this into our batch
|
||||
g_scalar += this_g_scalar;
|
||||
pairs.extend(&these_pairs);
|
||||
|
||||
// Also push this g_scalar onto these_pairs so these_pairs can be verified individually
|
||||
// upon error
|
||||
these_pairs.push((this_g_scalar, generators.g()));
|
||||
share_verification_statements_actual.insert(*i, these_pairs);
|
||||
|
||||
// Also format this data as we'd need it upon success
|
||||
let mut formatted_encrypted_secret_shares = HashMap::new();
|
||||
for (j, enc_share) in encrypted_secret_shares {
|
||||
/*
|
||||
We calculcate verification shares as the sum of the encrypted scalars, minus their
|
||||
masks. This only does one scalar multiplication, and `1+t` point additions (with
|
||||
one negation), and is accordingly much cheaper than interpolating the commitments.
|
||||
This is only possible because already interpolated the commitments to verify the
|
||||
encrypted secret share.
|
||||
*/
|
||||
let sum_encrypted_secret_share =
|
||||
sum_encrypted_secret_shares.get(j).copied().unwrap_or(C::F::ZERO);
|
||||
let sum_mask = sum_masks.get(j).copied().unwrap_or(C::G::identity());
|
||||
sum_encrypted_secret_shares.insert(*j, sum_encrypted_secret_share + enc_share);
|
||||
|
||||
let j_index = usize::from(u16::from(*j)) - 1;
|
||||
sum_masks.insert(*j, sum_mask + data.encryption_commitments[j_index]);
|
||||
|
||||
formatted_encrypted_secret_shares.insert(*j, (data.ecdh_keys[j_index], *enc_share));
|
||||
}
|
||||
all_encrypted_secret_shares.insert(*i, formatted_encrypted_secret_shares);
|
||||
}
|
||||
pairs.push((g_scalar, generators.g()));
|
||||
bool::from(multiexp_vartime(&pairs).is_identity())
|
||||
@@ -359,13 +393,13 @@ where
|
||||
// If the batch failed, verify them each individually
|
||||
for (i, pairs) in share_verification_statements_actual {
|
||||
if !bool::from(multiexp_vartime(&pairs).is_identity()) {
|
||||
res.remove(&i);
|
||||
valid.remove(&i);
|
||||
faulty.insert(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
debug_assert_eq!(res.len() + faulty.len(), participations.len());
|
||||
debug_assert_eq!(valid.len() + faulty.len(), participations.len());
|
||||
|
||||
let mut faulty = faulty.into_iter().collect::<Vec<_>>();
|
||||
if !faulty.is_empty() {
|
||||
@@ -373,13 +407,33 @@ where
|
||||
Err(faulty)?;
|
||||
}
|
||||
|
||||
if res.len() < usize::from(t) {
|
||||
if valid.len() < usize::from(t) {
|
||||
Err(vec![])?;
|
||||
}
|
||||
|
||||
Ok(EvrfDkg { t, n, evrf_public_keys: evrf_public_keys.to_vec(), participations: res })
|
||||
// If we now have >= t participations, calculate the group key and verification shares
|
||||
|
||||
// The group key is the sum of the zero coefficients
|
||||
let group_key = valid.values().map(|(_, evrf_data)| evrf_data.coefficients[0]).sum::<C::G>();
|
||||
|
||||
// Calculate each user's verification share
|
||||
let mut verification_shares = HashMap::new();
|
||||
for i in (1 ..= n).map(Participant) {
|
||||
verification_shares
|
||||
.insert(i, (C::generator() * sum_encrypted_secret_shares[&i]) - sum_masks[&i]);
|
||||
}
|
||||
|
||||
Ok(EvrfDkg {
|
||||
t,
|
||||
n,
|
||||
evrf_public_keys: evrf_public_keys.to_vec(),
|
||||
group_key,
|
||||
verification_shares,
|
||||
encrypted_secret_shares: all_encrypted_secret_shares,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Return all keys for this participant, not just the first
|
||||
pub fn keys(
|
||||
&self,
|
||||
evrf_private_key: &Zeroizing<<C::EmbeddedCurve as Ciphersuite>::F>,
|
||||
@@ -392,9 +446,11 @@ where
|
||||
let i = Participant(1 + i);
|
||||
|
||||
let mut secret_share = Zeroizing::new(C::F::ZERO);
|
||||
for (shares, evrf_data) in self.participations.values() {
|
||||
for shares in self.encrypted_secret_shares.values() {
|
||||
let (ecdh_keys, enc_share) = shares[&i];
|
||||
|
||||
let mut ecdh = Zeroizing::new(C::F::ZERO);
|
||||
for point in evrf_data.ecdh_keys[usize::from(u16::from(i)) - 1] {
|
||||
for point in ecdh_keys {
|
||||
// TODO: Explicitly ban 0-ECDH commitments, 0-eVRF public keys, and gen non-zero keys
|
||||
let (mut x, mut y) =
|
||||
<C::EmbeddedCurve as Ciphersuite>::G::to_xy(point * evrf_private_key.deref()).unwrap();
|
||||
@@ -402,45 +458,16 @@ where
|
||||
x.zeroize();
|
||||
y.zeroize();
|
||||
}
|
||||
*secret_share += shares[&i] - ecdh.deref();
|
||||
*secret_share += enc_share - ecdh.deref();
|
||||
}
|
||||
|
||||
// Stripe commitments per t and sum them in advance. Calculating verification shares relies on
|
||||
// these sums so preprocessing them is a massive speedup
|
||||
let mut stripes = Vec::with_capacity(usize::from(self.t));
|
||||
for t in 0 .. usize::from(self.t) {
|
||||
stripes.push(
|
||||
self.participations.values().map(|(_, evrf_data)| evrf_data.coefficients[t]).sum::<C::G>(),
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate each user's verification share
|
||||
let mut verification_shares = HashMap::new();
|
||||
for j in (1 ..= self.n).map(Participant) {
|
||||
verification_shares.insert(
|
||||
j,
|
||||
if j == i {
|
||||
C::generator() * secret_share.deref()
|
||||
} else {
|
||||
fn exponential<C: Ciphersuite>(i: Participant, values: &[C::G]) -> Vec<(C::F, C::G)> {
|
||||
let i = C::F::from(u16::from(i).into());
|
||||
let mut res = Vec::with_capacity(values.len());
|
||||
(0 .. values.len()).fold(C::F::ONE, |exp, l| {
|
||||
res.push((exp, values[l]));
|
||||
exp * i
|
||||
});
|
||||
res
|
||||
}
|
||||
multiexp_vartime(&exponential::<C>(j, &stripes))
|
||||
},
|
||||
);
|
||||
}
|
||||
debug_assert_eq!(self.verification_shares[&i], C::generator() * secret_share.deref());
|
||||
|
||||
Some(ThresholdCore {
|
||||
params: ThresholdParams::new(self.t, self.n, i).unwrap(),
|
||||
secret_share,
|
||||
group_key: stripes[0],
|
||||
verification_shares,
|
||||
group_key: self.group_key,
|
||||
verification_shares: self.verification_shares.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ pub(crate) struct EvrfProveResult<C: Ciphersuite> {
|
||||
}
|
||||
|
||||
/// The result of verifying an eVRF.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct EvrfVerifyResult<C: EvrfCurve> {
|
||||
/// The commitments to the coefficients for use in the DKG.
|
||||
pub(crate) coefficients: Vec<C::G>,
|
||||
|
||||
@@ -9,7 +9,7 @@ use ciphersuite::{group::ff::Field, Ciphersuite};
|
||||
use crate::{
|
||||
Participant, ThresholdKeys,
|
||||
evrf::*,
|
||||
tests::{THRESHOLD, PARTICIPANTS},
|
||||
tests::{THRESHOLD, PARTICIPANTS, recover_key},
|
||||
};
|
||||
|
||||
mod proof;
|
||||
@@ -18,6 +18,7 @@ use proof::{Pallas, Vesta};
|
||||
#[test]
|
||||
fn evrf_dkg() {
|
||||
let generators = EvrfDkg::<Pallas>::generators(THRESHOLD, PARTICIPANTS);
|
||||
let context = [0; 32];
|
||||
|
||||
let mut priv_keys = vec![];
|
||||
let mut pub_keys = vec![];
|
||||
@@ -36,7 +37,7 @@ fn evrf_dkg() {
|
||||
EvrfDkg::<Pallas>::participate(
|
||||
&mut OsRng,
|
||||
&generators,
|
||||
[0; 32],
|
||||
context,
|
||||
THRESHOLD,
|
||||
&pub_keys,
|
||||
priv_key,
|
||||
@@ -48,17 +49,29 @@ fn evrf_dkg() {
|
||||
let dkg = EvrfDkg::<Pallas>::verify(
|
||||
&mut OsRng,
|
||||
&generators,
|
||||
[0; 32],
|
||||
context,
|
||||
THRESHOLD,
|
||||
&pub_keys,
|
||||
&participations,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut group_key = None;
|
||||
let mut verification_shares = None;
|
||||
let mut all_keys = HashMap::new();
|
||||
for (i, priv_key) in priv_keys {
|
||||
let keys = ThresholdKeys::from(dkg.keys(&priv_key).unwrap());
|
||||
assert_eq!(keys.params().i(), i);
|
||||
assert_eq!(keys.params().t(), THRESHOLD);
|
||||
assert_eq!(keys.params().n(), PARTICIPANTS);
|
||||
group_key = group_key.or(Some(keys.group_key()));
|
||||
verification_shares = verification_shares.or(Some(keys.verification_shares()));
|
||||
assert_eq!(Some(keys.group_key()), group_key);
|
||||
assert_eq!(Some(keys.verification_shares()), verification_shares);
|
||||
|
||||
all_keys.insert(i, keys);
|
||||
}
|
||||
|
||||
// TODO: Test fo all possible combinations of keys
|
||||
assert_eq!(Pallas::generator() * recover_key(&all_keys), group_key.unwrap());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user