From cc6ed8a0c455f013577c408bf2421ad066cba941 Mon Sep 17 00:00:00 2001 From: vedhavyas Date: Tue, 9 Jul 2024 11:47:27 +0530 Subject: [PATCH] add functionality to freeze, unfreeze, and prune a given execution reciept of a given domain through Sudo --- crates/pallet-domains/src/block_tree.rs | 38 +++++++- crates/pallet-domains/src/lib.rs | 124 ++++++++++++++++++++++-- 2 files changed, 153 insertions(+), 9 deletions(-) diff --git a/crates/pallet-domains/src/block_tree.rs b/crates/pallet-domains/src/block_tree.rs index 5d57de1240..9cf59ccc01 100644 --- a/crates/pallet-domains/src/block_tree.rs +++ b/crates/pallet-domains/src/block_tree.rs @@ -616,10 +616,12 @@ mod tests { use crate::tests::{ create_dummy_bundle_with_receipts, create_dummy_receipt, extend_block_tree, extend_block_tree_from_zero, get_block_tree_node_at, new_test_ext_with_extensions, - register_genesis_domain, run_to_block, BlockTreePruningDepth, Test, + register_genesis_domain, run_to_block, BlockTreePruningDepth, Domains, Test, }; + use crate::FrozenDomains; use frame_support::dispatch::RawOrigin; use frame_support::{assert_err, assert_ok}; + use frame_system::Origin; use sp_core::H256; use sp_domains::{BundleDigest, InboxedBundle, InvalidBundleType}; @@ -971,6 +973,40 @@ mod tests { }); } + #[test] + fn test_prune_domain_execution_receipt() { + let creator = 0u128; + let operator_id = 1u64; + let mut ext = new_test_ext_with_extensions(); + ext.execute_with(|| { + let domain_id = register_genesis_domain(creator, vec![operator_id]); + let _next_receipt = extend_block_tree_from_zero(domain_id, operator_id, 3); + let head_receipt_number = HeadReceiptNumber::::get(domain_id); + + // freeze domain + assert!(!FrozenDomains::::get().contains(&domain_id)); + Domains::freeze_domain(Origin::::Root.into(), domain_id).unwrap(); + assert!(FrozenDomains::::get().contains(&domain_id)); + + // prune execution recept + let head_receipt_hash = BlockTree::::get(domain_id, head_receipt_number).unwrap(); + Domains::prune_domain_execution_receipt( + Origin::::Root.into(), + domain_id, + head_receipt_hash, + ) + .unwrap(); + assert_eq!( + HeadReceiptNumber::::get(domain_id), + head_receipt_number - 1 + ); + + // unfreeze domain + Domains::unfreeze_domain(Origin::::Root.into(), domain_id).unwrap(); + assert!(!FrozenDomains::::get().contains(&domain_id)); + }) + } + #[test] fn test_invalid_receipt() { let creator = 0u128; diff --git a/crates/pallet-domains/src/lib.rs b/crates/pallet-domains/src/lib.rs index 88398b6c5b..37c99430d5 100644 --- a/crates/pallet-domains/src/lib.rs +++ b/crates/pallet-domains/src/lib.rs @@ -167,12 +167,13 @@ pub(crate) type StateRootOf = <::Hashing as Hash>: mod pallet { #![allow(clippy::large_enum_variant)] + #[cfg(not(feature = "runtime-benchmarks"))] + use crate::block_tree::AcceptedReceiptType; use crate::block_tree::{ - execution_receipt_type, process_execution_receipt, Error as BlockTreeError, ReceiptType, + execution_receipt_type, process_execution_receipt, prune_receipt, Error as BlockTreeError, + ReceiptType, }; #[cfg(not(feature = "runtime-benchmarks"))] - use crate::block_tree::{prune_receipt, AcceptedReceiptType}; - #[cfg(not(feature = "runtime-benchmarks"))] use crate::bundle_storage_fund::refund_storage_fee; use crate::bundle_storage_fund::{charge_bundle_storage_fee, Error as BundleStorageFundError}; use crate::domain_registry::{ @@ -185,13 +186,12 @@ mod pallet { ScheduledRuntimeUpgrade, }; #[cfg(not(feature = "runtime-benchmarks"))] - use crate::staking::do_mark_operators_as_slashed; - #[cfg(not(feature = "runtime-benchmarks"))] use crate::staking::do_reward_operators; use crate::staking::{ - do_deregister_operator, do_nominate_operator, do_register_operator, do_unlock_funds, - do_unlock_nominator, do_withdraw_stake, Deposit, DomainEpoch, Error as StakingError, - Operator, OperatorConfig, SharePrice, StakingSummary, Withdrawal, + do_deregister_operator, do_mark_operators_as_slashed, do_nominate_operator, + do_register_operator, do_unlock_funds, do_unlock_nominator, do_withdraw_stake, Deposit, + DomainEpoch, Error as StakingError, Operator, OperatorConfig, SharePrice, StakingSummary, + Withdrawal, }; #[cfg(not(feature = "runtime-benchmarks"))] use crate::staking_epoch::do_slash_operator; @@ -713,6 +713,11 @@ mod pallet { pub type DomainSudoCalls = StorageMap<_, Identity, DomainId, DomainSudoCall, ValueQuery>; + /// Storage that hold a list of all frozen domains. + /// A frozen domain does not accept the bundles but does accept a fraud proof. + #[pallet::storage] + pub type FrozenDomains = StorageValue<_, BTreeSet, ValueQuery>; + #[derive(TypeInfo, Encode, Decode, PalletError, Debug, PartialEq)] pub enum BundleError { /// Can not find the operator for given operator id. @@ -752,6 +757,8 @@ mod pallet { SlotSmallerThanPreviousBlockBundle, /// Equivocated bundle in current block EquivocatedBundle, + /// Domain is frozen and cannot accept new bundles + DomainFrozen, } #[derive(TypeInfo, Encode, Decode, PalletError, Debug, PartialEq)] @@ -880,6 +887,8 @@ mod pallet { DomainSudoCallExists, /// Invalid Domain sudo call. InvalidDomainSudoCall, + /// Domain must be frozen before execution receipt can be pruned. + DomainNotFrozen, } /// Reason for slashing an operator @@ -977,6 +986,16 @@ mod pallet { nominator_id: NominatorId, amount: BalanceOf, }, + DomainFrozen { + domain_id: DomainId, + }, + DomainUnfrozen { + domain_id: DomainId, + }, + PrunedExecutionReceipt { + domain_id: DomainId, + new_head_receipt_number: Option>, + }, } /// Per-domain state for tx range calculation. @@ -1541,6 +1560,85 @@ mod pallet { ); Ok(()) } + + /// Freezes a given domain. + /// A frozen domain does not accept new bundles but accepts fraud proofs. + #[pallet::call_index(17)] + #[pallet::weight(::DbWeight::get().reads_writes(0, 1))] + pub fn freeze_domain(origin: OriginFor, domain_id: DomainId) -> DispatchResult { + ensure_root(origin)?; + FrozenDomains::::mutate(|frozen_domains| frozen_domains.insert(domain_id)); + Self::deposit_event(Event::DomainFrozen { domain_id }); + Ok(()) + } + + /// Unfreezes a frozen domain. + #[pallet::call_index(18)] + #[pallet::weight(::DbWeight::get().reads_writes(0, 1))] + pub fn unfreeze_domain(origin: OriginFor, domain_id: DomainId) -> DispatchResult { + ensure_root(origin)?; + FrozenDomains::::mutate(|frozen_domains| frozen_domains.remove(&domain_id)); + Self::deposit_event(Event::DomainUnfrozen { domain_id }); + Ok(()) + } + + /// Prunes a given execution receipt for given frozen domain. + /// This call assumes the execution receipt to be bad and implicitly trusts Sudo + /// to do necessary validation of the ER before dispatching this call. + #[pallet::call_index(19)] + #[pallet::weight(Pallet::::max_prune_domain_execution_receipt())] + pub fn prune_domain_execution_receipt( + origin: OriginFor, + domain_id: DomainId, + bad_receipt_hash: ReceiptHashFor, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + ensure!( + FrozenDomains::::get().contains(&domain_id), + Error::::DomainNotFrozen + ); + + let head_receipt_number = HeadReceiptNumber::::get(domain_id); + let bad_receipt_number = BlockTreeNodes::::get(bad_receipt_hash) + .ok_or::>(FraudProofError::BadReceiptNotFound.into())? + .execution_receipt + .domain_block_number; + // The `head_receipt_number` must greater than or equal to any existing receipt, including + // the bad receipt. + ensure!( + head_receipt_number >= bad_receipt_number, + Error::::from(FraudProofError::BadReceiptNotFound), + ); + + let mut actual_weight = T::DbWeight::get().reads(3); + + // prune the bad ER + let block_tree_node = prune_receipt::(domain_id, bad_receipt_number) + .map_err(Error::::from)? + .ok_or::>(FraudProofError::BadReceiptNotFound.into())?; + + actual_weight = actual_weight.saturating_add(T::WeightInfo::handle_bad_receipt( + (block_tree_node.operator_ids.len() as u32).min(MAX_BUNLDE_PER_BLOCK), + )); + + do_mark_operators_as_slashed::( + block_tree_node.operator_ids.into_iter(), + SlashedReason::BadExecutionReceipt(bad_receipt_hash), + ) + .map_err(Error::::from)?; + + // Update the head receipt number to `bad_receipt_number - 1` + let new_head_receipt_number = bad_receipt_number.saturating_sub(One::one()); + HeadReceiptNumber::::insert(domain_id, new_head_receipt_number); + actual_weight = actual_weight.saturating_add(T::DbWeight::get().reads_writes(0, 1)); + + Self::deposit_event(Event::PrunedExecutionReceipt { + domain_id, + new_head_receipt_number: Some(new_head_receipt_number), + }); + + Ok(Some(actual_weight).into()) + } } #[pallet::genesis_config] @@ -1950,6 +2048,11 @@ impl Pallet { pre_dispatch: bool, ) -> Result<(), BundleError> { let domain_id = opaque_bundle.domain_id(); + ensure!( + !FrozenDomains::::get().contains(&domain_id), + BundleError::DomainFrozen + ); + let operator_id = opaque_bundle.operator_id(); let sealed_header = &opaque_bundle.sealed_header; let slot_number = opaque_bundle.slot_number(); @@ -2466,6 +2569,11 @@ impl Pallet { ) } + pub fn max_prune_domain_execution_receipt() -> Weight { + T::WeightInfo::handle_bad_receipt(MAX_BUNLDE_PER_BLOCK) + .saturating_add(T::DbWeight::get().reads_writes(3, 1)) + } + fn actual_epoch_transition_weight(epoch_transition_res: EpochTransitionResult) -> Weight { let EpochTransitionResult { rewarded_operator_count,