mitigate FROST/OCW DoS via dynamic bridge out threshold

Signed-off-by: Uncle Stinky <uncle.stinky@ghostchain.io>
This commit is contained in:
Uncle Stinky 2026-09-23 21:17:53 +03:00
parent d3b926cd47
commit 8ef28234ae
Signed by: st1nky
GPG Key ID: 016064BD97603B40
5 changed files with 153 additions and 28 deletions

View File

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

View File

@ -506,7 +506,7 @@ mod benchmarks {
QualificationDkgState::<T>::insert(&network_curve, dkg_state);
ActiveDkgAuthorities::<T>::insert(&network_curve, active_dkg);
ActiveAuthorities::<T>::insert(&network_curve, authorities);
ExodusRequests::<T>::insert(network_curve, exodus_session, request);
ExodusRequests::<T>::insert((network_curve, exodus_session), request);
let nonce_package = PackageContext::default()
.with_network_curve(NetworkCurve::Secp256k1)
@ -584,7 +584,7 @@ mod benchmarks {
QualificationDkgState::<T>::insert(&network_curve, dkg_state);
ActiveDkgAuthorities::<T>::insert(&network_curve, active_dkg);
ActiveAuthorities::<T>::insert(&network_curve, authorities);
ExodusRequests::<T>::insert(network_curve, exodus_session, request);
ExodusRequests::<T>::insert((network_curve, exodus_session), request);
let mut roast_state = RoastSessionState::<ParticipantsBitmap::<T>>::default();
roast_state.status = RoastStatus::GroupCommitments;
@ -723,7 +723,7 @@ mod benchmarks {
QualificationDkgState::<T>::insert(&network_curve, dkg_state);
ActiveDkgAuthorities::<T>::insert(&network_curve, active_dkg);
ActiveAuthorities::<T>::insert(&network_curve, authorities);
ExodusRequests::<T>::insert(network_curve, exodus_session, request);
ExodusRequests::<T>::insert((network_curve, exodus_session), request);
let mut roast_state = RoastSessionState::<ParticipantsBitmap::<T>>::default();
roast_state.status = RoastStatus::PartialSignatures;
@ -796,7 +796,7 @@ mod benchmarks {
assert!(!consensus_exists);
assert!(!committers_exists);
assert!(!ExodusRequests::<T>::contains_key(&network_curve, &exodus_session));
assert!(!ExodusRequests::<T>::contains_key((&network_curve, &exodus_session)));
assert!(ExodusSignedGovernance::<T>::contains_key((&exodus_session, network_id), dkg_index));
Ok(())
@ -808,7 +808,7 @@ mod benchmarks {
let (_, network_curve, _, _) = prepare_pallet::<T>(1usize);
let network_id = NetworkIdOf::<T>::default();
let base_amount: BalanceOf<T> = 10_000u32.into();
let base_amount: BalanceOf<T> = T::BaseEvmAmount::get().unique_saturated_into();
let amount = base_amount.saturating_add(T::Currency::minimum_balance());
let bounty = Perbill::from_percent(5);
@ -840,7 +840,7 @@ mod benchmarks {
receiver,
);
assert!(ExodusRequests::<T>::contains_key(&network_curve, current_exodus_session));
assert!(ExodusRequests::<T>::contains_key((&network_curve, current_exodus_session)));
assert_eq!(CurrentExodus::<T>::get(), current_exodus_session + 1);
assert_eq!(T::Currency::balance(&caller), initial_balance - amount);
assert_eq!(T::Currency::total_issuance(), total_issuance - amount);

View File

@ -423,6 +423,9 @@ pub mod pallet {
#[pallet::constant]
type MaxCursorLen: Get<u32>;
#[pallet::constant]
type BaseEvmAmount: Get<u128>;
type WeightInfo: WeightInfo;
}
@ -502,7 +505,7 @@ pub mod pallet {
NoActiveAuthorities,
InvalidSignatureShare,
VerifyingShareNotFound,
BelowExistentialBalance,
BridgeQueueMinimumNotMet,
DkgAuthoritiesInProgress,
PackagesAlreadyRegistered,
DkgAuthoritiesNotInitialized,
@ -551,10 +554,12 @@ pub mod pallet {
#[pallet::storage]
#[pallet::getter(fn exodus_requests)]
pub(super) type ExodusRequests<T: Config> = StorageDoubleMap<
pub(super) type ExodusRequests<T: Config> = CountedStorageNMap<
_,
Twox64Concat, NetworkCurve,
Twox64Concat, ExodusSession,
(
NMapKey<Twox64Concat, NetworkCurve>,
NMapKey<Twox64Concat, ExodusSession>,
),
ExodusRequest<NetworkIdOf<T>, BalanceOf<T>>,
OptionQuery,
>;
@ -1450,7 +1455,7 @@ pub mod pallet {
exodus_request.next_roast_session = roast_session
.saturating_add(1);
ExodusRequests::<T>::insert(&network_curve, &exodus_session, exodus_request);
ExodusRequests::<T>::insert((&network_curve, &exodus_session), exodus_request);
}
RoastSessions::<T>::insert(&exodus_session, &authority_index, roast_session);
@ -1508,7 +1513,7 @@ pub mod pallet {
let exodus_session = package.session;
ensure!(
ExodusRequests::<T>::contains_key(&network_curve, &exodus_session),
ExodusRequests::<T>::contains_key((&network_curve, &exodus_session)),
Error::<T>::ExodusRequestNotFound
);
@ -1640,7 +1645,7 @@ pub mod pallet {
);
let exodus_request =
ExodusRequests::<T>::get(&network_curve, &exodus_session)
ExodusRequests::<T>::get((&network_curve, &exodus_session))
.ok_or(Error::<T>::ExodusRequestNotFound)?;
ensure!(
@ -1795,7 +1800,7 @@ pub mod pallet {
return Ok(());
}
ExodusRequests::<T>::remove(&network_curve, &exodus_session);
ExodusRequests::<T>::remove((&network_curve, &exodus_session));
match exodus_request.r#type {
ExodusRequestType::EvmRotation { .. } => {
@ -1838,10 +1843,17 @@ pub mod pallet {
) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(
amount > T::Currency::minimum_balance(),
Error::<T>::BelowExistentialBalance
);
let base_evm_amount: BalanceOf<T> = T::BaseEvmAmount::get()
.unique_saturated_into();
let pending_requests_count = ExodusRequests::<T>::count()
.unique_saturated_into();
let minimum_amount = base_evm_amount
.saturating_mul(pending_requests_count)
.saturating_add(T::Currency::minimum_balance());
ensure!(amount >= minimum_amount, Error::<T>::BridgeQueueMinimumNotMet);
let actual_amount = T::Currency::burn_from(
&who,
@ -1861,7 +1873,7 @@ pub mod pallet {
Self::deposit_event(Event::<T>::EvmBridgeOutRegistered {
who,
network_id,
amount,
amount: actual_amount,
bounty,
receiver,
});
@ -2151,7 +2163,7 @@ impl<T: Config> Pallet<T> {
let exodus_session = CurrentExodus::<T>::get();
let request = request_builder(exodus_session);
ExodusRequests::<T>::insert(&network.curve, &exodus_session, request);
ExodusRequests::<T>::insert((&network.curve, &exodus_session), request);
CurrentExodus::<T>::put(exodus_session.saturating_add(1));
Ok(())
@ -2566,7 +2578,7 @@ impl<T: Config> Pallet<T> {
};
weight.saturating_accrue(T::DbWeight::get().writes(1));
ExodusRequests::<T>::insert(&network_curve, &exodus_session, exodus_request);
ExodusRequests::<T>::insert((&network_curve, &exodus_session), exodus_request);
if let None = dkg_qualification
.try_push_rotation_session(exodus_session, network_type) {
@ -2607,7 +2619,7 @@ impl<T: Config> Pallet<T> {
let r#type = rotation_session.network_type;
weight.saturating_accrue(T::DbWeight::get().reads(1));
if ExodusRequests::<T>::contains_key(&network_curve, &session) {
if ExodusRequests::<T>::contains_key((&network_curve, &session)) {
return false
};
@ -3088,7 +3100,7 @@ impl<T: Config> Pallet<T> {
}
let exodus_request =
ExodusRequests::<T>::get(&network_curve, &package.session)
ExodusRequests::<T>::get((&network_curve, &package.session))
.ok_or(InvalidTransaction::BadProof)?;
let roast_session =
@ -3286,7 +3298,7 @@ impl<T: Config> Pallet<T> {
let threshold = get_byzantium_quorum_threshold(max_authorities);
let mut exodus_requests = ExodusRequests::<T>::iter_prefix(&network_curve)
let mut exodus_requests = ExodusRequests::<T>::iter_prefix((&network_curve,))
.collect::<Vec<(ExodusSession, ExodusRequest<_, _>)>>();
exodus_requests.sort_by_key(|&(session, _)| session);

View File

@ -3,7 +3,7 @@
use frame_system::EnsureRoot;
use frame_support::{
derive_impl, parameter_types,
traits::{ConstU32, ConstU64},
traits::{ConstU32, ConstU64, ConstU128},
};
use crate as ghost_exodus;
@ -143,6 +143,7 @@ impl ghost_exodus::Config for Runtime {
type DkgRoundPeriod = ConstU64<100>;
type RemovalLimit = ConstU32<20>;
type MaxCursorLen = ConstU32<20>;
type BaseEvmAmount = ConstU128<5>;
type WeightInfo = ();
}

View File

@ -810,7 +810,7 @@ fn run_dkg_session(
return Err(TestError::InvalidStorageAfter);
}
if ExodusRequests::<Runtime>::contains_key(curve, exodus) {
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)
@ -881,7 +881,7 @@ fn run_dkg_session(
for auth in prev_authorities.iter() {
let request_exists = ext.execute_with(|| {
ExodusRequests::<Runtime>::contains_key(&curve, &exodus_session)
ExodusRequests::<Runtime>::contains_key((&curve, &exodus_session))
});
if !request_exists { break; }
@ -1446,6 +1446,118 @@ fn test_prepare_round5_kick_on_empty_justifications() {
assert_eq!(indexes.active_bits_count(), authorities_len as u32);
}
#[test]
fn register_evm_bridge_out_dynamic_threshold_works() {
let (mut ext, _) = new_test_ext();
ext.execute_with(|| {
let network_curve = NetworkCurve::Secp256k1;
let network_id = 69u64;
let user = 420u64;
let receiver = EvmAddress::from([2u8; 20]);
let bounty = Perbill::from_percent(10);
let initial_balance: BalanceOf<Runtime> = 100_000u128.unique_saturated_into();
let _ = <Runtime as Config>::Currency::set_balance(&user, initial_balance);
let base_evm_amount_u128 = <<Runtime as Config>::BaseEvmAmount as Get<u128>>::get();
let base_evm_amount: BalanceOf<Runtime> = base_evm_amount_u128.unique_saturated_into();
let existential_deposit = <Runtime as Config>::Currency::minimum_balance();
let network_data = NetworkDataBuilder::default()
.with_network_curve(network_curve)
.build();
let _ = <Runtime as Config>::NetworkDataHandler::register(network_id, network_data).unwrap();
let _ = <Runtime as Config>::NetworkDataHandler::register_incoming(&network_id, 1337).unwrap();
let mut dkg_state = PendingDkgAuthorities::default();
loop {
if dkg_state.is_dkg_finalized() { break; }
dkg_state.next_phase();
}
QualificationDkgState::<Runtime>::insert(&network_curve, dkg_state);
let amount_1 = existential_deposit;
assert_ok!(Exodus::register_evm_bridge_out_exodus(
RuntimeOrigin::signed(user.clone()),
network_id,
amount_1,
bounty,
receiver,
));
assert_eq!(ExodusRequests::<Runtime>::count(), 1);
System::assert_last_event(RuntimeEvent::Exodus(
Event::EvmBridgeOutRegistered {
who: user.clone(),
network_id,
amount: amount_1,
bounty,
receiver,
}
));
assert_err!(
Exodus::register_evm_bridge_out_exodus(
RuntimeOrigin::signed(user.clone()),
network_id,
amount_1,
bounty,
receiver,
),
Error::<Runtime>::BridgeQueueMinimumNotMet
);
let amount_2 = existential_deposit.saturating_add(base_evm_amount);
assert_ok!(Exodus::register_evm_bridge_out_exodus(
RuntimeOrigin::signed(user.clone()),
network_id,
amount_2,
bounty,
receiver,
));
assert_err!(
Exodus::register_evm_bridge_out_exodus(
RuntimeOrigin::signed(user.clone()),
network_id,
amount_2,
bounty,
receiver,
),
Error::<Runtime>::BridgeQueueMinimumNotMet
);
assert_eq!(ExodusRequests::<Runtime>::count(), 2);
let amount_3 = amount_2.saturating_add(base_evm_amount);
assert_ok!(Exodus::register_evm_bridge_out_exodus(
RuntimeOrigin::signed(user.clone()),
network_id,
amount_3,
bounty,
receiver,
));
assert_eq!(ExodusRequests::<Runtime>::count(), 3);
assert_err!(
Exodus::register_evm_bridge_out_exodus(
RuntimeOrigin::signed(user.clone()),
network_id,
amount_3,
bounty,
receiver,
),
Error::<Runtime>::BridgeQueueMinimumNotMet
);
});
}
#[test]
fn test_merkle_tree_success_power_of_two_for_shares() {
let network_curve = NetworkCurve::Secp256k1;