use std::collections::HashMap; use zeroize::Zeroizing; use rand_core::SeedableRng; use rand_chacha::ChaCha20Rng; use transcript::{Transcript, RecommendedTranscript}; use ciphersuite::group::GroupEncoding; use frost::{ curve::{Ciphersuite, Ristretto}, dkg::{ DkgError, Participant, ThresholdParams, ThresholdCore, ThresholdKeys, encryption::*, pedpop::*, }, }; use log::info; use serai_client::validator_sets::primitives::{Session, KeyPair}; use messages::key_gen::*; use crate::{Get, DbTxn, Db, create_db, networks::Network}; #[derive(Debug)] pub struct KeyConfirmed { pub substrate_keys: Vec>, pub network_keys: Vec>, } create_db!( KeyGenDb { ParamsDb: (session: &Session, attempt: u32) -> (ThresholdParams, u16), // Not scoped to the set since that'd have latter attempts overwrite former // A former attempt may become the finalized attempt, even if it doesn't in a timely manner // Overwriting its commitments would be accordingly poor CommitmentsDb: (key: &KeyGenId) -> HashMap>, GeneratedKeysDb: (session: &Session, substrate_key: &[u8; 32], network_key: &[u8]) -> Vec, // These do assume a key is only used once across sets, which holds true so long as a single // participant is honest in their execution of the protocol KeysDb: (network_key: &[u8]) -> Vec, SessionDb: (network_key: &[u8]) -> Session, NetworkKeyDb: (session: Session) -> Vec, } ); impl GeneratedKeysDb { #[allow(clippy::type_complexity)] fn read_keys( getter: &impl Get, key: &[u8], ) -> Option<(Vec, (Vec>, Vec>))> { let keys_vec = getter.get(key)?; let mut keys_ref: &[u8] = keys_vec.as_ref(); let mut substrate_keys = vec![]; let mut network_keys = vec![]; while !keys_ref.is_empty() { substrate_keys.push(ThresholdKeys::new(ThresholdCore::read(&mut keys_ref).unwrap())); let mut these_network_keys = ThresholdKeys::new(ThresholdCore::read(&mut keys_ref).unwrap()); N::tweak_keys(&mut these_network_keys); network_keys.push(these_network_keys); } Some((keys_vec, (substrate_keys, network_keys))) } fn save_keys( txn: &mut impl DbTxn, id: &KeyGenId, substrate_keys: &[ThresholdCore], network_keys: &[ThresholdKeys], ) { let mut keys = Zeroizing::new(vec![]); for (substrate_keys, network_keys) in substrate_keys.iter().zip(network_keys) { keys.extend(substrate_keys.serialize().as_slice()); keys.extend(network_keys.serialize().as_slice()); } txn.put( Self::key( &id.session, &substrate_keys[0].group_key().to_bytes(), network_keys[0].group_key().to_bytes().as_ref(), ), keys, ); } } impl KeysDb { fn confirm_keys( txn: &mut impl DbTxn, session: Session, key_pair: &KeyPair, ) -> (Vec>, Vec>) { let (keys_vec, keys) = GeneratedKeysDb::read_keys::( txn, &GeneratedKeysDb::key(&session, &key_pair.0 .0, key_pair.1.as_ref()), ) .unwrap(); assert_eq!(key_pair.0 .0, keys.0[0].group_key().to_bytes()); assert_eq!( { let network_key: &[u8] = key_pair.1.as_ref(); network_key }, keys.1[0].group_key().to_bytes().as_ref(), ); txn.put(Self::key(key_pair.1.as_ref()), keys_vec); NetworkKeyDb::set(txn, session, &key_pair.1.clone().into_inner()); SessionDb::set(txn, key_pair.1.as_ref(), &session); keys } #[allow(clippy::type_complexity)] fn keys( getter: &impl Get, network_key: &::G, ) -> Option<(Session, (Vec>, Vec>))> { let res = GeneratedKeysDb::read_keys::(getter, &Self::key(network_key.to_bytes().as_ref()))?.1; assert_eq!(&res.1[0].group_key(), network_key); Some((SessionDb::get(getter, network_key.to_bytes().as_ref()).unwrap(), res)) } pub fn substrate_keys_by_session( getter: &impl Get, session: Session, ) -> Option>> { let network_key = NetworkKeyDb::get(getter, session)?; Some(GeneratedKeysDb::read_keys::(getter, &Self::key(&network_key))?.1 .0) } } type SecretShareMachines = Vec<(SecretShareMachine, SecretShareMachine<::Curve>)>; type KeyMachines = Vec<(KeyMachine, KeyMachine<::Curve>)>; #[derive(Debug)] pub struct KeyGen { db: D, entropy: Zeroizing<[u8; 32]>, active_commit: HashMap, Vec>)>, #[allow(clippy::type_complexity)] active_share: HashMap, Vec>>)>, } impl KeyGen { #[allow(clippy::new_ret_no_self)] pub fn new(db: D, entropy: Zeroizing<[u8; 32]>) -> KeyGen { KeyGen { db, entropy, active_commit: HashMap::new(), active_share: HashMap::new() } } pub fn in_set(&self, session: &Session) -> bool { // We determine if we're in set using if we have the parameters for a session's key generation // The usage of 0 for the attempt is valid so long as we aren't malicious and accordingly // aren't fatally slashed // TODO: Revisit once we do DKG removals for being offline ParamsDb::get(&self.db, session, 0).is_some() } #[allow(clippy::type_complexity)] pub fn keys( &self, key: &::G, ) -> Option<(Session, (Vec>, Vec>))> { // This is safe, despite not having a txn, since it's a static value // It doesn't change over time/in relation to other operations KeysDb::keys::(&self.db, key) } pub fn substrate_keys_by_session( &self, session: Session, ) -> Option>> { KeysDb::substrate_keys_by_session::(&self.db, session) } pub fn handle( &mut self, txn: &mut D::Transaction<'_>, msg: CoordinatorMessage, ) -> ProcessorMessage { const SUBSTRATE_KEY_CONTEXT: &str = "substrate"; const NETWORK_KEY_CONTEXT: &str = "network"; let context = |id: &KeyGenId, key| { // TODO2: Also embed the chain ID/genesis block format!( "Serai Key Gen. Session: {:?}, Network: {:?}, Attempt: {}, Key: {}", id.session, N::NETWORK, id.attempt, key, ) }; let rng = |label, id: KeyGenId| { let mut transcript = RecommendedTranscript::new(label); transcript.append_message(b"entropy", &self.entropy); transcript.append_message(b"context", context(&id, "rng")); ChaCha20Rng::from_seed(transcript.rng_seed(b"rng")) }; let coefficients_rng = |id| rng(b"Key Gen Coefficients", id); let secret_shares_rng = |id| rng(b"Key Gen Secret Shares", id); let share_rng = |id| rng(b"Key Gen Share", id); let key_gen_machines = |id, params: ThresholdParams, shares| { let mut rng = coefficients_rng(id); let mut machines = vec![]; let mut commitments = vec![]; for s in 0 .. shares { let params = ThresholdParams::new( params.t(), params.n(), Participant::new(u16::from(params.i()) + s).unwrap(), ) .unwrap(); let substrate = KeyGenMachine::new(params, context(&id, SUBSTRATE_KEY_CONTEXT)) .generate_coefficients(&mut rng); let network = KeyGenMachine::new(params, context(&id, NETWORK_KEY_CONTEXT)) .generate_coefficients(&mut rng); machines.push((substrate.0, network.0)); let mut serialized = vec![]; substrate.1.write(&mut serialized).unwrap(); network.1.write(&mut serialized).unwrap(); commitments.push(serialized); } (machines, commitments) }; let secret_share_machines = |id, params: ThresholdParams, machines: SecretShareMachines, commitments: HashMap>| -> Result<_, ProcessorMessage> { let mut rng = secret_shares_rng(id); #[allow(clippy::type_complexity)] fn handle_machine( rng: &mut ChaCha20Rng, id: KeyGenId, machine: SecretShareMachine, commitments: HashMap>>, ) -> Result< (KeyMachine, HashMap>>), ProcessorMessage, > { match machine.generate_secret_shares(rng, commitments) { Ok(res) => Ok(res), Err(e) => match e { DkgError::ZeroParameter(_, _) | DkgError::InvalidThreshold(_, _) | DkgError::InvalidParticipant(_, _) | DkgError::InvalidSigningSet | DkgError::InvalidShare { .. } => unreachable!("{e:?}"), DkgError::InvalidParticipantQuantity(_, _) | DkgError::DuplicatedParticipant(_) | DkgError::MissingParticipant(_) => { panic!("coordinator sent invalid DKG commitments: {e:?}") } DkgError::InvalidCommitments(i) => { Err(ProcessorMessage::InvalidCommitments { id, faulty: i })? } }, } } let mut substrate_commitments = HashMap::new(); let mut network_commitments = HashMap::new(); for i in 1 ..= params.n() { let i = Participant::new(i).unwrap(); let mut commitments = commitments[&i].as_slice(); substrate_commitments.insert( i, EncryptionKeyMessage::>::read(&mut commitments, params) .map_err(|_| ProcessorMessage::InvalidCommitments { id, faulty: i })?, ); network_commitments.insert( i, EncryptionKeyMessage::>::read(&mut commitments, params) .map_err(|_| ProcessorMessage::InvalidCommitments { id, faulty: i })?, ); if !commitments.is_empty() { // Malicious Participant included extra bytes in their commitments // (a potential DoS attack) Err(ProcessorMessage::InvalidCommitments { id, faulty: i })?; } } let mut key_machines = vec![]; let mut shares = vec![]; for (m, (substrate_machine, network_machine)) in machines.into_iter().enumerate() { let actual_i = Participant::new(u16::from(params.i()) + u16::try_from(m).unwrap()).unwrap(); let mut substrate_commitments = substrate_commitments.clone(); substrate_commitments.remove(&actual_i); let (substrate_machine, mut substrate_shares) = handle_machine::(&mut rng, id, substrate_machine, substrate_commitments)?; let mut network_commitments = network_commitments.clone(); network_commitments.remove(&actual_i); let (network_machine, network_shares) = handle_machine(&mut rng, id, network_machine, network_commitments.clone())?; key_machines.push((substrate_machine, network_machine)); let mut these_shares: HashMap<_, _> = substrate_shares.drain().map(|(i, share)| (i, share.serialize())).collect(); for (i, share) in &mut these_shares { share.extend(network_shares[i].serialize()); } shares.push(these_shares); } Ok((key_machines, shares)) }; match msg { CoordinatorMessage::GenerateKey { id, params, shares } => { info!("Generating new key. ID: {id:?} Params: {params:?} Shares: {shares}"); // Remove old attempts if self.active_commit.remove(&id.session).is_none() && self.active_share.remove(&id.session).is_none() { // If we haven't handled this session before, save the params ParamsDb::set(txn, &id.session, id.attempt, &(params, shares)); } let (machines, commitments) = key_gen_machines(id, params, shares); self.active_commit.insert(id.session, (machines, commitments.clone())); ProcessorMessage::Commitments { id, commitments } } CoordinatorMessage::Commitments { id, mut commitments } => { info!("Received commitments for {:?}", id); if self.active_share.contains_key(&id.session) { // We should've been told of a new attempt before receiving commitments again // The coordinator is either missing messages or repeating itself // Either way, it's faulty panic!("commitments when already handled commitments"); } let (params, share_quantity) = ParamsDb::get(txn, &id.session, id.attempt).unwrap(); // Unwrap the machines, rebuilding them if we didn't have them in our cache // We won't if the processor rebooted // This *may* be inconsistent if we receive a KeyGen for attempt x, then commitments for // attempt y // The coordinator is trusted to be proper in this regard let (prior, our_commitments) = self .active_commit .remove(&id.session) .unwrap_or_else(|| key_gen_machines(id, params, share_quantity)); for (i, our_commitments) in our_commitments.into_iter().enumerate() { assert!(commitments .insert( Participant::new(u16::from(params.i()) + u16::try_from(i).unwrap()).unwrap(), our_commitments, ) .is_none()); } CommitmentsDb::set(txn, &id, &commitments); match secret_share_machines(id, params, prior, commitments) { Ok((machines, shares)) => { self.active_share.insert(id.session, (machines, shares.clone())); ProcessorMessage::Shares { id, shares } } Err(e) => e, } } CoordinatorMessage::Shares { id, shares } => { info!("Received shares for {:?}", id); let (params, share_quantity) = ParamsDb::get(txn, &id.session, id.attempt).unwrap(); // Same commentary on inconsistency as above exists let (machines, our_shares) = self.active_share.remove(&id.session).unwrap_or_else(|| { let prior = key_gen_machines(id, params, share_quantity).0; let (machines, shares) = secret_share_machines(id, params, prior, CommitmentsDb::get(txn, &id).unwrap()) .expect("got Shares for a key gen which faulted"); (machines, shares) }); let mut rng = share_rng(id); fn handle_machine( rng: &mut ChaCha20Rng, id: KeyGenId, // These are the params of our first share, not this machine's shares params: ThresholdParams, m: usize, machine: KeyMachine, shares_ref: &mut HashMap, ) -> Result, ProcessorMessage> { let params = ThresholdParams::new( params.t(), params.n(), Participant::new(u16::from(params.i()) + u16::try_from(m).unwrap()).unwrap(), ) .unwrap(); // Parse the shares let mut shares = HashMap::new(); for i in 1 ..= params.n() { let i = Participant::new(i).unwrap(); let Some(share) = shares_ref.get_mut(&i) else { continue }; shares.insert( i, EncryptedMessage::>::read(share, params).map_err(|_| { ProcessorMessage::InvalidShare { id, accuser: params.i(), faulty: i, blame: None } })?, ); } Ok( (match machine.calculate_share(rng, shares) { Ok(res) => res, Err(e) => match e { DkgError::ZeroParameter(_, _) | DkgError::InvalidThreshold(_, _) | DkgError::InvalidParticipant(_, _) | DkgError::InvalidSigningSet | DkgError::InvalidCommitments(_) => unreachable!("{e:?}"), DkgError::InvalidParticipantQuantity(_, _) | DkgError::DuplicatedParticipant(_) | DkgError::MissingParticipant(_) => { panic!("coordinator sent invalid DKG shares: {e:?}") } DkgError::InvalidShare { participant, blame } => { Err(ProcessorMessage::InvalidShare { id, accuser: params.i(), faulty: participant, blame: Some(blame.map(|blame| blame.serialize())).flatten(), })? } }, }) .complete(), ) } let mut substrate_keys = vec![]; let mut network_keys = vec![]; for (m, machines) in machines.into_iter().enumerate() { let mut shares_ref: HashMap = shares[m].iter().map(|(i, shares)| (*i, shares.as_ref())).collect(); for (i, our_shares) in our_shares.iter().enumerate() { if m != i { assert!(shares_ref .insert( Participant::new(u16::from(params.i()) + u16::try_from(i).unwrap()).unwrap(), our_shares [&Participant::new(u16::from(params.i()) + u16::try_from(m).unwrap()).unwrap()] .as_ref(), ) .is_none()); } } let these_substrate_keys = match handle_machine(&mut rng, id, params, m, machines.0, &mut shares_ref) { Ok(keys) => keys, Err(msg) => return msg, }; let these_network_keys = match handle_machine(&mut rng, id, params, m, machines.1, &mut shares_ref) { Ok(keys) => keys, Err(msg) => return msg, }; for i in 1 ..= params.n() { let i = Participant::new(i).unwrap(); let Some(shares) = shares_ref.get(&i) else { continue }; if !shares.is_empty() { return ProcessorMessage::InvalidShare { id, accuser: these_substrate_keys.params().i(), faulty: i, blame: None, }; } } let mut these_network_keys = ThresholdKeys::new(these_network_keys); N::tweak_keys(&mut these_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::(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(), } } CoordinatorMessage::VerifyBlame { id, accuser, accused, share, blame } => { let params = ParamsDb::get(txn, &id.session, id.attempt).unwrap().0; let mut share_ref = share.as_slice(); let Ok(substrate_share) = EncryptedMessage::< Ristretto, SecretShare<::F>, >::read(&mut share_ref, params) else { return ProcessorMessage::Blame { id, participant: accused }; }; let Ok(network_share) = EncryptedMessage::< N::Curve, SecretShare<::F>, >::read(&mut share_ref, params) else { return ProcessorMessage::Blame { id, participant: accused }; }; if !share_ref.is_empty() { return ProcessorMessage::Blame { id, participant: accused }; } let mut substrate_commitment_msgs = HashMap::new(); let mut network_commitment_msgs = HashMap::new(); let commitments = CommitmentsDb::get(txn, &id).unwrap(); for (i, commitments) in commitments { let mut commitments = commitments.as_slice(); substrate_commitment_msgs .insert(i, EncryptionKeyMessage::<_, _>::read(&mut commitments, params).unwrap()); network_commitment_msgs .insert(i, EncryptionKeyMessage::<_, _>::read(&mut commitments, params).unwrap()); } // There is a mild DoS here where someone with a valid blame bloats it to the maximum size // Given the ambiguity, and limited potential to DoS (this being called means *someone* is // getting fatally slashed) voids the need to ensure blame is minimal let substrate_blame = blame.clone().and_then(|blame| EncryptionKeyProof::read(&mut blame.as_slice()).ok()); let network_blame = blame.clone().and_then(|blame| EncryptionKeyProof::read(&mut blame.as_slice()).ok()); let substrate_blame = AdditionalBlameMachine::new( &mut rand_core::OsRng, context(&id, SUBSTRATE_KEY_CONTEXT), params.n(), substrate_commitment_msgs, ) .unwrap() .blame(accuser, accused, substrate_share, substrate_blame); let network_blame = AdditionalBlameMachine::new( &mut rand_core::OsRng, context(&id, NETWORK_KEY_CONTEXT), params.n(), network_commitment_msgs, ) .unwrap() .blame(accuser, accused, network_share, network_blame); // If the accused was blamed for either, mark them as at fault if (substrate_blame == accused) || (network_blame == accused) { return ProcessorMessage::Blame { id, participant: accused }; } ProcessorMessage::Blame { id, participant: accuser } } } } // This should only be called if we're participating, hence taking our instance #[allow(clippy::unused_self)] pub fn confirm( &mut self, txn: &mut D::Transaction<'_>, session: Session, key_pair: &KeyPair, ) -> KeyConfirmed { info!( "Confirmed key pair {} {} for {:?}", hex::encode(key_pair.0), hex::encode(&key_pair.1), session, ); let (substrate_keys, network_keys) = KeysDb::confirm_keys::(txn, session, key_pair); KeyConfirmed { substrate_keys, network_keys } } }