From 653bf0848609c62f0a02ecf5903b212d99bb7375 Mon Sep 17 00:00:00 2001 From: Uncle Stinky Date: Sat, 10 Oct 2026 17:16:31 +0300 Subject: [PATCH] fix for https://ghostforum.org/t/h16-dkg-round-1-rejects-all-honest-packages-unless-exactly-threshold-n-1-registered-round-0-deterministic-bridge-halt-for-n-7/123 Signed-off-by: Uncle Stinky --- pallets/exodus/Cargo.toml | 2 +- pallets/exodus/src/lib.rs | 203 +++++++++++++++--------------------- pallets/exodus/src/tests.rs | 93 +++++++++++++++++ 3 files changed, 178 insertions(+), 120 deletions(-) diff --git a/pallets/exodus/Cargo.toml b/pallets/exodus/Cargo.toml index 4c56a27..a512406 100644 --- a/pallets/exodus/Cargo.toml +++ b/pallets/exodus/Cargo.toml @@ -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 diff --git a/pallets/exodus/src/lib.rs b/pallets/exodus/src/lib.rs index b051fef..79eb140 100644 --- a/pallets/exodus/src/lib.rs +++ b/pallets/exodus/src/lib.rs @@ -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::::Round1PackageInvalidProof.into()); }; + let state = QualificationDkgState::::get(&network_curve); + ensure!(state.is_first_phase(), Error::::DkgWrongRound); + let hash1 = Round0Packages::::get(&network_curve, authority_index); let hash2 = ExodusHash::from(blake2_256(&round1.package)); ensure!(!hash1.is_zero() && hash1 == hash2, Error::::Round1PackageBadHash); + let needed_commitment_count = QualificationAuthorities::::decode_len(&network_curve) + .map(|max_signers| { + get_byzantium_quorum_threshold(max_signers as AuthIndex) + }) + .ok_or(Error::::DkgQualificationNotInitialized)?; + let estimated_commitment_count = network_curve.dkg_verify_proof_of_knowledge( authority_index, &round1.package, ).map_err(|_| Error::::Round1PackageInvalidProof)?; - let state = QualificationDkgState::::get(&network_curve); - ensure!(state.is_first_phase(), Error::::DkgWrongRound); - - state.count_ones::().checked_sub(1) - .map(|needed_commitment_count| { - estimated_commitment_count - .eq(&needed_commitment_count) - .then(|| ()) - }) - .ok_or(Error::::Round1PackageInvalidLength)?; + ensure!( + estimated_commitment_count == needed_commitment_count, + Error::::Round1PackageInvalidLength, + ); Round1Packages::::try_mutate( &network_curve, @@ -1191,7 +1195,7 @@ pub mod pallet { Ok(()) })?; - Self::deposit_event(Event::::Round4JustificatPackage { + Self::deposit_event(Event::::Round4JustificationPackage { justifications_count, authority_index, network_curve, @@ -4173,36 +4177,12 @@ impl Pallet { network_curve, ); - let padded_participants = qualification_state - .highest_bit::() - .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 Pallet { network_curve: NetworkCurve, authority_index: AuthIndex, ) { - let padded_participants = qualification_state - .highest_bit::() - .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 Pallet { .collect() } - pub fn gather_incoming_offchain_packages( - network_curve: NetworkCurve, - authority_index: AuthIndex, - round: u8, - participants: &ParticipantsBitmap, - single_bytes_len: usize, - decode_fn: F - ) -> E - where - E: FromIterator<(AuthIndex, R)>, - F: Fn(&[u8]) -> Option, - { - participants - .iter::() - .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::() - .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::() - } - fn clear_incoming_offchain_packages( network_curve: NetworkCurve, rounds: &[u8], @@ -4797,55 +4734,83 @@ impl Pallet { } fn ecdh_decrypt_round2_packages( + qualification_state: &QualifyingState, round1_packages: &BTreeMap>, round1_secret_package: &Vec, - round2_packages: &BTreeMap>, authority_index: AuthIndex, - bitvec_len: usize, - dkg_index: DkgIndex, network_curve: NetworkCurve, ) -> (Vec, BTreeMap>) { - round2_packages + let dkg_index = qualification_state.get_dkg_index(); + + let bitmask_len = qualification_state + .highest_bit::() + .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::>, - ); - - let decryption_result = network_curve - .ecdh_get_cipher_from_keys( - &round1_sender_package, - &round1_secret_package, - &info, - sp_std::marker::PhantomData::>, - ) - .and_then(|cipher| { - network_curve.ecdh_decrypt_package( - &cipher, - &encrypted_round2_packages, - &info, - ) + let decryption_result = StorageValueRef::persistent(&key) + .get::() + .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| { + 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::>, + ); - Some(decryption_result) + network_curve + .ecdh_get_cipher_from_keys( + round1_sender_package, + round1_secret_package, + &info, + sp_std::marker::PhantomData::>, + ) + .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); }, diff --git a/pallets/exodus/src/tests.rs b/pallets/exodus/src/tests.rs index 4c5a99e..9f5ff94 100644 --- a/pallets/exodus/src/tests.rs +++ b/pallets/exodus/src/tests.rs @@ -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 = 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::::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::::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();