Uncle Stinky 2026-10-10 17:16:31 +03:00
parent 1dc61fb17d
commit 653bf08486
Signed by: st1nky
GPG Key ID: 016064BD97603B40
3 changed files with 178 additions and 120 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "ghost-exodus"
version = "0.0.25"
version = "0.0.26"
description = "Threshold signature generation with DKG included"
license.workspace = true
authors.workspace = true

View File

@ -146,6 +146,7 @@ const fn round1_package_size(
.saturating_add(commitments_bytes_len)
}
// TODO: revisit and make it correctly compute
const fn encrypted_ciphertext_bytes_len(
scalar_bytes_len: usize,
header_bytes_len: usize,
@ -451,7 +452,7 @@ pub mod pallet {
authority_index: AuthIndex,
network_curve: NetworkCurve,
},
Round4JustificatPackage {
Round4JustificationPackage {
justifications_count: AuthIndex,
authority_index: AuthIndex,
network_curve: NetworkCurve,
@ -502,11 +503,11 @@ pub mod pallet {
MemberNotQualified,
InvalidMerkleProof,
NetworkDoesNotExist,
NoActiveAuthorities,
VerifyingShareNotFound,
BridgeQueueMinimumNotMet,
DkgAuthoritiesInProgress,
DkgAuthoritiesNotInitialized,
DkgQualificationNotInitialized,
Round0PackageInvalidProof,
Round0PackageAlreadyRegistered,
@ -963,26 +964,29 @@ pub mod pallet {
return Err(Error::<T>::Round1PackageInvalidProof.into());
};
let state = QualificationDkgState::<T>::get(&network_curve);
ensure!(state.is_first_phase(), Error::<T>::DkgWrongRound);
let hash1 = Round0Packages::<T>::get(&network_curve, authority_index);
let hash2 = ExodusHash::from(blake2_256(&round1.package));
ensure!(!hash1.is_zero() && hash1 == hash2, Error::<T>::Round1PackageBadHash);
let needed_commitment_count = QualificationAuthorities::<T>::decode_len(&network_curve)
.map(|max_signers| {
get_byzantium_quorum_threshold(max_signers as AuthIndex)
})
.ok_or(Error::<T>::DkgQualificationNotInitialized)?;
let estimated_commitment_count =
network_curve.dkg_verify_proof_of_knowledge(
authority_index,
&round1.package,
).map_err(|_| Error::<T>::Round1PackageInvalidProof)?;
let state = QualificationDkgState::<T>::get(&network_curve);
ensure!(state.is_first_phase(), Error::<T>::DkgWrongRound);
state.count_ones::<AuthIndex>().checked_sub(1)
.map(|needed_commitment_count| {
estimated_commitment_count
.eq(&needed_commitment_count)
.then(|| ())
})
.ok_or(Error::<T>::Round1PackageInvalidLength)?;
ensure!(
estimated_commitment_count == needed_commitment_count,
Error::<T>::Round1PackageInvalidLength,
);
Round1Packages::<T>::try_mutate(
&network_curve,
@ -1191,7 +1195,7 @@ pub mod pallet {
Ok(())
})?;
Self::deposit_event(Event::<T>::Round4JustificatPackage {
Self::deposit_event(Event::<T>::Round4JustificationPackage {
justifications_count,
authority_index,
network_curve,
@ -4173,36 +4177,12 @@ impl<T: Config> Pallet<T> {
network_curve,
);
let padded_participants = qualification_state
.highest_bit::<usize>()
.map(|bit_pos| bit_pos.saturating_add(1))
.unwrap_or_default();
let bitmask_len = padded_participants.div_ceil(8);
let blob_len = round2_encrypted_package_size(
padded_participants,
network_curve.scalar_bytes_len(),
network_curve.header_bytes_len(),
);
let round2_packages =
Self::gather_incoming_offchain_packages(
network_curve,
authority_index,
ROUND_NUMBER_2,
&qualification_state.get_indexes(),
blob_len.saturating_div(padded_participants),
|mut raw_slice| EncryptionData::decode(&mut raw_slice).ok(),
);
let (indices, decrypted) =
Self::ecdh_decrypt_round2_packages(
&qualification_state,
&round1_packages,
&secret_package,
&round2_packages,
authority_index,
bitmask_len,
qualification_state.get_dkg_index(),
network_curve,
);
@ -4620,19 +4600,12 @@ impl<T: Config> Pallet<T> {
network_curve: NetworkCurve,
authority_index: AuthIndex,
) {
let padded_participants = qualification_state
.highest_bit::<usize>()
.map(|bit_pos| bit_pos.saturating_add(1))
.unwrap_or_default();
let blob_len = round2_package_size(
padded_participants,
let round2_package_size = round2_package_size(
1,
network_curve.scalar_bytes_len(),
network_curve.header_bytes_len(),
);
let round2_package_size = blob_len.saturating_div(padded_participants);
let mut kicked = sp_std::collections::btree_set::BTreeSet::new();
let mut all_justifications = sp_std::collections::btree_map::BTreeMap::new();
@ -4749,42 +4722,6 @@ impl<T: Config> Pallet<T> {
.collect()
}
pub fn gather_incoming_offchain_packages<R, E, F>(
network_curve: NetworkCurve,
authority_index: AuthIndex,
round: u8,
participants: &ParticipantsBitmap<T>,
single_bytes_len: usize,
decode_fn: F
) -> E
where
E: FromIterator<(AuthIndex, R)>,
F: Fn(&[u8]) -> Option<R>,
{
participants
.iter::<AuthIndex>()
.filter(|&index| index != authority_index)
.filter_map(|index| {
let key = Self::create_offchain_dkg_key(network_curve, index, round);
let bundle = StorageValueRef::persistent(&key)
.get::<BundledPackages>()
.ok()??;
let (start_offset, end_offset) =
Self::locate_package_bounds(
&bundle,
single_bytes_len,
authority_index,
)?;
let raw_encrypted_slice = &bundle.blob[start_offset..end_offset];
let decoded_data = decode_fn(raw_encrypted_slice)?;
Some((index, decoded_data))
})
.collect::<E>()
}
fn clear_incoming_offchain_packages(
network_curve: NetworkCurve,
rounds: &[u8],
@ -4797,55 +4734,83 @@ impl<T: Config> Pallet<T> {
}
fn ecdh_decrypt_round2_packages(
qualification_state: &QualifyingState<T>,
round1_packages: &BTreeMap<AuthIndex, Vec<u8>>,
round1_secret_package: &Vec<u8>,
round2_packages: &BTreeMap<AuthIndex, EncryptionData<T>>,
authority_index: AuthIndex,
bitvec_len: usize,
dkg_index: DkgIndex,
network_curve: NetworkCurve,
) -> (Vec<u8>, BTreeMap<AuthIndex, Vec<u8>>) {
round2_packages
let dkg_index = qualification_state.get_dkg_index();
let bitmask_len = qualification_state
.highest_bit::<usize>()
.map(|bit_pos| {
bit_pos.saturating_add(1).div_ceil(8)
})
.unwrap_or_default();
let encryption_size = round2_encrypted_package_size(
1,
network_curve.scalar_bytes_len(),
network_curve.header_bytes_len(),
);
round1_packages
.iter()
.filter(|(&sender_index, _)| sender_index != authority_index)
.filter_map(|(sender_index, encrypted_round2_packages)| {
let round1_sender_package = round1_packages.get(&sender_index)?;
.map(|(&sender_index, round1_sender_package)| {
let key = Self::create_offchain_dkg_key(network_curve, sender_index, ROUND_NUMBER_2);
let info = network_curve.ecdh_prepare_additional_info(
*sender_index,
authority_index,
dkg_index,
sp_std::marker::PhantomData::<EncryptionData<T>>,
);
let decryption_result = network_curve
.ecdh_get_cipher_from_keys(
&round1_sender_package,
&round1_secret_package,
&info,
sp_std::marker::PhantomData::<EncryptionData<T>>,
)
.and_then(|cipher| {
network_curve.ecdh_decrypt_package(
&cipher,
&encrypted_round2_packages,
&info,
)
let decryption_result = StorageValueRef::persistent(&key)
.get::<BundledPackages>()
.ok()
.flatten()
.and_then(|bundle| {
Self::locate_package_bounds(&bundle, encryption_size, authority_index)
.and_then(|(start, end)| {
bundle.blob.get(start..end).and_then(|raw_slice| {
EncryptionData::decode(&mut &raw_slice[..]).ok()
})
})
})
.and_then(|decrypted| {
network_curve.dkg_verify_private_package(
.ok_or(sender_index)
.and_then(|encrypted_round2_package: EncryptionData<T>| {
let info = network_curve.ecdh_prepare_additional_info(
sender_index,
authority_index,
&round1_sender_package,
&decrypted,
).map(|_| decrypted)
})
.map_err(|_| *sender_index)
.map(|decrypted_package| (*sender_index, decrypted_package));
dkg_index,
sp_std::marker::PhantomData::<EncryptionData<T>>,
);
Some(decryption_result)
network_curve
.ecdh_get_cipher_from_keys(
round1_sender_package,
round1_secret_package,
&info,
sp_std::marker::PhantomData::<EncryptionData<T>>,
)
.and_then(|cipher| {
network_curve.ecdh_decrypt_package(
&cipher,
&encrypted_round2_package,
&info,
)
})
.and_then(|decrypted| {
network_curve.dkg_verify_private_package(
authority_index,
round1_sender_package,
&decrypted,
).map(|_| decrypted)
})
.map_err(|_| sender_index)
})
.map(|decrypted_package| (sender_index, decrypted_package));
decryption_result
})
.fold(
(vec![0u8; bitvec_len], BTreeMap::new()),
(vec![0u8; bitmask_len], BTreeMap::new()),
|(mut accused_indexes, mut decrypted), decryption_result| {
match decryption_result {
Ok((sender_index, package)) => { decrypted.insert(sender_index, package); },

View File

@ -1042,6 +1042,99 @@ fn run_dkg_session(
assert_ne!(prev_active_authorities, active_authorities);
}
#[test]
fn test_dkg_robustness_against_malformed_scale_slots() {
let (mut ext, tx_pool_state) = new_test_ext();
let curve = NetworkCurve::Secp256k1;
let honest_validators = vec![0, 1, 2, 4, 5, 6, 7, 8, 9];
let malicious_validator = 3;
let all_validators = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let authorities: Vec<UintAuthorityId> = all_validators.iter().map(|&id| id.into()).collect();
ext.execute_with(|| Exodus::start_dkg_qualification(authorities, NetworkCurve::iter()));
run_to_next_round(&mut ext, curve);
for &authority in &all_validators {
assert_ok!(execute_round0_happy_path(&mut ext, &tx_pool_state, curve, authority));
}
run_to_next_round(&mut ext, curve);
for &authority in &all_validators {
assert_ok!(execute_round1_happy_path(&mut ext, &tx_pool_state, curve, authority));
}
run_to_next_round(&mut ext, curve);
for &authority in &honest_validators {
assert_ok!(execute_round2_happy_path(&mut ext, &tx_pool_state, curve, authority));
}
ext.execute_with(|| {
let dkg_index = QualificationDkgState::<Runtime>::get(curve).get_dkg_index();
let padded_participants = 10usize;
let bitmask_len = padded_participants.div_ceil(8);
let mut perfect_bitmask = vec![0u8; bitmask_len];
for &recipient in &honest_validators {
let idx = recipient as usize;
perfect_bitmask[idx >> 3] |= 1 << (idx & 7);
}
let expected_blob_len = round2_encrypted_package_size(
padded_participants,
curve.scalar_bytes_len(),
curve.header_bytes_len(),
);
let malformed_blob = vec![0xAAu8; expected_blob_len];
let dummy_encryption_bundle = BundledPackages {
bitmask: perfect_bitmask,
blob: malformed_blob,
};
let dummy_context = PackageContext::default()
.with_authority_index(malicious_validator as AuthIndex)
.with_network_curve(curve)
.with_dkg_index(dkg_index)
.with_dkg_round2(dummy_encryption_bundle)
.unwrap();
let dummy_package = DkgPackage::Round2(dummy_context);
let malicious_signer: UintAuthorityId = malicious_validator.into();
let dummy_signature = malicious_signer.sign(&dummy_package.encode()).unwrap();
assert_ok!(Exodus::register_encrypted_round2_packages(
frame_system::RawOrigin::None.into(),
dummy_package,
dummy_signature
));
});
run_to_next_round(&mut ext, curve);
for &authority in &honest_validators {
assert_ok!(execute_round3_happy_path(&mut ext, &tx_pool_state, curve, authority));
}
ext.execute_with(|| {
let complaints = Complaints::<Runtime>::get(curve);
for &accuser in &honest_validators {
let accuser_bitmap = complaints.get(&(accuser as AuthIndex)).unwrap();
assert!(accuser_bitmap.contains(malicious_validator as AuthIndex));
}
});
run_to_next_round(&mut ext, curve);
for &authority in &honest_validators {
assert_ok!(execute_round4_happy_path(&mut ext, &tx_pool_state, curve, authority));
}
}
#[test]
fn test_exodus_dkg_distributed_synced_exclusion() {
let (mut ext, tx_pool_state) = new_test_ext();