use crate::*; use crate::mock::*; use strum::IntoEnumIterator; use sp_std::{ prelude::Vec, collections::{btree_set::BTreeSet, btree_map::BTreeMap}, }; use frame_support::{assert_ok, assert_err, traits::Hooks}; use sp_runtime::testing::UintAuthorityId; use rand::Rng; use rand_chacha::rand_core::SeedableRng; use rand_chacha::ChaCha20Rng; use frost_core::Field; use frost_secp256k1::{ Secp256K1Sha256, Secp256K1ScalarField, Identifier as Secp256K1Identifier, keys::{ VerifyingShare as Secp256K1VerifyingShare, SigningShare as Secp256K1SigningShare, }, }; const MAX_SIGNERS: u16 = 10; const MIN_SIGNERS: u16 = 5; const DUMMY_HASH: ExodusHash = ExodusHash::repeat_byte(0x69); #[derive(Debug, PartialEq, Eq)] pub enum TestError { CallMismatch, PhaseMismatch, WorkerExecutionFailed, PoolIsEmpty, TransactionDecodeFailed, UnexpectedRuntimeCall, MempoolRejectedTransaction, OnChainApplicationFailed, InvalidStorageAfter, } fn decode_transaction(raw_tx: &[u8]) -> Option where Runtime: SendTransactionTypes, >::Extrinsic: Decode, >::Extrinsic: Into>, { let extrinsic = >::Extrinsic::decode(&mut &raw_tx[..]).ok()?; let test_extrinsic: Extrinsic = extrinsic.into(); Some(test_extrinsic.call) } fn run_increase_block_by(ext: &mut sp_io::TestExternalities, increase_by: u64) { ext.execute_with(|| { (0..increase_by).for_each(|_| { Exodus::on_finalize(System::block_number()); System::on_finalize(System::block_number()); System::set_block_number(System::block_number() + 1); System::on_initialize(System::block_number()); Exodus::on_initialize(System::block_number()); }); }); ext.persist_offchain_overlay(); } fn run_to_next_round(ext: &mut sp_io::TestExternalities, curve: NetworkCurve) { let target_block = ext.execute_with(|| { let stored = QualificationDkgState::::get(curve).get_block(); let delay = <::DkgRoundPeriod as frame_support::traits::Get< frame_system::pallet_prelude::BlockNumberFor >>::get(); stored + delay }); ext.execute_with(|| { while System::block_number() < target_block { Exodus::on_finalize(System::block_number()); System::on_finalize(System::block_number()); System::set_block_number(System::block_number() + 1); System::on_initialize(System::block_number()); Exodus::on_initialize(System::block_number()); } }); ext.persist_offchain_overlay(); } fn generate_mock_secp256k1_shares( indices: &[AuthIndex], ) -> BTreeMap { let mut shares = BTreeMap::new(); let mut rng = ChaCha20Rng::from_seed(Default::default()); for &i in indices { let random_scalar = ::random(&mut rng); let secret_share = Secp256K1SigningShare::new(random_scalar); let verifying_share = Secp256K1VerifyingShare::from(secret_share); let identifier = Secp256K1Identifier::try_from(i + 1).unwrap(); shares.insert(identifier, verifying_share); } shares } fn generate_mock_secp256k1_scalars( indices: &[AuthIndex], ) -> BTreeMap::Scalar> { let mut scalars = BTreeMap::new(); let mut rng = ChaCha20Rng::from_seed(Default::default()); for &i in indices { let random_scalar = ::random(&mut rng); let identifier = Secp256K1Identifier::try_from(i + 1).unwrap(); scalars.insert(identifier, random_scalar); } scalars } fn execute_dkg_happy_path( ext: &mut sp_io::TestExternalities, tx_pool_state: &std::sync::Arc>, curve: NetworkCurve, authority: u64, expected_phase: DkgPhase, mut execute_transaction: F1, mut apply_transaction: F2, mut check_storage_changed: F3, ) -> Result<(), TestError> where F1: FnMut( DkgStorage, NetworkCurve>, &QualifyingState, UintAuthorityId, ) -> Result, ExodusError>, F2: FnMut(Call) -> Result<(), TestError>, F3: FnMut(AuthIndex, NetworkCurve) -> Result<(), TestError>, { use sp_runtime::transaction_validity::TransactionSource; let state = ext.execute_with(|| QualificationDkgState::::get(&curve)); if state.get_phase() != expected_phase { return Err(TestError::PhaseMismatch); } let current_block = ext.execute_with(|| Exodus::current_block_number()); let block_longevity = ext.execute_with(|| { < ::UnsignedLongevity as frame_support::traits::Get >::get() }); let authority_index = authority as AuthIndex; let authority_key = authority.into(); let dkg_storage = DkgStorage::default() .with_network_curve(curve) .with_current_block(current_block) .with_block_longevity(block_longevity) .with_authority_index(authority_index); let round_result = ext.execute_with(|| { execute_transaction(dkg_storage, &state, authority_key) }); if round_result.is_err() { return Err(TestError::WorkerExecutionFailed); } let mut write_pool = tx_pool_state.write(); let raw_tx = write_pool.transactions.pop().ok_or(TestError::PoolIsEmpty)?; let runtime_call = decode_transaction::(&raw_tx) .ok_or(TestError::TransactionDecodeFailed)?; if let RuntimeCall::Exodus(inner_exodus_call) = runtime_call { ext.execute_with(|| { let is_mempool_ok = Exodus::validate_unsigned(TransactionSource::InBlock, &inner_exodus_call).is_ok(); if !is_mempool_ok { return Err(TestError::MempoolRejectedTransaction); } apply_transaction(inner_exodus_call) .map_err(|_| TestError::OnChainApplicationFailed)?; Ok(()) })?; } else { return Err(TestError::UnexpectedRuntimeCall); } write_pool.transactions.clear(); ext.execute_with(|| check_storage_changed(authority_index, curve))?; Ok(()) } fn execute_exodus_happy_path( ext: &mut sp_io::TestExternalities, tx_pool_state: &std::sync::Arc>, curve: NetworkCurve, authority: u64, mut apply_transaction: F1, mut check_storage_changed: F2, ) -> Result<(), TestError> where F1: FnMut(Call) -> Result<(), TestError>, F2: FnMut(ExodusOk) -> Result<(), TestError>, { use sp_runtime::transaction_validity::TransactionSource; let state = ext.execute_with(|| QualificationDkgState::::get(&curve)); if state.is_dkg_pending() { return Err(TestError::PhaseMismatch); } let active_authorities = ext.execute_with(|| { ActiveDkgAuthorities::::get(&curve) }); let current_block = ext.execute_with(|| Exodus::current_block_number()); let authority_index = authority as AuthIndex; let authority_key = authority.into(); let exodus_process_result = ext.execute_with(|| { Exodus::exodus_run_process( authority_index, authority_key, curve, current_block, &active_authorities, ) }); match exodus_process_result { Ok(ExodusOk::ExodusEmptyRequests(_, _)) => return Ok(()), Err(_) => return Err(TestError::WorkerExecutionFailed), _ => {} } let mut write_pool = tx_pool_state.write(); let raw_tx = write_pool.transactions.pop().ok_or(TestError::PoolIsEmpty)?; let runtime_call = decode_transaction::(&raw_tx) .ok_or(TestError::TransactionDecodeFailed)?; if let RuntimeCall::Exodus(inner_exodus_call) = runtime_call { ext.execute_with(|| { let is_mempool_ok = Exodus::validate_unsigned(TransactionSource::InBlock, &inner_exodus_call).is_ok(); if !is_mempool_ok { return Err(TestError::MempoolRejectedTransaction); } apply_transaction(inner_exodus_call) .map_err(|_| TestError::OnChainApplicationFailed)?; Ok(()) })?; } else { return Err(TestError::UnexpectedRuntimeCall); } write_pool.transactions.clear(); ext.execute_with(|| { check_storage_changed(exodus_process_result.unwrap()) })?; Ok(()) } fn execute_round0_happy_path( ext: &mut sp_io::TestExternalities, tx_pool_state: &std::sync::Arc>, curve: NetworkCurve, authority: u64, ) -> Result<(), TestError> { let execute_transaction = |sb, state: &_, key| Exodus::dkg_run_round0(sb, &state, key); let apply_transaction = |call| match call { Call::register_round0_package { dkg_package, signature } => { Exodus::register_round0_package(RuntimeOrigin::none(), dkg_package, signature) .map_err(|_| TestError::OnChainApplicationFailed) }, _ => Err(TestError::CallMismatch), }; let check_storage_changed = |_, curve| { for (_, hash) in Round0Packages::::iter_prefix(&curve) { if hash == ExodusHash::default() || hash == DUMMY_HASH { return Err(TestError::InvalidStorageAfter); } } Ok(()) }; execute_dkg_happy_path( ext, &tx_pool_state, curve, authority, DkgPhase::Round0, execute_transaction, apply_transaction, check_storage_changed, ) } fn execute_round1_happy_path( ext: &mut sp_io::TestExternalities, tx_pool_state: &std::sync::Arc>, curve: NetworkCurve, authority: u64, ) -> Result<(), TestError> { let execute_transaction = |sb, state: &_, key| Exodus::dkg_run_round1(sb, &state, key); let apply_transaction = |call| match call { Call::register_round1_package { dkg_package, signature } => { Exodus::register_round1_package(RuntimeOrigin::none(), dkg_package, signature) .map_err(|_| TestError::OnChainApplicationFailed) }, _ => Err(TestError::CallMismatch), }; let check_storage_changed = |idx, curve| { let participants = QualificationAuthorities::::decode_len(&curve).unwrap(); let round1_packages = Round1Packages::::get(curve); let round1_contains_idx = round1_packages.contains(idx); let round1_valid_len = round1_packages.active_bits_count() == participants as u32; if !round1_contains_idx || !round1_valid_len { return Err(TestError::InvalidStorageAfter); } Ok(()) }; execute_dkg_happy_path( ext, &tx_pool_state, curve, authority, DkgPhase::Round1, execute_transaction, apply_transaction, check_storage_changed, ) } fn execute_round2_happy_path( ext: &mut sp_io::TestExternalities, tx_pool_state: &std::sync::Arc>, curve: NetworkCurve, authority: u64, ) -> Result<(), TestError> { let execute_transaction = |sb, state: &_, key| Exodus::dkg_run_round2(sb, &state, key); let apply_transaction = |call| match call { Call::register_encrypted_round2_packages { dkg_package, signature } => { Exodus::register_encrypted_round2_packages(RuntimeOrigin::none(), dkg_package, signature) .map_err(|_| TestError::OnChainApplicationFailed) }, _ => Err(TestError::CallMismatch), }; let check_storage_changed = |idx, curve| { let participants = QualificationAuthorities::::decode_len(&curve).unwrap(); let round2_packages = Round2Packages::::get(curve); let round2_contains_idx = round2_packages.contains(idx); let round2_valid_len = round2_packages.active_bits_count() == participants as u32; if !round2_contains_idx || !round2_valid_len { return Err(TestError::InvalidStorageAfter); } Ok(()) }; execute_dkg_happy_path( ext, &tx_pool_state, curve, authority, DkgPhase::Round2, execute_transaction, apply_transaction, check_storage_changed, ) } fn execute_round3_happy_path( ext: &mut sp_io::TestExternalities, tx_pool_state: &std::sync::Arc>, curve: NetworkCurve, authority: u64, ) -> Result<(), TestError> { let execute_transaction = |sb, state: &_, key| Exodus::dkg_run_round3(sb, &state, key); let apply_transaction = |call| match call { Call::register_round3_complaints { dkg_package, signature } => { Exodus::register_round3_complaints(RuntimeOrigin::none(), dkg_package, signature) .map_err(|_| TestError::OnChainApplicationFailed) }, _ => Err(TestError::CallMismatch), }; let check_storage_changed = |idx, curve| { let participants = QualificationAuthorities::::decode_len(&curve).unwrap(); let complaints = Complaints::::get(curve); let complaints_valid = complaints.get(&idx) .map(|bitmap| { bitmap.active_bits_count() <= participants as u32 }) .unwrap_or(false); if !complaints_valid { return Err(TestError::InvalidStorageAfter); } Ok(()) }; execute_dkg_happy_path( ext, &tx_pool_state, curve, authority, DkgPhase::Round3, execute_transaction, apply_transaction, check_storage_changed, ) } fn execute_round4_happy_path( ext: &mut sp_io::TestExternalities, tx_pool_state: &std::sync::Arc>, curve: NetworkCurve, authority: u64, ) -> Result<(), TestError> { let execute_transaction = |sb, state: &_, key| Exodus::dkg_run_round4(sb, &state, key); let apply_transaction = |call| match call { Call::register_round4_justifications { dkg_package, signature } => { Exodus::register_round4_justifications(RuntimeOrigin::none(), dkg_package, signature) .map_err(|_| TestError::OnChainApplicationFailed) }, _ => Err(TestError::CallMismatch), }; let check_storage_changed = |idx, curve| { let curr_dkg_index = QualificationDkgState::::get(curve).get_dkg_index(); let participants = QualificationAuthorities::::decode_len(&curve).unwrap(); let justifications = Justifications::::get(curve); let justifications_valid = justifications.get(&idx) .map(|bitmap| { bitmap.active_bits_count() <= participants as u32 }) .unwrap_or(false); if !justifications_valid { return Err(TestError::InvalidStorageAfter); } let non_empty_past_verifications = (0..curr_dkg_index).any(|dkg_index| { Verifications::::iter_key_prefix((curve, dkg_index)) .next() .is_some() }); if non_empty_past_verifications { return Err(TestError::InvalidStorageAfter); } Ok(()) }; execute_dkg_happy_path( ext, &tx_pool_state, curve, authority, DkgPhase::Round4, execute_transaction, apply_transaction, check_storage_changed, ) } fn execute_round5_happy_path( ext: &mut sp_io::TestExternalities, tx_pool_state: &std::sync::Arc>, curve: NetworkCurve, authority: u64, ) -> Result<(), TestError> { let execute_transaction = |sb, state: &_, key| Exodus::dkg_run_round5(sb, &state, key); let apply_transaction = |call| match call { Call::register_round5_verifying_package { dkg_package, signature } => { Exodus::register_round5_verifying_package(RuntimeOrigin::none(), dkg_package, signature) .map_err(|_| TestError::OnChainApplicationFailed) }, _ => Err(TestError::CallMismatch), }; let check_storage_changed = |_, curve| { let curr_dkg_index = QualificationDkgState::::get(curve).get_dkg_index(); let non_empty_verification_states = (0..curr_dkg_index).any(|dkg_index| { VerifyingKeyConsensus::::contains_key(curve, dkg_index) }); if non_empty_verification_states { return Err(TestError::InvalidStorageAfter); } Ok(()) }; execute_dkg_happy_path( ext, &tx_pool_state, curve, authority, DkgPhase::Round5, execute_transaction, apply_transaction, check_storage_changed, ) } fn execute_round6_happy_path( ext: &mut sp_io::TestExternalities, tx_pool_state: &std::sync::Arc>, curve: NetworkCurve, authority: u64, ) -> Result<(), TestError> { let execute_transaction = |sb, state: &_, key| Exodus::dkg_run_round6(sb, &state, key); let apply_transaction = |call| match call { Call::register_round6_public_share_package { dkg_package, signature } => { Exodus::register_round6_public_share_package(RuntimeOrigin::none(), dkg_package, signature) .map_err(|_| TestError::OnChainApplicationFailed) }, _ => Err(TestError::CallMismatch), }; let check_storage_changed = |idx, curve| { let curr_dkg_index = QualificationDkgState::::get(curve).get_dkg_index(); let verifying_share_key = (curve, curr_dkg_index, idx); if VerifyingShares::::get(verifying_share_key).is_none() { return Err(TestError::InvalidStorageAfter); } Ok(()) }; execute_dkg_happy_path( ext, &tx_pool_state, curve, authority, DkgPhase::Round6, execute_transaction, apply_transaction, check_storage_changed, ) } fn run_succesfull_dkg_session_partially( ext: &mut sp_io::TestExternalities, tx_pool_state: &std::sync::Arc>, raw_authorities: &[u64], curve: NetworkCurve, ) -> (u64, u64) { let authorities: Vec = raw_authorities.iter().map(|&id| id.into()).collect(); let prev_dkg_index = ext.execute_with(|| QualificationDkgState::::get(curve).get_dkg_index()); ext.execute_with(|| Exodus::start_dkg_qualification(authorities, NetworkCurve::iter())); let dkg_index = ext.execute_with(|| QualificationDkgState::::get(curve).get_dkg_index()); let (start_block, round_delay) = ext.execute_with(|| { let round_delay = <::DkgRoundPeriod as frame_support::traits::Get< frame_system::pallet_prelude::BlockNumberFor >>::get(); let start_block = System::block_number(); (start_block, round_delay) }); assert_eq!(prev_dkg_index + 1, dkg_index); ext.execute_with(|| { let dummy_oversized_bitmap = ParticipantsBitmap::::all_ones(1337); let mut map: BitmapByAuthority = Default::default(); for authority in raw_authorities.iter() { let auth_idx = *authority as AuthIndex; Round0Packages::::insert(curve, auth_idx, DUMMY_HASH); map.try_insert(auth_idx, dummy_oversized_bitmap.clone()).unwrap(); } Round1Packages::::insert(curve, dummy_oversized_bitmap.clone()); Round2Packages::::insert(curve, dummy_oversized_bitmap.clone()); let prev_verifications_key1 = (curve, prev_dkg_index, DUMMY_HASH); let prev_verifications_key2 = (curve, prev_dkg_index, ExodusHash::default()); Verifications::::insert(prev_verifications_key1, dummy_oversized_bitmap.clone()); Verifications::::insert(prev_verifications_key2, dummy_oversized_bitmap.clone()); let dummy_verifying_key = BoundedVec::>::try_from( vec![69u8; ELEMENT_MAX_BYTES as usize], ).unwrap(); let dummy_consensus_state = ConsensusState::new( dummy_verifying_key, dummy_oversized_bitmap, DUMMY_HASH, ); VerifyingKeyConsensus::::insert(curve, prev_dkg_index, dummy_consensus_state); }); let dkg_rounds = [ execute_round0_happy_path, execute_round1_happy_path, execute_round2_happy_path, execute_round3_happy_path, execute_round4_happy_path, execute_round5_happy_path, execute_round6_happy_path, ]; for dkg_round in dkg_rounds { run_to_next_round(ext, curve); for authority in raw_authorities.iter() { assert_ok!(dkg_round(ext, &tx_pool_state, curve, *authority)); } } run_to_next_round(ext, curve); (start_block, round_delay) } fn run_initial_dkg_session(raw_authorities: &[u64], curve: NetworkCurve) -> ( sp_io::TestExternalities, std::sync::Arc>, ) { let (mut ext, tx_pool_state) = new_test_ext(); let prev_verifying_key = ext.execute_with(|| ActiveVerifyingKey::::get(curve)); assert!(prev_verifying_key.is_empty()); let (start_block, round_delay) = run_succesfull_dkg_session_partially(&mut ext, &tx_pool_state, &raw_authorities, curve); run_to_next_round(&mut ext, curve); let curr_verifying_key = ext.execute_with(|| ActiveVerifyingKey::::get(curve)); assert!(!curr_verifying_key.is_empty()); let state = ext.execute_with(|| QualificationDkgState::::get(curve)); let authorities_len = raw_authorities.len(); for authority in raw_authorities { let member = *authority as AuthIndex; assert!(state.contains_index(member)); } assert_eq!(state.get_block(), start_block + round_delay * 9); assert_eq!(state.get_dkg_index(), 1); assert_eq!(state.get_phase(), DkgPhase::Finalized); assert_eq!(state.count_ones::(), authorities_len); assert_eq!(state.get_indexes().active_bits_count(), authorities_len as u32); let active = ext.execute_with(|| ActiveDkgAuthorities::::get(curve)); assert_eq!(active.get_indexes(), state.get_indexes()); assert_eq!(active.get_dkg_index(), state.get_dkg_index()); let active_authorities = ext.execute_with(|| ActiveAuthorities::::get(curve)); let qualification_authorities = ext.execute_with(|| QualificationAuthorities::::get(curve)); assert_eq!(active_authorities, qualification_authorities); (ext, tx_pool_state) } fn run_dkg_session( ext: &mut sp_io::TestExternalities, tx_pool_state: &std::sync::Arc>, prev_authorities: Vec, raw_authorities: Vec, curve: NetworkCurve, ) { let prev_active_authorities = ext.execute_with(|| ActiveAuthorities::::get(curve)); let prev_verifying_key = ext.execute_with(|| ActiveVerifyingKey::::get(curve)); assert!(!prev_verifying_key.is_empty()); let dkg_index = ext.execute_with(|| { QualificationDkgState::::get(curve).get_dkg_index() }); let next_dkg_index = dkg_index.saturating_add(1); let (start_block, round_delay) = run_succesfull_dkg_session_partially(ext, tx_pool_state, &raw_authorities, curve); let register_nonce = |call| { match call { Call::register_nonce_commitment { exodus_package, signature } => { Exodus::register_nonce_commitment(RuntimeOrigin::none(), exodus_package, signature) .map_err(|_| TestError::OnChainApplicationFailed) } _ => Err(TestError::CallMismatch), } }; let register_group = |call| { match call { Call::register_group_commitment { exodus_package, signature } => { Exodus::register_group_commitment(RuntimeOrigin::none(), exodus_package, signature) .map_err(|_| TestError::OnChainApplicationFailed) } _ => Err(TestError::CallMismatch), } }; let register_share = |call| { match call { Call::register_signature_share { exodus_package, signature } => { Exodus::register_signature_share(RuntimeOrigin::none(), exodus_package, signature) .map_err(|_| TestError::OnChainApplicationFailed) } _ => Err(TestError::CallMismatch), } }; let check_nonce = |exodus_ok| { match exodus_ok { ExodusOk::ExodusNonceCommitted(exodus, roast, idx, _) => { if !NonceCommitments::::contains_key((exodus, roast), idx) { return Err(TestError::InvalidStorageAfter); } if !RoastSessions::::contains_key(exodus, idx) { return Err(TestError::InvalidStorageAfter); } let roast_state = RoastSessionStates::::get(exodus, roast); if !roast_state.nonce_committers.contains(idx) || roast_state.group_committers.contains(idx) || roast_state.partial_signers.contains(idx) { return Err(TestError::InvalidStorageAfter); } Ok(()) } _ => Err(TestError::InvalidStorageAfter), } }; let check_group = |exodus_ok| { match exodus_ok { ExodusOk::ExodusGroupCommitted(exodus, roast, idx, _) => { if !RoastSessions::::contains_key(exodus, idx) { return Err(TestError::InvalidStorageAfter); } if !GroupCommitters::::iter_prefix_values((exodus, roast)) .any(|participants| participants.contains(idx)) { return Err(TestError::InvalidStorageAfter); } let roast_state = RoastSessionStates::::get(exodus, roast); let is_nonce_committer = roast_state.nonce_committers.contains(idx); let is_group_committer = roast_state.group_committers.contains(idx); if !is_nonce_committer || !is_group_committer { return Err(TestError::InvalidStorageAfter); } Ok(()) } _ => Err(TestError::InvalidStorageAfter), } }; let check_share = |exodus_ok| { match exodus_ok { ExodusOk::ExodusSignedPartially(exodus, roast, idx, curve) => { if RoastSessions::::contains_key(exodus, idx) { return Err(TestError::InvalidStorageAfter); } if RoastSessions::::contains_key(exodus, idx) { return Err(TestError::InvalidStorageAfter); } if ExodusRequests::::contains_key(curve, exodus) { let roast_state = RoastSessionStates::::get(exodus, roast); if !roast_state.partial_signers.contains(idx) || !roast_state.nonce_committers.contains(idx) || !roast_state.group_committers.contains(idx) { return Err(TestError::InvalidStorageAfter); } } else { if RoastSessionStates::::contains_key(exodus, roast) { return Err(TestError::InvalidStorageAfter); } if SignatureScalars::::contains_key(exodus, roast) { return Err(TestError::InvalidStorageAfter); } if GroupCommitmentsConsensus::::contains_key(exodus, roast) { return Err(TestError::InvalidStorageAfter); } } Ok(()) } _ => Err(TestError::InvalidStorageAfter), } }; let threshold = ext.execute_with(|| { let len = ActiveAuthorities::::decode_len(&curve) .unwrap_or(0); get_byzantium_threshold(len) as u16 }); let mut exodus_steps_outer = BTreeMap::new(); let mut roast_sessions_outer = BTreeMap::new(); let mut roast_participants_outer = BTreeMap::new(); let mut malicious_participants_outer = BTreeMap::new(); let mut throttled_participants_outer = BTreeMap::new(); let malicious = prev_authorities.first().copied().unwrap(); let throttled = prev_authorities.last().copied().unwrap(); let prev_len = prev_authorities.len() as AuthIndex; let mut start = 0; loop { if start >= 100 { break; } start += 1; let mut current_pending_requests: Vec = ext.execute_with(|| { ExodusRequests::::iter_keys() .filter_map(|(current_curve, exodus_session)| { curve.eq(¤t_curve).then(|| exodus_session) }) .collect() }); current_pending_requests.sort(); if let Some(exodus_session) = current_pending_requests.first() { if !current_pending_requests.contains(exodus_session) { continue; } let exodus_steps: &mut BTreeMap = exodus_steps_outer.entry(*exodus_session).or_default(); let roast_sessions: &mut BTreeMap = roast_sessions_outer.entry(*exodus_session).or_default(); let roast_participants: &mut BTreeMap = roast_participants_outer.entry(*exodus_session).or_default(); let malicious_participants: &mut BTreeSet = malicious_participants_outer.entry(*exodus_session).or_default(); let throttled_participants: &mut BTreeSet = throttled_participants_outer.entry(*exodus_session).or_default(); for auth in prev_authorities.iter() { let request_exists = ext.execute_with(|| { ExodusRequests::::contains_key(&curve, &exodus_session) }); if !request_exists { break; } if malicious_participants.contains(auth) { continue; } if throttled_participants.contains(auth) { continue; } let mut current_step = exodus_steps.get(&auth).copied().unwrap_or(0); let mut roast_session = roast_sessions.get(&auth).copied().unwrap_or(0); let mut participants = roast_participants .get(&roast_session) .copied() .unwrap_or((0, 0, 0)); let mut has_enough_nonces = participants.0 >= threshold; let mut has_enough_groups = participants.1 >= threshold; if has_enough_nonces && current_step == 0 { roast_session += 1; participants = roast_participants .get(&roast_session) .copied() .unwrap_or((0, 0, 0)); has_enough_nonces = participants.0 >= threshold; has_enough_groups = participants.1 >= threshold; } let result = match current_step { 0 => { current_step += 1; participants.0 += 1; execute_exodus_happy_path(ext, tx_pool_state, curve, *auth, register_nonce, check_nonce) }, 1 if has_enough_nonces => { participants.1 += 1; let result = execute_exodus_happy_path(ext, tx_pool_state, curve, *auth, register_group, check_group); if malicious == *auth && prev_len > threshold { ext.execute_with(|| { let raw_vec = vec![69u8; ELEMENT_MAX_BYTES as usize]; let fake_group_commitment = BoundedVec::>::try_from(raw_vec) .unwrap(); let empty_participants = ParticipantsBitmap::::empty(prev_len); let mut self_participant = empty_participants.clone(); self_participant.insert(malicious); let (correct_commitment, correct_root) = GroupCommitmentsConsensus::::mutate( &exodus_session, &roast_session, |consensus| { let binding_root = consensus.merkle_root; let group_commitment = consensus .element_bytes .clone(); *consensus = ConsensusState::new( fake_group_commitment.clone(), consensus.participants.clone(), DUMMY_HASH, ); (group_commitment, binding_root) }); let prev_group_key = (exodus_session, roast_session, correct_root, correct_commitment); GroupCommitters::::insert(prev_group_key, empty_participants); let fake_group_key = (exodus_session, roast_session, DUMMY_HASH, fake_group_commitment); GroupCommitters::::insert(fake_group_key, self_participant); malicious_participants.insert(malicious); }); } else { current_step += 1; } result } 2 if has_enough_groups => { if prev_len > threshold + 1 && *auth == throttled { throttled_participants.insert(*auth); continue; } current_step = 0; participants.2 += 1; execute_exodus_happy_path(ext, tx_pool_state, curve, *auth, register_share, check_share).unwrap(); exodus_steps.insert(*auth, current_step); roast_sessions.insert(*auth, roast_session + 1); roast_participants.insert(roast_session, participants); continue; } _ => continue, }; result.unwrap(); exodus_steps.insert(*auth, current_step); roast_sessions.insert(*auth, roast_session); roast_participants.insert(roast_session, participants); } run_increase_block_by(ext, 1); } else { break; } } run_to_next_round(ext, curve); let curr_verifying_key = ext.execute_with(|| ActiveVerifyingKey::::get(curve)); assert!(curr_verifying_key != prev_verifying_key); let state = ext.execute_with(|| QualificationDkgState::::get(curve)); let authorities_len = raw_authorities.len(); for authority in raw_authorities { let member = authority as AuthIndex; assert!(state.contains_index(member)); } assert_eq!(state.get_block(), start_block + round_delay * 9); assert_eq!(state.get_dkg_index(), next_dkg_index); assert_eq!(state.get_phase(), DkgPhase::Finalized); assert_eq!(state.count_ones::(), authorities_len); assert_eq!(state.get_indexes().active_bits_count(), authorities_len as u32); let active = ext.execute_with(|| ActiveDkgAuthorities::::get(curve)); assert_eq!(active.get_indexes(), state.get_indexes()); assert_eq!(active.get_dkg_index(), state.get_dkg_index()); let active_authorities = ext.execute_with(|| ActiveAuthorities::::get(curve)); let qualification_authorities = ext.execute_with(|| QualificationAuthorities::::get(curve)); assert_eq!(active_authorities, qualification_authorities); assert_ne!(prev_active_authorities, active_authorities); } #[test] fn test_all_validators_can_execute_initial_dkg() { let curve = NetworkCurve::Secp256k1; let raw_authorities = FixedAuthorities::get(); run_initial_dkg_session(&raw_authorities, curve); } #[test] fn test_all_validators_can_execute_dkg() { let curve = NetworkCurve::Secp256k1; let authorities_1 = FixedAuthorities::get(); let (mut ext, tx_pool_state) = run_initial_dkg_session(&authorities_1, curve); ext.execute_with(|| { Exodus::do_register_evm_bridge_out_exodus( 31_337, 69, Default::default(), EvmAddress::from_low_u64_be(1), ).unwrap(); }); let mut authorities_2 = authorities_1.clone(); let new_authority = authorities_2.iter().max().unwrap() + 1; authorities_2.push(new_authority); authorities_2.push(new_authority + 1); authorities_2.push(new_authority + 2); run_dkg_session(&mut ext, &tx_pool_state, authorities_1, authorities_2.clone(), curve); ext.execute_with(|| { Exodus::do_register_evm_bridge_out_exodus( 31_337, 420, Perbill::from_percent(50), EvmAddress::from_low_u64_be(2), ).unwrap(); }); let mut authorities_3 = FixedAuthorities::get(); authorities_3.push(new_authority); run_dkg_session(&mut ext, &tx_pool_state, authorities_2, authorities_3, curve); let rotations_count = ext.execute_with(|| { ExodusSignedRotations::::iter().count() }); let bridges_count = ext.execute_with(|| { ExodusSignedBridges::::iter().count() }); assert_eq!(rotations_count, 3); assert_eq!(bridges_count, 2); } #[test] fn test_exodus_dkg_robustness() { let (mut ext, tx_pool_state) = new_test_ext(); let curve = NetworkCurve::Secp256k1; let raw_authorities = FixedAuthorities::get(); let authorities: Vec = raw_authorities.iter().map(|&id| id.into()).collect(); let mut excluded_authorities = sp_std::collections::btree_set::BTreeSet::new(); let dkg_rounds = [ execute_round0_happy_path, execute_round1_happy_path, execute_round2_happy_path, execute_round3_happy_path, execute_round4_happy_path, execute_round5_happy_path, execute_round6_happy_path, ]; let mut dkg_index = ext.execute_with(|| { Exodus::start_dkg_qualification(authorities, NetworkCurve::iter()); QualificationDkgState::::get(curve).get_dkg_index() }); let (start_block, round_delay) = ext.execute_with(|| { let round_delay = <::DkgRoundPeriod as frame_support::traits::Get< frame_system::pallet_prelude::BlockNumberFor >>::get(); let start_block = System::block_number(); (start_block, round_delay) }); let mut failed_at_round = None; let mut total_round_idx = 0; let mut round_idx = 0; loop { run_to_next_round(&mut ext, curve); total_round_idx += 1; let phase = ext.execute_with(|| { QualificationDkgState::::get(curve).get_phase() }); match phase { DkgPhase::Finalized => break, DkgPhase::Round7 => continue, DkgPhase::Vacant => { failed_at_round = Some(round_idx); dkg_index += 1; round_idx = 0; excluded_authorities.clear(); continue; } _ => {} } let is_fast_track = failed_at_round .map(|failed_round| round_idx < failed_round) .unwrap_or(false); let mut maybe_lost_authority = None; if !is_fast_track { maybe_lost_authority = Some((total_round_idx % raw_authorities.len()) as u64); } for authority in raw_authorities.iter() { if is_fast_track { assert_ok!(dkg_rounds[round_idx](&mut ext, &tx_pool_state, curve, *authority)); continue; } if let Some(lost_auth) = maybe_lost_authority { if *authority == lost_auth { continue; } } if excluded_authorities.contains(authority) { if round_idx != 0 { assert_err!( dkg_rounds[round_idx](&mut ext, &tx_pool_state, curve, *authority), TestError::PoolIsEmpty, ); } continue; } assert_ok!(dkg_rounds[round_idx](&mut ext, &tx_pool_state, curve, *authority)); } if !is_fast_track { if let Some(lost_auth) = maybe_lost_authority { excluded_authorities.insert(lost_auth); } } round_idx += 1; } let state = ext.execute_with(|| QualificationDkgState::::get(curve)); let authorities_len = raw_authorities.len(); let qualification_count = raw_authorities .iter() .filter(|&authority| { let member = *authority as AuthIndex; state.contains_index(member) }) .count(); let estimated_block = start_block + round_delay * (total_round_idx as u64); assert_eq!(state.get_block(), estimated_block); assert_eq!(state.get_dkg_index(), dkg_index); assert_eq!(state.get_phase(), DkgPhase::Finalized); let bft_threshold = get_byzantium_threshold(authorities_len); assert!(qualification_count >= bft_threshold); assert!(state.count_ones::() >= bft_threshold); assert_eq!(state.get_indexes().active_bits_count(), authorities_len as u32); let active = ext.execute_with(|| ActiveDkgAuthorities::::get(curve)); assert_eq!(active.get_indexes(), state.get_indexes()); assert_eq!(active.get_dkg_index(), state.get_dkg_index()); let active_authorities = ext.execute_with(|| ActiveAuthorities::::get(curve)); let qualification_authorities = ext.execute_with(|| QualificationAuthorities::::get(curve)); assert_eq!(active_authorities, qualification_authorities); } #[test] fn test_prepare_round4_abort_on_byzantine_attack() { let (mut ext, tx_pool_state) = new_test_ext(); let curve = NetworkCurve::Secp256k1; let raw_authorities = FixedAuthorities::get(); let authorities: Vec = raw_authorities.iter().map(|&id| id.into()).collect(); let authorities_len = authorities.len(); ext.execute_with(|| Exodus::start_dkg_qualification(authorities, NetworkCurve::iter())); let dkg_rounds = [ execute_round0_happy_path, execute_round1_happy_path, execute_round2_happy_path, execute_round3_happy_path, ]; for dkg_round in dkg_rounds { run_to_next_round(&mut ext, curve); for authority in raw_authorities.iter() { assert_ok!(dkg_round(&mut ext, &tx_pool_state, curve, *authority)); } } let bft_threshold = get_byzantium_threshold(authorities_len); ext.execute_with(|| { let mut complaints_map = BitmapByAuthority::::default(); for authority in 0..=bft_threshold { let auth_index = authority as AuthIndex; let complaints = ParticipantsBitmap::::all_ones(authorities_len); complaints_map.try_insert(auth_index, complaints).unwrap(); } Complaints::::insert(curve, complaints_map); }); run_to_next_round(&mut ext, curve); let state = ext.execute_with(|| QualificationDkgState::::get(curve)); assert!(state.is_dkg_vacant()); assert!(state.get_indexes().is_empty()); } #[test] fn test_prepare_round5_abort_on_byzantine_attack() { let (mut ext, tx_pool_state) = new_test_ext(); let curve = NetworkCurve::Secp256k1; let raw_authorities = FixedAuthorities::get(); let authorities: Vec = raw_authorities.iter().map(|&id| id.into()).collect(); let authorities_len = authorities.len(); ext.execute_with(|| Exodus::start_dkg_qualification(authorities, NetworkCurve::iter())); let dkg_rounds = [ execute_round0_happy_path, execute_round1_happy_path, execute_round2_happy_path, execute_round3_happy_path, execute_round4_happy_path, ]; for dkg_round in dkg_rounds { run_to_next_round(&mut ext, curve); for authority in raw_authorities.iter() { assert_ok!(dkg_round(&mut ext, &tx_pool_state, curve, *authority)); } } let bft_threshold = get_byzantium_threshold(authorities_len); ext.execute_with(|| { let mut justifications_map = BitmapByAuthority::::default(); for authority in 0..=bft_threshold { let auth_index = authority as AuthIndex; let justifications = ParticipantsBitmap::::all_ones(authorities_len); justifications_map.try_insert(auth_index, justifications).unwrap(); } Justifications::::insert(curve, justifications_map); }); run_to_next_round(&mut ext, curve); let state = ext.execute_with(|| QualificationDkgState::::get(curve)); let indexes = state.get_indexes(); assert!(state.is_dkg_vacant()); assert!(indexes.is_empty()); assert_eq!(indexes.active_bits_count(), authorities_len as u32); } #[test] fn test_prepare_round5_kick_on_empty_justifications() { let (mut ext, tx_pool_state) = new_test_ext(); let curve = NetworkCurve::Secp256k1; let raw_authorities = FixedAuthorities::get(); let authorities: Vec = raw_authorities.iter().map(|&id| id.into()).collect(); let authorities_len = authorities.len(); ext.execute_with(|| Exodus::start_dkg_qualification(authorities, NetworkCurve::iter())); let dkg_rounds = [ execute_round0_happy_path, execute_round1_happy_path, execute_round2_happy_path, execute_round3_happy_path, execute_round4_happy_path, ]; for dkg_round in dkg_rounds { run_to_next_round(&mut ext, curve); for authority in raw_authorities.iter() { assert_ok!(dkg_round(&mut ext, &tx_pool_state, curve, *authority)); } } ext.execute_with(|| { let mut complaints_map = BitmapByAuthority::::default(); let mut justifications_map = BitmapByAuthority::::default(); let auth: Vec = raw_authorities.iter().map(|&id| id as AuthIndex).collect(); let mut complaints_from_1 = ParticipantsBitmap::::empty(authorities_len); complaints_from_1.insert(auth[0]); complaints_from_1.insert(auth[3]); complaints_map.try_insert(auth[1], complaints_from_1).unwrap(); let mut complaints_from_2 = ParticipantsBitmap::::empty(authorities_len); complaints_from_2.insert(auth[0]); complaints_map.try_insert(auth[2], complaints_from_2).unwrap(); complaints_map.try_insert(auth[0], ParticipantsBitmap::::empty(authorities_len)).unwrap(); complaints_map.try_insert(auth[3], ParticipantsBitmap::::empty(authorities_len)).unwrap(); let mut justifications_from_0 = ParticipantsBitmap::::empty(authorities_len); justifications_from_0.insert(auth[1]); justifications_from_0.insert(auth[2]); justifications_map.try_insert(auth[0], justifications_from_0).unwrap(); let mut justifications_from_3 = ParticipantsBitmap::::empty(authorities_len); justifications_from_3.insert(auth[0]); justifications_map.try_insert(auth[3], justifications_from_3).unwrap(); justifications_map.try_insert(auth[1], ParticipantsBitmap::::empty(authorities_len)).unwrap(); justifications_map.try_insert(auth[2], ParticipantsBitmap::::empty(authorities_len)).unwrap(); Complaints::::insert(curve, complaints_map); Justifications::::insert(curve, justifications_map); }); run_to_next_round(&mut ext, curve); let state = ext.execute_with(|| QualificationDkgState::::get(curve)); let indexes = state.get_indexes(); for authority in raw_authorities.iter() { let auth_index = *authority as AuthIndex; if auth_index == 3 { assert!(!indexes.contains(auth_index)); continue; } assert!(indexes.contains(auth_index)); } assert_eq!(state.get_phase(), DkgPhase::Round5); assert_eq!(indexes.active_bits_count(), authorities_len as u32); } #[test] fn test_merkle_tree_success_power_of_two_for_shares() { let network_curve = NetworkCurve::Secp256k1; let indices = (1..=32).collect::>(); let shares = generate_mock_secp256k1_shares(&indices); let tree = NetworkCurve::build_merkle_tree::( &shares, |share| share.serialize().map_err(|_| ExodusError::SerializationError), ).unwrap(); let root = *tree.last().unwrap(); for (identifier, _) in shares.iter() { let auth_index = NetworkCurve::convert_identifier_to_index( &identifier.serialize(), ).unwrap(); let (shares_bytes, proof) = NetworkCurve::generate_merkle_proof_from_tree::( &shares, &tree, auth_index, |share| share.serialize().map_err(|_| ExodusError::SerializationError) ).unwrap(); let result = network_curve.verify_merkle_proof( &shares_bytes, &proof, root, auth_index, ); assert!(result.is_ok()); } } #[test] fn test_merkle_tree_success_power_of_two_for_scalar() { let network_curve = NetworkCurve::Secp256k1; let indices = (1..=32).collect::>(); let scalars = generate_mock_secp256k1_scalars(&indices); let tree = NetworkCurve::build_merkle_tree::( &scalars, |scalar| { let serialized = ::serialize(scalar); Ok(serialized.as_ref().to_vec()) } ).unwrap(); let root = *tree.last().unwrap(); for (identifier, _) in scalars.iter() { let auth_index = NetworkCurve::convert_identifier_to_index( &identifier.serialize(), ).unwrap(); let (shares_bytes, proof) = NetworkCurve::generate_merkle_proof_from_tree::( &scalars, &tree, auth_index, |scalar| { let serialized = ::serialize(scalar); Ok(serialized.as_ref().to_vec()) } ).unwrap(); let result = network_curve.verify_merkle_proof( &shares_bytes, &proof, root, auth_index, ); assert!(result.is_ok()); } } #[test] fn test_random_sparse_combinations_stress_for_shares() { let network_curve = NetworkCurve::Secp256k1; let mut rng = ChaCha20Rng::from_entropy(); let mut unique_indices = BTreeSet::new(); while unique_indices.len() < 15 { unique_indices.insert(rng.gen_range(1..150) as AuthIndex); } let indices: Vec = unique_indices.into_iter().collect(); let shares = generate_mock_secp256k1_shares(&indices); let tree = NetworkCurve::build_merkle_tree::( &shares, |share| share.serialize().map_err(|_| ExodusError::SerializationError), ).unwrap(); let root = *tree.last().unwrap(); for &auth_index in &indices { let (shares_bytes, proof) = NetworkCurve::generate_merkle_proof_from_tree::( &shares, &tree, auth_index, |share| share.serialize().map_err(|_| ExodusError::SerializationError), ).unwrap(); let result = network_curve.verify_merkle_proof( &shares_bytes, &proof, root, auth_index, ); assert!(result.is_ok()); } } #[test] fn test_random_sparse_combinations_stress_for_scalars() { let network_curve = NetworkCurve::Secp256k1; let mut rng = ChaCha20Rng::from_entropy(); let mut unique_indices = BTreeSet::new(); while unique_indices.len() < 15 { unique_indices.insert(rng.gen_range(1..150) as AuthIndex); } let indices: Vec = unique_indices.into_iter().collect(); let scalars = generate_mock_secp256k1_scalars(&indices); let tree = NetworkCurve::build_merkle_tree::( &scalars, |scalar| { let serialized = ::serialize(scalar); Ok(serialized.as_ref().to_vec()) } ).unwrap(); let root = *tree.last().unwrap(); for &auth_index in &indices { let (scalars_bytes, proof) = NetworkCurve::generate_merkle_proof_from_tree::( &scalars, &tree, auth_index, |scalar| { let serialized = ::serialize(scalar); Ok(serialized.as_ref().to_vec()) } ).unwrap(); let result = network_curve.verify_merkle_proof( &scalars_bytes, &proof, root, auth_index, ); assert!(result.is_ok()); } } #[test] fn test_participants_with_high_index_gap_for_shares() { let network_curve = NetworkCurve::Secp256k1; let indices = vec![1, 1023]; let shares = generate_mock_secp256k1_shares(&indices); let tree = NetworkCurve::build_merkle_tree::( &shares, |share| share.serialize().map_err(|_| ExodusError::SerializationError), ).unwrap(); let root = *tree.last().unwrap(); let (shares_bytes, proof) = NetworkCurve::generate_merkle_proof_from_tree::( &shares, &tree, 1023, |share| share.serialize().map_err(|_| ExodusError::SerializationError), ).unwrap(); assert_eq!(proof.len(), 10); let result = network_curve.verify_merkle_proof( &shares_bytes, &proof, root, 1023, ); assert!(result.is_ok()); } #[test] fn test_participants_with_high_index_gap_for_scalars() { let network_curve = NetworkCurve::Secp256k1; let indices = vec![1, 1023]; let scalars = generate_mock_secp256k1_scalars(&indices); let tree = NetworkCurve::build_merkle_tree::( &scalars, |scalar| { let serialized = ::serialize(scalar); Ok(serialized.as_ref().to_vec()) } ).unwrap(); let root = *tree.last().unwrap(); let (scalars_bytes, proof) = NetworkCurve::generate_merkle_proof_from_tree::( &scalars, &tree, 1023, |scalar| { let serialized = ::serialize(scalar); Ok(serialized.as_ref().to_vec()) } ).unwrap(); assert_eq!(proof.len(), 10); let result = network_curve.verify_merkle_proof( &scalars_bytes, &proof, root, 1023, ); assert!(result.is_ok()); } #[test] fn test_regression_consecutive_vs_position_for_shares() { let network_curve = NetworkCurve::Secp256k1; let custom_indices = vec![2, 3]; let shares = generate_mock_secp256k1_shares(&custom_indices); let tree = NetworkCurve::build_merkle_tree::( &shares, |scalar| scalar.serialize().map_err(|_| ExodusError::SerializationError), ).unwrap(); let root = *tree.last().unwrap(); let (shares_bytes, proof) = NetworkCurve::generate_merkle_proof_from_tree::( &shares, &tree, 2, |scalar| scalar.serialize().map_err(|_| ExodusError::SerializationError), ).unwrap(); let result = network_curve.verify_merkle_proof(&shares_bytes, &proof, root, 2); assert!(result.is_ok()); } #[test] fn test_regression_consecutive_vs_position_for_scalars() { let network_curve = NetworkCurve::Secp256k1; let custom_indices = vec![2, 3]; let scalars = generate_mock_secp256k1_scalars(&custom_indices); let tree = NetworkCurve::build_merkle_tree::( &scalars, |scalar| { let serialized = ::serialize(scalar); Ok(serialized.as_ref().to_vec()) } ).unwrap(); let root = *tree.last().unwrap(); let (scalars_bytes, proof) = NetworkCurve::generate_merkle_proof_from_tree::( &scalars, &tree, 2, |scalar| { let serialized = ::serialize(scalar); Ok(serialized.as_ref().to_vec()) } ).unwrap(); let result = network_curve.verify_merkle_proof(&scalars_bytes, &proof, root, 2); assert!(result.is_ok()); } #[test] fn test_attack_with_unknown_authority_index_for_shares() { let network_curve = NetworkCurve::Secp256k1; let custom_indices = vec![1, 2, 3]; let shares = generate_mock_secp256k1_shares(&custom_indices); let tree = NetworkCurve::build_merkle_tree::( &shares, |scalar| scalar.serialize().map_err(|_| ExodusError::SerializationError), ).unwrap(); let root = *tree.last().unwrap(); let (shares_bytes, proof) = NetworkCurve::generate_merkle_proof_from_tree::( &shares, &tree, 2, |scalar| scalar.serialize().map_err(|_| ExodusError::SerializationError), ).unwrap(); let result = network_curve.verify_merkle_proof(&shares_bytes, &proof, root, 4); assert!(result.is_err()); } #[test] fn test_attack_with_unknown_authority_index_for_scalars() { let network_curve = NetworkCurve::Secp256k1; let custom_indices = vec![1, 2, 3]; let scalars = generate_mock_secp256k1_scalars(&custom_indices); let tree = NetworkCurve::build_merkle_tree::( &scalars, |scalar| { let serialized = ::serialize(scalar); Ok(serialized.as_ref().to_vec()) } ).unwrap(); let root = *tree.last().unwrap(); let (scalars_bytes, proof) = NetworkCurve::generate_merkle_proof_from_tree::( &scalars, &tree, 2, |scalar| { let serialized = ::serialize(scalar); Ok(serialized.as_ref().to_vec()) } ).unwrap(); let result = network_curve.verify_merkle_proof(&scalars_bytes, &proof, root, 4); assert!(result.is_err()); } #[test] fn curve_wrapper_for_dkg_works() { let mut rng = ChaCha20Rng::from_seed(Default::default()); let curve = NetworkCurve::Secp256k1; let dkg_index = 69 as DkgIndex; // NOTE: iterate over all NetworkCurve enum entries. // For now it is fine, because we are using only one curve let mut lost_participants = BTreeSet::new(); let mut round1_secret_packages = BTreeMap::new(); let mut received_round1_packages = BTreeMap::new(); let mut round2_secret_packages = BTreeMap::new(); let mut round2_encrypted_ciphers = BTreeMap::new(); let mut round2_decrypted_packages = BTreeMap::new(); let mut secret_key_packages = BTreeMap::new(); let mut public_verifying_shares = BTreeMap::new(); let mut public_verifying_keys = BTreeMap::new(); // throttle first participant lost_participants.insert(0u16); for index in 0..MAX_SIGNERS { if lost_participants.contains(&index) { continue; } let (round1_secret_package_bytes, round1_package_bytes) = curve.dkg_part1(index, MAX_SIGNERS, MIN_SIGNERS, &mut rng).unwrap(); let number_of_coefficients = curve.dkg_verify_proof_of_knowledge(index, &round1_package_bytes).unwrap(); assert!(number_of_coefficients == MIN_SIGNERS); round1_secret_packages.insert(index, round1_secret_package_bytes); received_round1_packages.insert(index, round1_package_bytes); } assert!(round1_secret_packages.len() == (MAX_SIGNERS - 1) as usize); assert!(received_round1_packages.len() == (MAX_SIGNERS - 1) as usize); // throttle last participant lost_participants.insert(MAX_SIGNERS - 1); for sender_index in 0..MAX_SIGNERS { if lost_participants.contains(&sender_index) { continue; } let sender_round1_package = received_round1_packages .remove(&sender_index) .unwrap(); round1_secret_packages.get_mut(&sender_index) .map(|round1_secret_package| { curve.dkg_narrow_round_max_signers( round1_secret_package, received_round1_packages.len() as AuthIndex + 1, 1u8, // narrow max signers for round1 ) .map(|new_secret_package| { *round1_secret_package = new_secret_package; }) .ok() }) .flatten() .unwrap(); let round1_secret_package_bytes = round1_secret_packages .get(&sender_index) .unwrap(); let (round2_secret_package_bytes, round2_packages_bytes) = curve.dkg_part2(&round1_secret_package_bytes, &received_round1_packages) .unwrap(); assert_eq!( round2_packages_bytes.len(), (MAX_SIGNERS as usize) - lost_participants.len(), ); round2_secret_packages.insert(sender_index, round2_secret_package_bytes); let insertion_outcome = received_round1_packages .insert(sender_index, sender_round1_package); assert!(insertion_outcome.is_none()); for (receiver_index, round2_package_bytes) in round2_packages_bytes { let additional_info = curve.ecdh_prepare_additional_info( sender_index, receiver_index, dkg_index, sp_std::marker::PhantomData::>, ); let round1_receiver_package_bytes = received_round1_packages .get(&receiver_index) .unwrap(); let encryption_cipher = curve.ecdh_get_cipher_from_keys( &round1_receiver_package_bytes, &round1_secret_package_bytes, &additional_info, sp_std::marker::PhantomData::>, ).unwrap(); let encryption_data: EncryptionData = curve .ecdh_encrypt_package( &encryption_cipher, &additional_info, &round2_package_bytes, &mut rng, ).unwrap(); round2_encrypted_ciphers .entry(receiver_index) .or_insert_with(BTreeMap::new) .insert(sender_index, encryption_data); } } // throttle someone in the middle let malicious_index = MAX_SIGNERS / 2; lost_participants.insert(malicious_index); for receiver_index in 0..MAX_SIGNERS { if lost_participants.contains(&receiver_index) { continue; } let round1_secret_package_bytes = round1_secret_packages .get(&receiver_index) .unwrap(); let my_encrypted_messages = round2_encrypted_ciphers .get(&receiver_index) .unwrap(); assert_eq!( my_encrypted_messages.len(), (MAX_SIGNERS as usize) - lost_participants.len(), ); for (&sender_index, encrypted_message) in my_encrypted_messages.iter() { if lost_participants.contains(&sender_index) { continue; } let sender_round1_package_bytes = received_round1_packages .get(&sender_index) .unwrap(); let additional_info = curve.ecdh_prepare_additional_info( sender_index, receiver_index, dkg_index, sp_std::marker::PhantomData::>, ); let encryption_cipher = curve.ecdh_get_cipher_from_keys( &sender_round1_package_bytes, &round1_secret_package_bytes, &additional_info, sp_std::marker::PhantomData::>, ).unwrap(); let decrypted_private_package = curve.ecdh_decrypt_package( &encryption_cipher, &encrypted_message, &additional_info, ).unwrap(); curve.dkg_verify_private_package( receiver_index, &sender_round1_package_bytes, &decrypted_private_package, ).unwrap(); round2_decrypted_packages .entry(receiver_index) .or_insert_with(BTreeMap::new) .insert(sender_index, decrypted_private_package); } let decrypted_packages_len = round2_decrypted_packages .get(&receiver_index) .map(|inner_map| inner_map.len()) .unwrap_or_default(); assert_eq!( decrypted_packages_len, (MAX_SIGNERS as usize) - 1 - lost_participants.len(), ); } assert_eq!( round2_decrypted_packages.len(), (MAX_SIGNERS as usize) - lost_participants.len(), ); for index in 0..MAX_SIGNERS { if lost_participants.contains(&index) { continue; } let round2_packages_bytes = round2_decrypted_packages .get(&index) .unwrap(); round2_secret_packages.get_mut(&index) .map(|round2_secret_package| { curve.dkg_narrow_round_max_signers( round2_secret_package, MAX_SIGNERS - lost_participants.len() as AuthIndex, 2u8, // narrow max signers for round2 ) .map(|new_secret_package| { *round2_secret_package = new_secret_package; }) .ok() }) .flatten() .unwrap(); let round2_secret_package_bytes = round2_secret_packages .get(&index) .unwrap(); let round1_packages = received_round1_packages .iter() .filter(|(&i, _)| i != index && !lost_participants.contains(&i)) .map(|(i, package)| (*i, package.clone())) .collect::>>(); let (secret_key_package, verifying_key, merkle_proof, merkle_root) = curve.dkg_part3( index, round2_secret_package_bytes, &round1_packages, round2_packages_bytes, ).unwrap(); let verifying_share = curve.dkg_derive_verifying_share(&secret_key_package).unwrap(); curve.verify_merkle_proof(&verifying_share, &merkle_proof, merkle_root, index).unwrap(); secret_key_packages.insert(index, secret_key_package); public_verifying_shares.insert(index, verifying_share); public_verifying_keys.insert(index, verifying_key); } let group_public_key = public_verifying_keys.values().next().unwrap(); let all_equal = public_verifying_keys.values().all(|val| val == group_public_key); assert!(all_equal, "Consensus on group pubkey not reached"); assert!(public_verifying_shares.keys().count() == (MAX_SIGNERS as usize) - lost_participants.len()); let mut nonces_map = BTreeMap::new(); let mut commitments_map = BTreeMap::new(); let mut signing_packages = BTreeMap::new(); let mut group_commitments = BTreeMap::new(); let mut self_binding_factors = BTreeMap::new(); let mut binding_factors_proofs = BTreeMap::new(); let mut binding_factor_roots = BTreeMap::new(); let mut session_commitments = BTreeMap::new(); let message = "message to sign".as_bytes(); let mut number_of_valid_signatures = 0; let mut session_id = 0; let mut member_id = 0; let max_possible_sessions = MAX_SIGNERS .saturating_sub(MIN_SIGNERS) .saturating_add(1) .saturating_sub(lost_participants.len() as AuthIndex); loop { if member_id >= MAX_SIGNERS { member_id = 0; } if lost_participants.contains(&member_id) { member_id += 1; continue; } let session_members = nonces_map.get(&session_id) .map(|members: &BTreeMap>| members.len()) .unwrap_or_default(); if session_members >= MIN_SIGNERS as usize { session_id += 1; } if session_id == max_possible_sessions { break; } let secret_share = secret_key_packages.get(&member_id).unwrap(); let (nonce, hiding, binding) = curve .generate_nonce_commitment(&secret_share, &mut rng) .unwrap(); nonces_map .entry(session_id) .or_insert_with(BTreeMap::new) .insert(member_id, nonce); commitments_map .entry(session_id) .or_insert_with(BTreeMap::new) .insert(member_id, (hiding, binding)); member_id += 1; } commitments_map.iter().for_each(|(&session_id, raw_commitments)| { let commitments = raw_commitments .iter() .map(|(&auth_index, (hiding, binding))| { (auth_index, (hiding.as_ref(), binding.as_ref())) }) .collect::>(); raw_commitments.keys().for_each(|&auth_index| { let (group_commitment, self_factor, factors_proof, factor_root) = curve .generate_group_commitment( auth_index, &commitments, &group_public_key, &message, ) .unwrap(); group_commitments .entry(session_id) .or_insert_with(BTreeMap::new) .insert(auth_index, group_commitment); self_binding_factors .entry(session_id) .or_insert_with(BTreeMap::new) .insert(auth_index, self_factor); binding_factors_proofs .entry(session_id) .or_insert_with(BTreeMap::new) .insert(auth_index, factors_proof); binding_factor_roots .entry(session_id) .or_insert_with(BTreeMap::new) .insert(auth_index, factor_root); }); let signing_package = curve .init_signing_package(&commitments, &message) .unwrap(); signing_packages.insert(session_id, signing_package); session_commitments.insert(session_id, raw_commitments); }); group_commitments.iter().for_each(|(_, commitments)| { let first_commitment = commitments.values().next().unwrap(); let all_equal = commitments.values().all(|val| val == first_commitment); assert!(all_equal, "Group commitments inside ROAST session differs."); }); for session_id in commitments_map.keys() { let signing_package = match signing_packages.get(session_id) { Some(signing_package) => signing_package, None => continue, }; let nonces = match nonces_map.get(&session_id) { Some(nonces) => nonces, None => continue, }; let current_raw_commitments = session_commitments.get(session_id).unwrap(); let participants_list: Vec = current_raw_commitments.keys().cloned().collect(); let mut accumulated_signature_scalar = vec![0u8; 32]; for (sender_index, signer_nonces) in nonces.iter() { let secret_share = secret_key_packages.get(&sender_index).unwrap(); let verifying_share = public_verifying_shares.get(sender_index).unwrap(); let group_commitment = group_commitments.get(session_id).unwrap().get(sender_index).unwrap(); let self_factor = self_binding_factors.get(session_id).unwrap().get(sender_index).unwrap(); let factors_proof = binding_factors_proofs.get(session_id).unwrap().get(sender_index).unwrap(); let factor_root = binding_factor_roots.get(session_id).unwrap().get(sender_index).unwrap(); curve.verify_merkle_proof( self_factor.as_ref(), factors_proof.as_ref(), *factor_root, *sender_index, ).unwrap(); let signature_share = curve.partial_sign_message( signing_package, signer_nonces, secret_share, ).unwrap(); let (raw_hiding, raw_binding) = session_commitments .get(session_id) .unwrap() .get(sender_index) .unwrap(); curve.verify_signature_share( *sender_index, participants_list.iter().cloned(), signature_share.as_ref(), self_factor.as_ref(), raw_hiding.as_ref(), raw_binding.as_ref(), group_commitment.as_ref(), verifying_share, &group_public_key, &message, ).unwrap(); accumulated_signature_scalar = curve.accumulate_signature_scalar( &accumulated_signature_scalar, signature_share.as_ref(), ).unwrap(); } let any_sender = nonces.keys().next().unwrap(); let current_session_group_commitment = group_commitments .get(session_id) .unwrap() .get(any_sender) .unwrap(); if curve .is_signature_valid( &group_public_key, current_session_group_commitment.as_ref(), &accumulated_signature_scalar, &message, ) .is_ok() { number_of_valid_signatures += 1; } } assert_eq!(number_of_valid_signatures, commitments_map.len()); } #[test] fn test_common_way_of_frost_usage() { use frost_secp256k1 as frost; let mut rng = ChaCha20Rng::from_seed(Default::default()); // Key generation, Round 1 // Keep track of each participant's round 1 secret package. In // practice each participant will keep its copy; no one will have // all the participant's packages. let mut round1_secret_packages = BTreeMap::new(); // Keep track of all round 1 packages sent to the given // participant. This is used to simulate the broadcast; in // practice the packages will be sent through some communication // channel. let mut received_round1_packages = BTreeMap::new(); // For each participant, perform the first part of the DKG // protocol. In practice, each participant will perform this on // their own environments. for participant_index in 1..=MAX_SIGNERS { let participant_identifier = participant_index.try_into().expect("should be nonzero"); let (round1_secret_package, round1_package) = frost::keys::dkg::part1( participant_identifier, MAX_SIGNERS, MIN_SIGNERS, &mut rng, ).unwrap(); // store the participants secret Store the participant's // secret package for later use. In practice each participant // will store it in their own environment. round1_secret_packages.insert( participant_identifier, round1_secret_package, ); // "Send" the round 1 package to all other participants. In // this test this is simulated using a BTreeMap; in practice // this will be sent through some communication channel. for receiver_participant_index in 1..=MAX_SIGNERS { if receiver_participant_index == participant_index { continue; } let receiver_participant_identifier: frost::Identifier = receiver_participant_index .try_into() .expect("should be nonzero"); received_round1_packages .entry(receiver_participant_identifier) .or_insert_with(BTreeMap::new) .insert(participant_identifier, round1_package.clone()); } } // Ket generation, Round 2 // Keep track of each participant's round 2 secret package. In // practice each participant will keep its copy; no one will have // all the participant's packages. let mut round2_secret_packages = BTreeMap::new(); // Keep track of all round 2 packages sent to the given // participant. This is used to simulate the broadcast; in // practice the packages will be sent through some communication // channel. let mut received_round2_packages = BTreeMap::new(); // For each participant, perform the second part of the DKG // protocol. In practice, each participant will perform this on // their own environments. for participant_index in 1..=MAX_SIGNERS { let participant_identifier = participant_index.try_into().expect("should be nonzero"); let round1_secret_package = round1_secret_packages .remove(&participant_identifier) .unwrap(); let round1_package = &received_round1_packages[&participant_identifier]; let (round2_secret_package, round2_packages) = frost::keys::dkg::part2(round1_secret_package, round1_package).unwrap(); // Store the participant's secret package for later use. In // practice each participant will store in their own // environment. round2_secret_packages.insert( participant_identifier, round2_secret_package, ); // "Send" the round 2 package to all other participants. In // this test this is simulated using a BTreeMap; in practice // this will be sent through some communication channel. Note // that, in contrast to the previous part, here each other // participant gets its own specific package. for (receiver_identifier, round2_package) in round2_packages { received_round2_packages .entry(receiver_identifier) .or_insert_with(BTreeMap::new) .insert(participant_identifier, round2_package); } } // Key generation, final computation // Keep track of each participant's long-lived key package. In // practice each participant will keep it's own copy; no one will // have all the participant's packages. let mut key_packages = BTreeMap::new(); // Keep track of each participant's public key package. In // practice, if there is a Coordinator, only they need to store // the set. If there is not, then all candidates must store their // own sets. All participants will have the same exact public key // package. let mut pubkey_packages = BTreeMap::new(); // For each participant, perform the third part of the DKG // protocol. In practice, each participant will perform this on // their own environments. for participant_index in 1..=MAX_SIGNERS { let participant_identifier = participant_index.try_into().expect("should be nonzero"); let round2_secret_package = &round2_secret_packages[&participant_identifier]; let round1_packages = &received_round1_packages[&participant_identifier]; let round2_packages = &received_round2_packages[&participant_identifier]; let (key_package, pubkey_package) = frost::keys::dkg::part3( round2_secret_package, round1_packages, round2_packages, ).unwrap(); key_packages.insert(participant_identifier, key_package); pubkey_packages.insert(participant_identifier, pubkey_package); } let pubkey_package = pubkey_packages.values().next().expect("should be one"); let mut nonces_map = BTreeMap::new(); let mut commitments_map = BTreeMap::new(); // Round 1, generate nonces and signing commitments // In practice, each iteration of this loop will be executed by its respective participant. for participant_index in 1..=MIN_SIGNERS { let participant_identifier = participant_index.try_into().expect("should be nonzero"); let key_package = &key_packages[&participant_identifier]; // Generate one (1) nonce and one SigningCommitments instance // for each participant, up to _threshold_. let (nonces, commitments) = frost::round1::commit( key_package.signing_share(), &mut rng, ); // In practice, the nonces must be kept by the participant to // use in the next round, while the commitment must be sent to // the coordinator (or to every other participant if there is // no coordinator) using an authenticated channel. nonces_map.insert(participant_identifier, nonces); commitments_map.insert(participant_identifier, commitments); } // This is what the signature aggregator / coordinator needs to do: // - decide what message to sign // - take one (unused) commitment per signing participant let mut signature_shares = BTreeMap::new(); let message = "message to sign".as_bytes(); let signing_package = frost::SigningPackage::new(commitments_map, message); // Round 2, each participant generates their signature shares // In practice, each iteration of this loop will be executed by // its respective participant. for participant_identifier in nonces_map.keys() { let key_package = &key_packages[participant_identifier]; let nonces = &nonces_map[participant_identifier]; // Each participant generates their signature share. let signature_share = frost::round2::sign( &signing_package, nonces, key_package, ).unwrap(); // In practice, the signature share must be sent to the // Coordinator using an authenticated channel. signature_shares.insert(*participant_identifier, signature_share); } // Aggregation: collects signing shares from all participants // Aggregate (also verifies the signature shares) let group_signature = frost::aggregate( &signing_package, &signature_shares, &pubkey_package, ).unwrap(); // Check that the threshold signature can be verified by the group // public key (the verification key). let is_signature_valid = pubkey_package .verifying_key() .verify(message, &group_signature) .is_ok(); assert!(is_signature_valid); }