Finish routing the new key gen in the processor

Doesn't touch the tests, coordinator, nor Substrate yet.
`cargo +nightly fmt && cargo +nightly-2024-07-01 clippy --all-features -p serai-processor`
does pass.
This commit is contained in:
Luke Parker
2024-08-01 03:49:28 -04:00
parent 12f74e1813
commit 2f564c230e
6 changed files with 174 additions and 136 deletions

View File

@@ -238,11 +238,7 @@ pub struct EvrfDkg<C: EvrfCurve> {
HashMap<Participant, HashMap<Participant, ([<C::EmbeddedCurve as Ciphersuite>::G; 2], C::F)>>,
}
impl<C: EvrfCurve> EvrfDkg<C>
where
<<C as EvrfCurve>::EmbeddedCurve as Ciphersuite>::G:
DivisorCurve<FieldElement = <C as Ciphersuite>::F>,
{
impl<C: EvrfCurve> EvrfDkg<C> {
// Form the initial transcript for the proofs.
fn initial_transcript(
invocation: [u8; 32],
@@ -497,10 +493,15 @@ where
for i in valid.keys() {
let evrf_public_key = evrf_public_keys[usize::from(u16::from(*i)) - 1];
// We remove all keys considered participating from the Vec in order to ensure they aren't
// counted multiple times. That could happen if a participant shares a key with another
// participant. While that's presumably some degree of invalid, we're robust against it
// regardless.
// Remove this key from the Vec to prevent double-counting
/*
Double-counting would be a risk if multiple participants shared an eVRF public key and
participated. This code does still allow such participants (in order to let participants
be weighted), and any one of them participating will count as all participating. This is
fine as any one such participant will be able to decrypt the shares for themselves and
all other participants, so this is still a key generated by an amount of participants who
could simply reconstruct the key.
*/
let start_len = evrf_public_keys.len();
evrf_public_keys.retain(|key| *key != evrf_public_key);
let end_len = evrf_public_keys.len();

View File

@@ -29,7 +29,7 @@ use generalized_bulletproofs_ec_gadgets::*;
/// A pair of curves to perform the eVRF with.
pub trait EvrfCurve: Ciphersuite {
type EmbeddedCurve: Ciphersuite;
type EmbeddedCurve: Ciphersuite<G: DivisorCurve<FieldElement = <Self as Ciphersuite>::F>>;
type EmbeddedCurveParameters: DiscreteLogParameters;
}
@@ -67,11 +67,7 @@ fn sample_point<C: Ciphersuite>(rng: &mut (impl RngCore + CryptoRng)) -> C::G {
#[derive(Clone, Debug)]
pub struct EvrfGenerators<C: EvrfCurve>(pub(crate) Generators<C>);
impl<C: EvrfCurve> EvrfGenerators<C>
where
<<C as EvrfCurve>::EmbeddedCurve as Ciphersuite>::G:
DivisorCurve<FieldElement = <C as Ciphersuite>::F>,
{
impl<C: EvrfCurve> EvrfGenerators<C> {
/// Create a new set of generators.
pub fn new(max_threshold: u16, max_participants: u16) -> EvrfGenerators<C> {
let g = C::generator();
@@ -117,11 +113,7 @@ impl<C: EvrfCurve> fmt::Debug for EvrfVerifyResult<C> {
/// A struct to prove/verify eVRFs with.
pub(crate) struct Evrf<C: EvrfCurve>(PhantomData<C>);
impl<C: EvrfCurve> Evrf<C>
where
<<C as EvrfCurve>::EmbeddedCurve as Ciphersuite>::G:
DivisorCurve<FieldElement = <C as Ciphersuite>::F>,
{
impl<C: EvrfCurve> Evrf<C> {
// Sample uniform points (via rejection-sampling) on the embedded elliptic curve
fn transcript_to_points(
seed: [u8; 32],

View File

@@ -15,10 +15,7 @@ pub use poly::*;
mod tests;
/// A curve usable with this library.
pub trait DivisorCurve: Group
where
Self::Scalar: PrimeField,
{
pub trait DivisorCurve: Group {
/// An element of the field this curve is defined over.
type FieldElement: PrimeField;

View File

@@ -11,7 +11,7 @@ use ciphersuite::{
group::{Group, GroupEncoding},
Ciphersuite, Ristretto,
};
use frost::dkg::{Participant, ThresholdCore, ThresholdKeys, evrf::*};
use dkg::{Participant, ThresholdCore, ThresholdKeys, evrf::*};
use log::info;
@@ -20,6 +20,48 @@ use messages::key_gen::*;
use crate::{Get, DbTxn, Db, create_db, networks::Network};
mod generators {
use core::any::{TypeId, Any};
use std::{
sync::{LazyLock, Mutex},
collections::HashMap,
};
use frost::dkg::evrf::*;
use serai_client::validator_sets::primitives::MAX_KEY_SHARES_PER_SET;
/// A cache of the generators used by the eVRF DKG.
///
/// This performs a lookup of the Ciphersuite to its generators. Since the Ciphersuite is a
/// generic, this takes advantage of `Any`. This static is isolated in a module to ensure
/// correctness can be evaluated solely by reviewing these few lines of code.
///
/// This is arguably over-engineered as of right now, as we only need generators for Ristretto
/// and N::Curve. By having this HashMap, we enable de-duplication of the Ristretto == N::Curve
/// case, and we automatically support the n-curve case (rather than hard-coding to the 2-curve
/// case).
static GENERATORS: LazyLock<Mutex<HashMap<TypeId, &'static (dyn Send + Sync + Any)>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub(crate) fn generators<C: EvrfCurve>() -> &'static EvrfGenerators<C> {
GENERATORS
.lock()
.unwrap()
.entry(TypeId::of::<C>())
.or_insert_with(|| {
// If we haven't prior needed generators for this Ciphersuite, generate new ones
Box::leak(Box::new(EvrfGenerators::<C>::new(
((MAX_KEY_SHARES_PER_SET * 2 / 3) + 1).try_into().unwrap(),
MAX_KEY_SHARES_PER_SET.try_into().unwrap(),
)))
})
.downcast_ref()
.unwrap()
}
}
use generators::generators;
#[derive(Debug)]
pub struct KeyConfirmed<C: Ciphersuite> {
pub substrate_keys: Vec<ThresholdKeys<Ristretto>>,
@@ -66,7 +108,7 @@ impl GeneratedKeysDb {
fn save_keys<N: Network>(
txn: &mut impl DbTxn,
session: &Session,
substrate_keys: &[ThresholdCore<Ristretto>],
substrate_keys: &[ThresholdKeys<Ristretto>],
network_keys: &[ThresholdKeys<N::Curve>],
) {
let mut keys = Zeroizing::new(vec![]);
@@ -74,7 +116,7 @@ impl GeneratedKeysDb {
keys.extend(substrate_keys.serialize().as_slice());
keys.extend(network_keys.serialize().as_slice());
}
txn.put(Self::key(&session), keys);
txn.put(Self::key(session), keys);
}
}
@@ -176,7 +218,7 @@ fn coerce_keys<C: EvrfCurve>(
faulty.push(i);
// Generate a random key
let mut rng = ChaCha20Rng::from_seed(Blake2s256::digest(&key).into());
let mut rng = ChaCha20Rng::from_seed(Blake2s256::digest(key).into());
loop {
let mut repr = <<C::EmbeddedCurve as Ciphersuite>::G as GroupEncoding>::Repr::default();
rng.fill_bytes(repr.as_mut());
@@ -201,11 +243,7 @@ pub struct KeyGen<N: Network, D: Db> {
network_evrf_private_key: Zeroizing<<<N::Curve as EvrfCurve>::EmbeddedCurve as Ciphersuite>::F>,
}
impl<N: Network, D: Db> KeyGen<N, D>
where
<<N::Curve as EvrfCurve>::EmbeddedCurve as Ciphersuite>::G:
ec_divisors::DivisorCurve<FieldElement = <N::Curve as Ciphersuite>::F>,
{
impl<N: Network, D: Db> KeyGen<N, D> {
#[allow(clippy::new_ret_no_self)]
pub fn new(
db: D,
@@ -264,13 +302,6 @@ where
let network_evrf_public_keys =
evrf_public_keys.into_iter().map(|(_, key)| key).collect::<Vec<_>>();
// Save the params
ParamsDb::set(
txn,
&session,
&(threshold, substrate_evrf_public_keys, network_evrf_public_keys),
);
let mut participation = Vec::with_capacity(2048);
let mut faulty = HashSet::new();
{
@@ -278,9 +309,9 @@ where
for faulty_i in faulty_is {
faulty.insert(faulty_i);
}
let participation = EvrfDkg::<Ristretto>::participate(
EvrfDkg::<Ristretto>::participate(
&mut OsRng,
todo!("TODO"),
generators(),
context(session, SUBSTRATE_KEY_CONTEXT),
threshold,
&coerced_keys,
@@ -297,7 +328,7 @@ where
}
EvrfDkg::<N::Curve>::participate(
&mut OsRng,
todo!("TODO"),
generators(),
context(session, NETWORK_KEY_CONTEXT),
threshold,
&coerced_keys,
@@ -308,6 +339,13 @@ where
.unwrap();
}
// Save the params
ParamsDb::set(
txn,
&session,
&(threshold, substrate_evrf_public_keys, network_evrf_public_keys),
);
// Send back our Participation and all faulty parties
let mut faulty = faulty.into_iter().collect::<Vec<_>>();
faulty.sort();
@@ -324,21 +362,51 @@ where
CoordinatorMessage::Participation { session, participant, participation } => {
info!("Received participation from {:?}", participant);
// TODO: Read Pariticpations, declare faulty if necessary, then re-serialize
let substrate_participation: Vec<u8> = todo!("TODO");
let network_participation: Vec<u8> = todo!("TODO");
let (threshold, substrate_evrf_public_keys, network_evrf_public_keys) =
ParamsDb::get(txn, &session).unwrap();
let n = substrate_evrf_public_keys
.len()
.try_into()
.expect("performing a key gen with more than u16::MAX participants");
// Read these `Participation`s
// If they fail basic sanity checks, fail fast
let (substrate_participation, network_participation) = {
let mid_point = {
let mut participation = participation.as_slice();
let start_len = participation.len();
let blame = vec![ProcessorMessage::Blame { session, participant }];
if Participation::<Ristretto>::read(&mut participation, n).is_err() {
return blame;
}
let len_at_mid_point = participation.len();
if Participation::<N::Curve>::read(&mut participation, n).is_err() {
return blame;
};
// If they added random noise after their participations, they're faulty
// This prevents DoS by causing a slash upon such spam
if !participation.is_empty() {
return blame;
}
start_len - len_at_mid_point
};
// Instead of re-serializing the `Participation`s we read, we just use the relevant
// sections of the existing byte buffer
(participation[.. mid_point].to_vec(), participation[mid_point ..].to_vec())
};
// Since these are valid `Participation`s, save them
let (mut substrate_participations, mut network_participations) =
ParticipationDb::get(txn, &session)
.unwrap_or((HashMap::with_capacity(1), HashMap::with_capacity(1)));
assert!(
substrate_participations.insert(participant, substrate_participation).is_none(),
"received participation for someone multiple times"
);
assert!(
network_participations.insert(participant, network_participation).is_none(),
substrate_participations.insert(participant, substrate_participation).is_none() &&
network_participations.insert(participant, network_participation).is_none(),
"received participation for someone multiple times"
);
ParticipationDb::set(
@@ -355,7 +423,15 @@ where
for i in substrate_participations.keys() {
let evrf_public_key = evrf_public_keys[usize::from(u16::from(*i)) - 1];
// Removes from Vec to prevent double-counting
// Remove this key from the Vec to prevent double-counting
/*
Double-counting would be a risk if multiple participants shared an eVRF public key
and participated. This code does still allow such participants (in order to let
participants be weighted), and any one of them participating will count as all
participating. This is fine as any one such participant will be able to decrypt
the shares for themselves and all other participants, so this is still a key
generated by an amount of participants who could simply reconstruct the key.
*/
let start_len = evrf_public_keys.len();
evrf_public_keys.retain(|key| *key != evrf_public_key);
let end_len = evrf_public_keys.len();
@@ -371,7 +447,7 @@ where
let mut res = Vec::with_capacity(1);
let substrate_dkg = match EvrfDkg::<Ristretto>::verify(
&mut OsRng,
&todo!("TODO"),
generators(),
context(session, SUBSTRATE_KEY_CONTEXT),
threshold,
// Ignores the list of participants who couldn't have their keys coerced due to prior
@@ -382,14 +458,8 @@ where
.map(|(key, participation)| {
(
*key,
Participation::read(
&mut participation.as_slice(),
substrate_evrf_public_keys
.len()
.try_into()
.expect("performing a key gen with more than u16::MAX participants"),
)
.expect("prior read participation was invalid"),
Participation::read(&mut participation.as_slice(), n)
.expect("prior read participation was invalid"),
)
})
.collect(),
@@ -418,7 +488,7 @@ where
};
let network_dkg = match EvrfDkg::<N::Curve>::verify(
&mut OsRng,
&todo!("TODO"),
generators(),
context(session, NETWORK_KEY_CONTEXT),
threshold,
// Ignores the list of participants who couldn't have their keys coerced due to prior
@@ -429,14 +499,8 @@ where
.map(|(key, participation)| {
(
*key,
Participation::read(
&mut participation.as_slice(),
network_evrf_public_keys
.len()
.try_into()
.expect("performing a key gen with more than u16::MAX participants"),
)
.expect("prior read participation was invalid"),
Participation::read(&mut participation.as_slice(), n)
.expect("prior read participation was invalid"),
)
})
.collect(),
@@ -463,36 +527,23 @@ where
}
};
/*
let mut these_network_keys = ThresholdKeys::new(these_network_keys);
N::tweak_keys(&mut these_network_keys);
let substrate_keys = substrate_dkg.keys(&self.substrate_evrf_private_key);
let mut network_keys = network_dkg.keys(&self.network_evrf_private_key);
// TODO: Some of these keys may be decrypted by us, yet not actually meant for us, if
// another validator set our eVRF public key as their eVRF public key. We either need to
// ensure the coordinator tracks amount of shares we're supposed to have by the eVRF public
// keys OR explicitly reduce to the keys we're supposed to have based on our `i` index.
for network_keys in &mut network_keys {
N::tweak_keys(network_keys);
}
GeneratedKeysDb::save_keys::<N>(txn, &session, &substrate_keys, &network_keys);
substrate_keys.push(these_substrate_keys);
network_keys.push(these_network_keys);
let mut generated_substrate_key = None;
let mut generated_network_key = None;
for keys in substrate_keys.iter().zip(&network_keys) {
if generated_substrate_key.is_none() {
generated_substrate_key = Some(keys.0.group_key());
generated_network_key = Some(keys.1.group_key());
} else {
assert_eq!(generated_substrate_key, Some(keys.0.group_key()));
assert_eq!(generated_network_key, Some(keys.1.group_key()));
}
}
GeneratedKeysDb::save_keys::<N>(txn, &id, &substrate_keys, &network_keys);
ProcessorMessage::GeneratedKeyPair {
id,
substrate_key: generated_substrate_key.unwrap().to_bytes(),
// TODO: This can be made more efficient since tweaked keys may be a subset of keys
network_key: generated_network_key.unwrap().to_bytes().as_ref().to_vec(),
}
*/
todo!("TODO")
vec![ProcessorMessage::GeneratedKeyPair {
session,
substrate_key: substrate_keys[0].group_key().to_bytes(),
// TODO: This can be made more efficient since tweaked keys may be a subset of keys
network_key: network_keys[0].group_key().to_bytes().as_ref().to_vec(),
}]
}
}
}

View File

@@ -2,8 +2,11 @@ use std::{time::Duration, collections::HashMap};
use zeroize::{Zeroize, Zeroizing};
use transcript::{Transcript, RecommendedTranscript};
use ciphersuite::{group::GroupEncoding, Ciphersuite};
use ciphersuite::{
group::{ff::PrimeField, GroupEncoding},
Ciphersuite, Ristretto,
};
use dkg::evrf::EvrfCurve;
use log::{info, warn};
use tokio::time::sleep;
@@ -224,7 +227,9 @@ async fn handle_coordinator_msg<D: Db, N: Network, Co: Coordinator>(
match msg.msg.clone() {
CoordinatorMessage::KeyGen(msg) => {
coordinator.send(tributary_mutable.key_gen.handle(txn, msg)).await;
for msg in tributary_mutable.key_gen.handle(txn, msg) {
coordinator.send(msg).await;
}
}
CoordinatorMessage::Sign(msg) => {
@@ -485,41 +490,31 @@ async fn boot<N: Network, D: Db, Co: Coordinator>(
network: &N,
coordinator: &mut Co,
) -> (D, TributaryMutable<N, D>, SubstrateMutable<N, D>) {
let mut entropy_transcript = {
let entropy = Zeroizing::new(env::var("ENTROPY").expect("entropy wasn't specified"));
if entropy.len() != 64 {
panic!("entropy isn't the right length");
fn read_key_from_env<C: Ciphersuite>(label: &'static str) -> Zeroizing<C::F> {
let key_hex =
Zeroizing::new(env::var(label).unwrap_or_else(|| panic!("{label} wasn't provided")));
let bytes = Zeroizing::new(
hex::decode(key_hex).unwrap_or_else(|_| panic!("{label} wasn't a valid hex string")),
);
let mut repr = <C::F as PrimeField>::Repr::default();
if repr.as_ref().len() != bytes.len() {
panic!("{label} wasn't the correct length");
}
let mut bytes =
Zeroizing::new(hex::decode(entropy).map_err(|_| ()).expect("entropy wasn't hex-formatted"));
if bytes.len() != 32 {
bytes.zeroize();
panic!("entropy wasn't 32 bytes");
}
let mut entropy = Zeroizing::new([0; 32]);
let entropy_mut: &mut [u8] = entropy.as_mut();
entropy_mut.copy_from_slice(bytes.as_ref());
let mut transcript = RecommendedTranscript::new(b"Serai Processor Entropy");
transcript.append_message(b"entropy", entropy);
transcript
};
// TODO: Save a hash of the entropy to the DB and make sure the entropy didn't change
let mut entropy = |label| {
let mut challenge = entropy_transcript.challenge(label);
let mut res = Zeroizing::new([0; 32]);
let res_mut: &mut [u8] = res.as_mut();
res_mut.copy_from_slice(&challenge[.. 32]);
challenge.zeroize();
repr.as_mut().copy_from_slice(bytes.as_slice());
let res = Zeroizing::new(
Option::from(<C::F as PrimeField>::from_repr(repr))
.unwrap_or_else(|| panic!("{label} wasn't a valid scalar")),
);
repr.as_mut().zeroize();
res
};
}
// We don't need to re-issue GenerateKey orders because the coordinator is expected to
// schedule/notify us of new attempts
// TODO: Is this above comment still true? Not at all due to the planned lack of DKG timeouts?
let key_gen = KeyGen::<N, _>::new(raw_db.clone(), entropy(b"key-gen_entropy"));
let key_gen = KeyGen::<N, _>::new(
raw_db.clone(),
read_key_from_env::<<Ristretto as EvrfCurve>::EmbeddedCurve>("SUBSTRATE_EVRF_KEY"),
read_key_from_env::<<N::Curve as EvrfCurve>::EmbeddedCurve>("NETWORK_EVRF_KEY"),
);
let (multisig_manager, current_keys, actively_signing) =
MultisigManager::new(raw_db, network).await;

View File

@@ -241,9 +241,11 @@ pub struct PreparedSend<N: Network> {
}
#[async_trait]
#[rustfmt::skip]
pub trait Network: 'static + Send + Sync + Clone + PartialEq + Debug {
/// The elliptic curve used for this network.
type Curve: Curve + EvrfCurve;
type Curve: Curve
+ EvrfCurve<EmbeddedCurve: Ciphersuite<G: ec_divisors::DivisorCurve<FieldElement = <Self::Curve as Ciphersuite>::F>>>;
/// The type representing the transaction for this network.
type Transaction: Transaction<Self>; // TODO: Review use of