2379 lines
84 KiB
Rust
2379 lines
84 KiB
Rust
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<Runtime, Call, Signature>(raw_tx: &[u8]) -> Option<Call>
|
|
where
|
|
Runtime: SendTransactionTypes<Call>,
|
|
<Runtime as SendTransactionTypes<Call>>::Extrinsic: Decode,
|
|
<Runtime as SendTransactionTypes<Call>>::Extrinsic: Into<Extrinsic<Call>>,
|
|
{
|
|
let extrinsic = <Runtime as SendTransactionTypes<Call>>::Extrinsic::decode(&mut &raw_tx[..]).ok()?;
|
|
let test_extrinsic: Extrinsic<Call> = 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::<Runtime>::get(curve).get_block();
|
|
let delay = <<Runtime as Config>::DkgRoundPeriod as frame_support::traits::Get<
|
|
frame_system::pallet_prelude::BlockNumberFor<Runtime>
|
|
>>::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<Secp256K1Identifier, Secp256K1VerifyingShare> {
|
|
let mut shares = BTreeMap::new();
|
|
let mut rng = ChaCha20Rng::from_seed(Default::default());
|
|
|
|
for &i in indices {
|
|
let random_scalar = <Secp256K1ScalarField as Field>::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<Secp256K1Identifier, <Secp256K1ScalarField as Field>::Scalar> {
|
|
let mut scalars = BTreeMap::new();
|
|
let mut rng = ChaCha20Rng::from_seed(Default::default());
|
|
|
|
for &i in indices {
|
|
let random_scalar = <Secp256K1ScalarField as Field>::random(&mut rng);
|
|
let identifier = Secp256K1Identifier::try_from(i + 1).unwrap();
|
|
scalars.insert(identifier, random_scalar);
|
|
}
|
|
|
|
scalars
|
|
}
|
|
|
|
fn execute_dkg_happy_path<F1, F2, F3>(
|
|
ext: &mut sp_io::TestExternalities,
|
|
tx_pool_state: &std::sync::Arc<parking_lot::RwLock<sp_core::offchain::testing::PoolState>>,
|
|
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<BlockNumberFor<Runtime>, NetworkCurve>,
|
|
&QualifyingState<Runtime>,
|
|
UintAuthorityId,
|
|
) -> Result<ExodusOk<NetworkCurve>, ExodusError>,
|
|
|
|
F2: FnMut(Call<Runtime>) -> Result<(), TestError>,
|
|
F3: FnMut(AuthIndex, NetworkCurve) -> Result<(), TestError>,
|
|
{
|
|
use sp_runtime::transaction_validity::TransactionSource;
|
|
|
|
let state = ext.execute_with(|| QualificationDkgState::<Runtime>::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(|| {
|
|
< <Runtime as Config>::UnsignedLongevity as frame_support::traits::Get<u64> >::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::<Runtime, RuntimeCall, ()>(&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<F1, F2>(
|
|
ext: &mut sp_io::TestExternalities,
|
|
tx_pool_state: &std::sync::Arc<parking_lot::RwLock<sp_core::offchain::testing::PoolState>>,
|
|
curve: NetworkCurve,
|
|
authority: u64,
|
|
mut apply_transaction: F1,
|
|
mut check_storage_changed: F2,
|
|
) -> Result<(), TestError>
|
|
where
|
|
F1: FnMut(Call<Runtime>) -> Result<(), TestError>,
|
|
F2: FnMut(ExodusOk<NetworkCurve>) -> Result<(), TestError>,
|
|
{
|
|
use sp_runtime::transaction_validity::TransactionSource;
|
|
|
|
let state = ext.execute_with(|| QualificationDkgState::<Runtime>::get(&curve));
|
|
if state.is_dkg_pending() {
|
|
return Err(TestError::PhaseMismatch);
|
|
}
|
|
|
|
let active_authorities = ext.execute_with(|| {
|
|
ActiveDkgAuthorities::<Runtime>::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::<Runtime, RuntimeCall, ()>(&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<parking_lot::RwLock<sp_core::offchain::testing::PoolState>>,
|
|
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::<Runtime>::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<parking_lot::RwLock<sp_core::offchain::testing::PoolState>>,
|
|
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::<Runtime>::decode_len(&curve).unwrap();
|
|
let round1_packages = Round1Packages::<Runtime>::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<parking_lot::RwLock<sp_core::offchain::testing::PoolState>>,
|
|
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::<Runtime>::decode_len(&curve).unwrap();
|
|
let round2_packages = Round2Packages::<Runtime>::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<parking_lot::RwLock<sp_core::offchain::testing::PoolState>>,
|
|
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::<Runtime>::decode_len(&curve).unwrap();
|
|
let complaints = Complaints::<Runtime>::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<parking_lot::RwLock<sp_core::offchain::testing::PoolState>>,
|
|
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::<Runtime>::get(curve).get_dkg_index();
|
|
let participants = QualificationAuthorities::<Runtime>::decode_len(&curve).unwrap();
|
|
let justifications = Justifications::<Runtime>::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::<Runtime>::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<parking_lot::RwLock<sp_core::offchain::testing::PoolState>>,
|
|
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::<Runtime>::get(curve).get_dkg_index();
|
|
|
|
let non_empty_verification_states = (0..curr_dkg_index).any(|dkg_index| {
|
|
VerifyingKeyConsensus::<Runtime>::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<parking_lot::RwLock<sp_core::offchain::testing::PoolState>>,
|
|
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::<Runtime>::get(curve).get_dkg_index();
|
|
let verifying_share_key = (curve, curr_dkg_index, idx);
|
|
|
|
if VerifyingShares::<Runtime>::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<parking_lot::RwLock<sp_core::offchain::testing::PoolState>>,
|
|
raw_authorities: &[u64],
|
|
curve: NetworkCurve,
|
|
) -> (u64, u64) {
|
|
let authorities: Vec<UintAuthorityId> = raw_authorities.iter().map(|&id| id.into()).collect();
|
|
let prev_dkg_index = ext.execute_with(|| QualificationDkgState::<Runtime>::get(curve).get_dkg_index());
|
|
ext.execute_with(|| Exodus::start_dkg_qualification(authorities, NetworkCurve::iter()));
|
|
|
|
let dkg_index = ext.execute_with(|| QualificationDkgState::<Runtime>::get(curve).get_dkg_index());
|
|
let (start_block, round_delay) = ext.execute_with(|| {
|
|
let round_delay = <<Runtime as Config>::DkgRoundPeriod as frame_support::traits::Get<
|
|
frame_system::pallet_prelude::BlockNumberFor<Runtime>
|
|
>>::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::<Runtime>::all_ones(1337);
|
|
let mut map: BitmapByAuthority<Runtime> = Default::default();
|
|
|
|
for authority in raw_authorities.iter() {
|
|
let auth_idx = *authority as AuthIndex;
|
|
Round0Packages::<Runtime>::insert(curve, auth_idx, DUMMY_HASH);
|
|
map.try_insert(auth_idx, dummy_oversized_bitmap.clone()).unwrap();
|
|
}
|
|
|
|
Round1Packages::<Runtime>::insert(curve, dummy_oversized_bitmap.clone());
|
|
Round2Packages::<Runtime>::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::<Runtime>::insert(prev_verifications_key1, dummy_oversized_bitmap.clone());
|
|
Verifications::<Runtime>::insert(prev_verifications_key2, dummy_oversized_bitmap.clone());
|
|
|
|
let dummy_verifying_key = BoundedVec::<u8, ConstU32<ELEMENT_MAX_BYTES>>::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::<Runtime>::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<parking_lot::RwLock<sp_core::offchain::testing::PoolState>>,
|
|
) {
|
|
let (mut ext, tx_pool_state) = new_test_ext();
|
|
|
|
let prev_verifying_key = ext.execute_with(|| ActiveVerifyingKey::<Runtime>::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::<Runtime>::get(curve));
|
|
assert!(!curr_verifying_key.is_empty());
|
|
|
|
let state = ext.execute_with(|| QualificationDkgState::<Runtime>::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::<usize>(), authorities_len);
|
|
assert_eq!(state.get_indexes().active_bits_count(), authorities_len as u32);
|
|
|
|
let active = ext.execute_with(|| ActiveDkgAuthorities::<Runtime>::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::<Runtime>::get(curve));
|
|
let qualification_authorities = ext.execute_with(|| QualificationAuthorities::<Runtime>::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<parking_lot::RwLock<sp_core::offchain::testing::PoolState>>,
|
|
prev_authorities: Vec<u64>,
|
|
raw_authorities: Vec<u64>,
|
|
curve: NetworkCurve,
|
|
) {
|
|
let prev_active_authorities = ext.execute_with(|| ActiveAuthorities::<Runtime>::get(curve));
|
|
let prev_verifying_key = ext.execute_with(|| ActiveVerifyingKey::<Runtime>::get(curve));
|
|
assert!(!prev_verifying_key.is_empty());
|
|
|
|
let dkg_index = ext.execute_with(|| {
|
|
QualificationDkgState::<Runtime>::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::<Runtime>::contains_key((exodus, roast), idx) {
|
|
return Err(TestError::InvalidStorageAfter);
|
|
}
|
|
|
|
if !RoastSessions::<Runtime>::contains_key(exodus, idx) {
|
|
return Err(TestError::InvalidStorageAfter);
|
|
}
|
|
|
|
let roast_state = RoastSessionStates::<Runtime>::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::<Runtime>::contains_key(exodus, idx) {
|
|
return Err(TestError::InvalidStorageAfter);
|
|
}
|
|
|
|
if !GroupCommitters::<Runtime>::iter_prefix_values((exodus, roast))
|
|
.any(|participants| participants.contains(idx)) {
|
|
return Err(TestError::InvalidStorageAfter);
|
|
}
|
|
|
|
let roast_state = RoastSessionStates::<Runtime>::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::<Runtime>::contains_key(exodus, idx) {
|
|
return Err(TestError::InvalidStorageAfter);
|
|
}
|
|
|
|
if RoastSessions::<Runtime>::contains_key(exodus, idx) {
|
|
return Err(TestError::InvalidStorageAfter);
|
|
}
|
|
|
|
if ExodusRequests::<Runtime>::contains_key(curve, exodus) {
|
|
let roast_state = RoastSessionStates::<Runtime>::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::<Runtime>::contains_key(exodus, roast) {
|
|
return Err(TestError::InvalidStorageAfter);
|
|
}
|
|
|
|
if SignatureScalars::<Runtime>::contains_key(exodus, roast) {
|
|
return Err(TestError::InvalidStorageAfter);
|
|
}
|
|
|
|
if GroupCommitmentsConsensus::<Runtime>::contains_key(exodus, roast) {
|
|
return Err(TestError::InvalidStorageAfter);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
_ => Err(TestError::InvalidStorageAfter),
|
|
}
|
|
};
|
|
|
|
let threshold = ext.execute_with(|| {
|
|
let len = ActiveAuthorities::<Runtime>::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<ExodusSession> = ext.execute_with(|| {
|
|
ExodusRequests::<Runtime>::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<u64, RoastSession> = exodus_steps_outer.entry(*exodus_session).or_default();
|
|
let roast_sessions: &mut BTreeMap<u64, RoastSession> = roast_sessions_outer.entry(*exodus_session).or_default();
|
|
let roast_participants: &mut BTreeMap<RoastSession, (u16, u16, u16)> = roast_participants_outer.entry(*exodus_session).or_default();
|
|
|
|
let malicious_participants: &mut BTreeSet<u64> = malicious_participants_outer.entry(*exodus_session).or_default();
|
|
let throttled_participants: &mut BTreeSet<u64> = throttled_participants_outer.entry(*exodus_session).or_default();
|
|
|
|
for auth in prev_authorities.iter() {
|
|
let request_exists = ext.execute_with(|| {
|
|
ExodusRequests::<Runtime>::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::<u8, ConstU32<{ ELEMENT_MAX_BYTES }>>::try_from(raw_vec)
|
|
.unwrap();
|
|
|
|
let empty_participants = ParticipantsBitmap::<Runtime>::empty(prev_len);
|
|
let mut self_participant = empty_participants.clone();
|
|
self_participant.insert(malicious);
|
|
|
|
let (correct_commitment, correct_root) = GroupCommitmentsConsensus::<Runtime>::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::<Runtime>::insert(prev_group_key, empty_participants);
|
|
|
|
let fake_group_key = (exodus_session, roast_session, DUMMY_HASH, fake_group_commitment);
|
|
GroupCommitters::<Runtime>::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::<Runtime>::get(curve));
|
|
assert!(curr_verifying_key != prev_verifying_key);
|
|
|
|
let state = ext.execute_with(|| QualificationDkgState::<Runtime>::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::<usize>(), authorities_len);
|
|
assert_eq!(state.get_indexes().active_bits_count(), authorities_len as u32);
|
|
|
|
let active = ext.execute_with(|| ActiveDkgAuthorities::<Runtime>::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::<Runtime>::get(curve));
|
|
let qualification_authorities = ext.execute_with(|| QualificationAuthorities::<Runtime>::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::<Runtime>::iter().count()
|
|
});
|
|
|
|
let bridges_count = ext.execute_with(|| {
|
|
ExodusSignedBridges::<Runtime>::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<UintAuthorityId> = 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::<Runtime>::get(curve).get_dkg_index()
|
|
});
|
|
|
|
let (start_block, round_delay) = ext.execute_with(|| {
|
|
let round_delay = <<Runtime as Config>::DkgRoundPeriod as frame_support::traits::Get<
|
|
frame_system::pallet_prelude::BlockNumberFor<Runtime>
|
|
>>::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::<Runtime>::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::<Runtime>::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::<usize>() >= bft_threshold);
|
|
assert_eq!(state.get_indexes().active_bits_count(), authorities_len as u32);
|
|
|
|
let active = ext.execute_with(|| ActiveDkgAuthorities::<Runtime>::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::<Runtime>::get(curve));
|
|
let qualification_authorities = ext.execute_with(|| QualificationAuthorities::<Runtime>::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<UintAuthorityId> = 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::<Runtime>::default();
|
|
for authority in 0..=bft_threshold {
|
|
let auth_index = authority as AuthIndex;
|
|
let complaints = ParticipantsBitmap::<Runtime>::all_ones(authorities_len);
|
|
complaints_map.try_insert(auth_index, complaints).unwrap();
|
|
}
|
|
Complaints::<Runtime>::insert(curve, complaints_map);
|
|
});
|
|
|
|
run_to_next_round(&mut ext, curve);
|
|
|
|
let state = ext.execute_with(|| QualificationDkgState::<Runtime>::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<UintAuthorityId> = 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::<Runtime>::default();
|
|
for authority in 0..=bft_threshold {
|
|
let auth_index = authority as AuthIndex;
|
|
let justifications = ParticipantsBitmap::<Runtime>::all_ones(authorities_len);
|
|
justifications_map.try_insert(auth_index, justifications).unwrap();
|
|
}
|
|
Justifications::<Runtime>::insert(curve, justifications_map);
|
|
});
|
|
|
|
run_to_next_round(&mut ext, curve);
|
|
|
|
let state = ext.execute_with(|| QualificationDkgState::<Runtime>::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<UintAuthorityId> = 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::<Runtime>::default();
|
|
let mut justifications_map = BitmapByAuthority::<Runtime>::default();
|
|
|
|
let auth: Vec<AuthIndex> = raw_authorities.iter().map(|&id| id as AuthIndex).collect();
|
|
|
|
let mut complaints_from_1 = ParticipantsBitmap::<Runtime>::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::<Runtime>::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::<Runtime>::empty(authorities_len)).unwrap();
|
|
complaints_map.try_insert(auth[3], ParticipantsBitmap::<Runtime>::empty(authorities_len)).unwrap();
|
|
|
|
let mut justifications_from_0 = ParticipantsBitmap::<Runtime>::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::<Runtime>::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::<Runtime>::empty(authorities_len)).unwrap();
|
|
justifications_map.try_insert(auth[2], ParticipantsBitmap::<Runtime>::empty(authorities_len)).unwrap();
|
|
|
|
Complaints::<Runtime>::insert(curve, complaints_map);
|
|
Justifications::<Runtime>::insert(curve, justifications_map);
|
|
});
|
|
|
|
run_to_next_round(&mut ext, curve);
|
|
|
|
let state = ext.execute_with(|| QualificationDkgState::<Runtime>::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::<Vec<AuthIndex>>();
|
|
let shares = generate_mock_secp256k1_shares(&indices);
|
|
|
|
let tree = NetworkCurve::build_merkle_tree::<Secp256K1Sha256, _, _>(
|
|
&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::<Secp256K1Sha256, _, _>(
|
|
&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::<Vec<AuthIndex>>();
|
|
let scalars = generate_mock_secp256k1_scalars(&indices);
|
|
|
|
let tree = NetworkCurve::build_merkle_tree::<Secp256K1Sha256, _, _>(
|
|
&scalars,
|
|
|scalar| {
|
|
let serialized = <Secp256K1ScalarField as Field>::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::<Secp256K1Sha256, _, _>(
|
|
&scalars,
|
|
&tree,
|
|
auth_index,
|
|
|scalar| {
|
|
let serialized = <Secp256K1ScalarField as Field>::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<AuthIndex> = unique_indices.into_iter().collect();
|
|
let shares = generate_mock_secp256k1_shares(&indices);
|
|
|
|
let tree = NetworkCurve::build_merkle_tree::<Secp256K1Sha256, _, _>(
|
|
&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::<Secp256K1Sha256, _, _>(
|
|
&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<AuthIndex> = unique_indices.into_iter().collect();
|
|
let scalars = generate_mock_secp256k1_scalars(&indices);
|
|
|
|
let tree = NetworkCurve::build_merkle_tree::<Secp256K1Sha256, _, _>(
|
|
&scalars,
|
|
|scalar| {
|
|
let serialized = <Secp256K1ScalarField as Field>::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::<Secp256K1Sha256, _, _>(
|
|
&scalars,
|
|
&tree,
|
|
auth_index,
|
|
|scalar| {
|
|
let serialized = <Secp256K1ScalarField as Field>::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::<Secp256K1Sha256, _, _>(
|
|
&shares,
|
|
|share| share.serialize().map_err(|_| ExodusError::SerializationError),
|
|
).unwrap();
|
|
|
|
let root = *tree.last().unwrap();
|
|
|
|
let (shares_bytes, proof) =
|
|
NetworkCurve::generate_merkle_proof_from_tree::<Secp256K1Sha256, _, _>(
|
|
&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::<Secp256K1Sha256, _, _>(
|
|
&scalars,
|
|
|scalar| {
|
|
let serialized = <Secp256K1ScalarField as Field>::serialize(scalar);
|
|
Ok(serialized.as_ref().to_vec())
|
|
}
|
|
).unwrap();
|
|
|
|
let root = *tree.last().unwrap();
|
|
|
|
let (scalars_bytes, proof) =
|
|
NetworkCurve::generate_merkle_proof_from_tree::<Secp256K1Sha256, _, _>(
|
|
&scalars,
|
|
&tree,
|
|
1023,
|
|
|scalar| {
|
|
let serialized = <Secp256K1ScalarField as Field>::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::<Secp256K1Sha256, _, _>(
|
|
&shares,
|
|
|scalar| scalar.serialize().map_err(|_| ExodusError::SerializationError),
|
|
).unwrap();
|
|
|
|
let root = *tree.last().unwrap();
|
|
|
|
let (shares_bytes, proof) =
|
|
NetworkCurve::generate_merkle_proof_from_tree::<Secp256K1Sha256, _, _>(
|
|
&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::<Secp256K1Sha256, _, _>(
|
|
&scalars,
|
|
|scalar| {
|
|
let serialized = <Secp256K1ScalarField as Field>::serialize(scalar);
|
|
Ok(serialized.as_ref().to_vec())
|
|
}
|
|
).unwrap();
|
|
|
|
let root = *tree.last().unwrap();
|
|
|
|
let (scalars_bytes, proof) =
|
|
NetworkCurve::generate_merkle_proof_from_tree::<Secp256K1Sha256, _, _>(
|
|
&scalars,
|
|
&tree,
|
|
2,
|
|
|scalar| {
|
|
let serialized = <Secp256K1ScalarField as Field>::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::<Secp256K1Sha256, _, _>(
|
|
&shares,
|
|
|scalar| scalar.serialize().map_err(|_| ExodusError::SerializationError),
|
|
).unwrap();
|
|
|
|
let root = *tree.last().unwrap();
|
|
|
|
let (shares_bytes, proof) =
|
|
NetworkCurve::generate_merkle_proof_from_tree::<Secp256K1Sha256, _, _>(
|
|
&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::<Secp256K1Sha256, _, _>(
|
|
&scalars,
|
|
|scalar| {
|
|
let serialized = <Secp256K1ScalarField as Field>::serialize(scalar);
|
|
Ok(serialized.as_ref().to_vec())
|
|
}
|
|
).unwrap();
|
|
|
|
let root = *tree.last().unwrap();
|
|
|
|
let (scalars_bytes, proof) =
|
|
NetworkCurve::generate_merkle_proof_from_tree::<Secp256K1Sha256, _, _>(
|
|
&scalars,
|
|
&tree,
|
|
2,
|
|
|scalar| {
|
|
let serialized = <Secp256K1ScalarField as Field>::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::<EncryptionData<Runtime>>,
|
|
);
|
|
|
|
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::<EncryptionData<Runtime>>,
|
|
).unwrap();
|
|
|
|
let encryption_data: EncryptionData<Runtime> = 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::<EncryptionData<Runtime>>,
|
|
);
|
|
|
|
let encryption_cipher = curve.ecdh_get_cipher_from_keys(
|
|
&sender_round1_package_bytes,
|
|
&round1_secret_package_bytes,
|
|
&additional_info,
|
|
sp_std::marker::PhantomData::<EncryptionData<Runtime>>,
|
|
).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::<BTreeMap<AuthIndex, Vec<u8>>>();
|
|
|
|
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<AuthIndex, Vec<u8>>| 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::<BTreeMap<AuthIndex, _>>();
|
|
|
|
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<AuthIndex> = 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);
|
|
}
|