pallet_domains/
block_tree.rs

1//! Domain block tree
2
3#[cfg(not(feature = "std"))]
4extern crate alloc;
5
6use crate::{
7    BalanceOf, BlockTree, BlockTreeNodeFor, BlockTreeNodes, Config, ConsensusBlockHash,
8    DomainBlockNumberFor, DomainGenesisBlockExecutionReceipt, DomainHashingFor,
9    DomainRuntimeUpgradeRecords, ExecutionInbox, ExecutionReceiptOf, HeadDomainNumber,
10    HeadReceiptNumber, InboxedBundleAuthor, LatestConfirmedDomainExecutionReceipt,
11    LatestSubmittedER, NewAddedHeadReceipt, Pallet, ReceiptHashFor, SkipBalanceChecks,
12};
13#[cfg(not(feature = "std"))]
14use alloc::vec::Vec;
15use frame_support::{PalletError, ensure};
16use frame_system::pallet_prelude::BlockNumberFor;
17use parity_scale_codec::{Decode, Encode};
18use scale_info::TypeInfo;
19use sp_core::Get;
20use sp_domains::merkle_tree::MerkleTree;
21use sp_domains::{
22    ChainId, DomainId, DomainsTransfersTracker, ExecutionReceipt, OnChainRewards, OperatorId,
23    Transfers,
24};
25use sp_runtime::traits::{BlockNumberProvider, CheckedSub, One, Saturating, Zero};
26use sp_std::cmp::Ordering;
27use sp_std::collections::btree_map::BTreeMap;
28
29/// Block tree specific errors
30#[derive(TypeInfo, Encode, Decode, PalletError, Debug, PartialEq)]
31pub enum Error {
32    InvalidExtrinsicsRoots,
33    UnknownParentBlockReceipt,
34    BuiltOnUnknownConsensusBlock,
35    InFutureReceipt,
36    PrunedReceipt,
37    StaleReceipt,
38    NewBranchReceipt,
39    BadGenesisReceipt,
40    UnexpectedReceiptType,
41    MaxHeadDomainNumber,
42    MissingDomainBlock,
43    InvalidTraceRoot,
44    InvalidExecutionTrace,
45    UnavailableConsensusBlockHash,
46    InvalidStateRoot,
47    BalanceOverflow,
48    DomainTransfersTracking,
49    InvalidDomainTransfers,
50    OverwritingER,
51    RuntimeNotFound,
52    LastBlockNotFound,
53    UnmatchedNewHeadReceipt,
54}
55
56#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
57pub struct BlockTreeNode<Number, Hash, DomainNumber, DomainHash, Balance> {
58    /// The full ER for this block.
59    pub execution_receipt: ExecutionReceipt<Number, Hash, DomainNumber, DomainHash, Balance>,
60    /// A set of all operators who have committed to this ER within a bundle. Used to determine who to
61    /// slash if a fraudulent branch of the `block_tree` is pruned.
62    ///
63    /// NOTE: there may be duplicated operator id as an operator can submit multiple bundles with the
64    /// same head receipt to a consensus block.
65    pub operator_ids: Vec<OperatorId>,
66}
67
68#[derive(Debug, PartialEq, Eq)]
69pub(crate) enum AcceptedReceiptType {
70    // New head receipt that extend the longest branch
71    NewHead,
72    // Receipt that confirms the head receipt that added in the current block
73    CurrentHead,
74}
75
76#[derive(Debug, PartialEq, Eq)]
77pub(crate) enum RejectedReceiptType {
78    // Receipt that is newer than the head receipt but does not extend the head receipt
79    InFuture,
80    // Receipt that already been pruned
81    Pruned,
82    // Receipt that confirm a non-head receipt or head receipt of the previous block
83    Stale,
84    // Receipt that tries to create a new branch of the block tree
85    //
86    // The honests operator must submit fraud proof to prune the bad receipt at the
87    // same height before submitting the valid receipt.
88    NewBranch,
89}
90
91impl From<RejectedReceiptType> for Error {
92    fn from(rejected_receipt: RejectedReceiptType) -> Error {
93        match rejected_receipt {
94            RejectedReceiptType::InFuture => Error::InFutureReceipt,
95            RejectedReceiptType::Pruned => Error::PrunedReceipt,
96            RejectedReceiptType::Stale => Error::StaleReceipt,
97            RejectedReceiptType::NewBranch => Error::NewBranchReceipt,
98        }
99    }
100}
101
102/// The type of receipt regarding to its freshness
103#[derive(Debug, PartialEq, Eq)]
104pub(crate) enum ReceiptType {
105    Accepted(AcceptedReceiptType),
106    Rejected(RejectedReceiptType),
107}
108
109pub(crate) fn does_receipt_exists<T: Config>(
110    domain_id: DomainId,
111    domain_number: DomainBlockNumberFor<T>,
112    receipt_hash: ReceiptHashFor<T>,
113) -> bool {
114    BlockTree::<T>::get(domain_id, domain_number)
115        .map(|h| h == receipt_hash)
116        .unwrap_or(false)
117}
118
119/// Get the receipt type of the given receipt based on the current block tree state
120pub(crate) fn execution_receipt_type<T: Config>(
121    domain_id: DomainId,
122    execution_receipt: &ExecutionReceiptOf<T>,
123) -> ReceiptType {
124    let receipt_number = execution_receipt.domain_block_number;
125    let head_receipt_number = HeadReceiptNumber::<T>::get(domain_id);
126    let head_receipt_extended = NewAddedHeadReceipt::<T>::get(domain_id).is_some();
127    let next_receipt_number = head_receipt_number.saturating_add(One::one());
128    let latest_confirmed_domain_block_number =
129        Pallet::<T>::latest_confirmed_domain_block_number(domain_id);
130
131    match receipt_number.cmp(&next_receipt_number) {
132        Ordering::Greater => ReceiptType::Rejected(RejectedReceiptType::InFuture),
133        Ordering::Equal => {
134            // we do not allow consecutive ER in a single consensus block
135            // if the head receipt is already extended, then reject this ER
136            // as it is a Future ER
137            if head_receipt_extended {
138                ReceiptType::Rejected(RejectedReceiptType::InFuture)
139            } else {
140                ReceiptType::Accepted(AcceptedReceiptType::NewHead)
141            }
142        }
143        Ordering::Less => {
144            // Reject receipt that already confirmed
145            if !latest_confirmed_domain_block_number.is_zero()
146                && receipt_number <= latest_confirmed_domain_block_number
147            {
148                return ReceiptType::Rejected(RejectedReceiptType::Pruned);
149            }
150
151            // Reject receipt that try to create new branch in the block tree
152            let already_exist = does_receipt_exists::<T>(
153                domain_id,
154                receipt_number,
155                execution_receipt.hash::<DomainHashingFor<T>>(),
156            );
157            if !already_exist {
158                return ReceiptType::Rejected(RejectedReceiptType::NewBranch);
159            }
160
161            // Add confirm to the head receipt that added in the current block or it is
162            // the first genesis receipt
163            let is_first_genesis_receipt =
164                receipt_number.is_zero() && HeadDomainNumber::<T>::get(domain_id).is_zero();
165            if receipt_number == head_receipt_number
166                && (head_receipt_extended || is_first_genesis_receipt)
167            {
168                return ReceiptType::Accepted(AcceptedReceiptType::CurrentHead);
169            }
170
171            // Add confirm to a non-head receipt or head receipt of the previous block
172            ReceiptType::Rejected(RejectedReceiptType::Stale)
173        }
174    }
175}
176
177/// Verify the execution receipt
178pub(crate) fn verify_execution_receipt<T: Config>(
179    domain_id: DomainId,
180    execution_receipt: &ExecutionReceiptOf<T>,
181) -> Result<(), Error> {
182    let ExecutionReceipt {
183        consensus_block_number,
184        consensus_block_hash,
185        domain_block_number,
186        inboxed_bundles,
187        parent_domain_block_receipt_hash,
188        execution_trace,
189        execution_trace_root,
190        final_state_root,
191        ..
192    } = execution_receipt;
193
194    // Checking if the incoming ER is expected regarding to its `domain_block_number` or freshness
195    if let ReceiptType::Rejected(rejected_receipt_type) =
196        execution_receipt_type::<T>(domain_id, execution_receipt)
197    {
198        return Err(rejected_receipt_type.into());
199    }
200
201    // If there is new head receipt added in the current block, as long as the incoming
202    // receipt is the same as the new head receipt we can safely skip the following checks,
203    // if they are not the same, we just reject the incoming receipt and expecting a fraud
204    // proof will be submit if the new head receipt is fraudulent and then the incoming
205    // receipt will be re-submit.
206    if let Some(new_added_head_receipt) = NewAddedHeadReceipt::<T>::get(domain_id) {
207        ensure!(
208            new_added_head_receipt == execution_receipt.hash::<DomainHashingFor<T>>(),
209            Error::UnmatchedNewHeadReceipt,
210        );
211        return Ok(());
212    }
213
214    // The genesis receipt is generated and added to the block tree by the runtime upon domain
215    // instantiation thus it is unchallengeable, we can safely skip other checks as long as we
216    // can ensure it is always be the same.
217    if domain_block_number.is_zero() {
218        ensure!(
219            does_receipt_exists::<T>(
220                domain_id,
221                *domain_block_number,
222                execution_receipt.hash::<DomainHashingFor<T>>(),
223            ),
224            Error::BadGenesisReceipt
225        );
226        return Ok(());
227    }
228
229    // Check if the ER has at least 2 trace root (for Initialization and Finalization of block at least)
230    if execution_trace.len() < 2 {
231        return Err(Error::InvalidExecutionTrace);
232    }
233
234    let maybe_domain_runtime_upgraded_at = {
235        let runtime_id = Pallet::<T>::runtime_id(domain_id).ok_or(Error::RuntimeNotFound)?;
236        DomainRuntimeUpgradeRecords::<T>::get(runtime_id).remove(consensus_block_number)
237    };
238
239    // Check if the ER is derived from the correct consensus block in the current chain
240    let excepted_consensus_block_hash =
241        match ConsensusBlockHash::<T>::get(domain_id, consensus_block_number) {
242            Some(hash) => hash,
243            None => {
244                // The `initialize_block` of non-system pallets is skipped in the `validate_transaction`,
245                // thus the hash of best block, which is recorded in the this pallet's `on_initialize` hook,
246                // is unavailable at this point.
247                let parent_block_number =
248                    frame_system::Pallet::<T>::current_block_number() - One::one();
249                if *consensus_block_number == parent_block_number {
250                    frame_system::Pallet::<T>::parent_hash()
251
252                // The domain runtime upgrade is forced to happen even if there is no bundle, in this case,
253                // the `ConsensusBlockHash` will be empty so we need to get the consensus block hash from
254                // `DomainRuntimeUpgradeRecords`
255                } else if let Some(ref upgrade_entry) = maybe_domain_runtime_upgraded_at {
256                    upgrade_entry.at_hash
257                } else {
258                    return Err(Error::UnavailableConsensusBlockHash);
259                }
260            }
261        };
262    ensure!(
263        *consensus_block_hash == excepted_consensus_block_hash,
264        Error::BuiltOnUnknownConsensusBlock
265    );
266
267    // Check if the ER is derived from the expected inboxed bundles of the consensus block
268    let bundles_extrinsics_roots: Vec<_> =
269        inboxed_bundles.iter().map(|b| b.extrinsics_root).collect();
270    let execution_inbox =
271        ExecutionInbox::<T>::get((domain_id, domain_block_number, consensus_block_number));
272    let expected_extrinsics_roots: Vec<_> =
273        execution_inbox.iter().map(|b| b.extrinsics_root).collect();
274    ensure!(
275        (!bundles_extrinsics_roots.is_empty() || maybe_domain_runtime_upgraded_at.is_some())
276            && bundles_extrinsics_roots == expected_extrinsics_roots,
277        Error::InvalidExtrinsicsRoots
278    );
279
280    // Check if the `execution_trace_root` is well-format
281    let mut trace = Vec::with_capacity(execution_trace.len());
282    for root in execution_trace {
283        trace.push(
284            root.encode()
285                .try_into()
286                .map_err(|_| Error::InvalidTraceRoot)?,
287        );
288    }
289    let expected_execution_trace_root: sp_core::H256 = MerkleTree::from_leaves(trace.as_slice())
290        .root()
291        .ok_or(Error::InvalidTraceRoot)?
292        .into();
293    ensure!(
294        expected_execution_trace_root == *execution_trace_root,
295        Error::InvalidTraceRoot
296    );
297
298    // check state root on ER and in the Execution trace
299    if let Some(expected_final_state_root) = execution_trace.last() {
300        ensure!(
301            final_state_root == expected_final_state_root,
302            Error::InvalidStateRoot
303        );
304    }
305
306    // Check if the ER is extending an existing parent ER
307    if let Some(parent_block_number) = domain_block_number.checked_sub(&One::one()) {
308        let parent_block_exist = does_receipt_exists::<T>(
309            domain_id,
310            parent_block_number,
311            *parent_domain_block_receipt_hash,
312        );
313        ensure!(parent_block_exist, Error::UnknownParentBlockReceipt);
314    }
315
316    Ok(())
317}
318
319/// Details of the confirmed domain block such as operators, rewards they would receive.
320#[derive(Debug, PartialEq)]
321pub(crate) struct ConfirmedDomainBlockInfo<ConsensusNumber, DomainNumber, Balance> {
322    pub consensus_block_number: ConsensusNumber,
323    pub domain_block_number: DomainNumber,
324    pub operator_ids: Vec<OperatorId>,
325    pub rewards: Balance,
326    pub invalid_bundle_authors: Vec<OperatorId>,
327    pub total_storage_fee: Balance,
328    pub paid_bundle_storage_fees: BTreeMap<OperatorId, u32>,
329}
330
331pub(crate) type ProcessExecutionReceiptResult<T> = Result<
332    Option<ConfirmedDomainBlockInfo<BlockNumberFor<T>, DomainBlockNumberFor<T>, BalanceOf<T>>>,
333    Error,
334>;
335
336/// Process the execution receipt to add it to the block tree
337/// Returns the domain block number that was pruned, if any
338pub(crate) fn process_execution_receipt<T: Config>(
339    domain_id: DomainId,
340    submitter: OperatorId,
341    execution_receipt: ExecutionReceiptOf<T>,
342    receipt_type: AcceptedReceiptType,
343) -> ProcessExecutionReceiptResult<T> {
344    let er_hash = execution_receipt.hash::<DomainHashingFor<T>>();
345    let receipt_block_number = execution_receipt.domain_block_number;
346    match receipt_type {
347        AcceptedReceiptType::NewHead => {
348            add_new_receipt_to_block_tree::<T>(domain_id, submitter, execution_receipt)?;
349
350            // Update the head receipt number
351            HeadReceiptNumber::<T>::insert(domain_id, receipt_block_number);
352            NewAddedHeadReceipt::<T>::insert(domain_id, er_hash);
353
354            // Prune expired domain block
355            if let Some(to_prune) =
356                receipt_block_number.checked_sub(&T::BlockTreePruningDepth::get())
357            {
358                let BlockTreeNode {
359                    execution_receipt,
360                    operator_ids,
361                } = match prune_receipt::<T>(domain_id, to_prune)? {
362                    Some(n) => n,
363                    // The receipt at `to_prune` may already been pruned if there is fraud proof being
364                    // processed previously and the `HeadReceiptNumber` is reverted.
365                    None => return Ok(None),
366                };
367
368                // Collect the paid bundle storage fees and the invalid bundle author
369                let mut paid_bundle_storage_fees = BTreeMap::new();
370                let mut invalid_bundle_authors = Vec::new();
371                let bundle_digests = ExecutionInbox::<T>::get((
372                    domain_id,
373                    to_prune,
374                    execution_receipt.consensus_block_number,
375                ));
376                for (index, bd) in bundle_digests.into_iter().enumerate() {
377                    if let Some(bundle_author) = InboxedBundleAuthor::<T>::take(bd.header_hash) {
378                        // It is okay to index `ER::bundles` here since `verify_execution_receipt` have checked
379                        // the `ER::bundles` have the same length of `ExecutionInbox`
380                        if execution_receipt.inboxed_bundles[index].is_invalid() {
381                            invalid_bundle_authors.push(bundle_author);
382                        } else {
383                            paid_bundle_storage_fees
384                                .entry(bundle_author)
385                                .and_modify(|s| *s += bd.size)
386                                .or_insert(bd.size);
387                        }
388                    }
389                }
390
391                // Remove the block's `ExecutionInbox` as the domain block is confirmed and no need to verify
392                // its receipt's `extrinsics_root` anymore.
393                let _ = ExecutionInbox::<T>::clear_prefix((domain_id, to_prune), u32::MAX, None);
394
395                LatestConfirmedDomainExecutionReceipt::<T>::insert(
396                    domain_id,
397                    execution_receipt.clone(),
398                );
399
400                ConsensusBlockHash::<T>::remove(
401                    domain_id,
402                    execution_receipt.consensus_block_number,
403                );
404
405                let block_fees = execution_receipt
406                    .block_fees
407                    .total_fees()
408                    .ok_or(Error::BalanceOverflow)?;
409
410                ensure!(
411                    execution_receipt
412                        .transfers
413                        .is_valid(ChainId::Domain(domain_id)),
414                    Error::InvalidDomainTransfers
415                );
416
417                // TODO: remove skipping domain balance checks for domain 0
418                //  once https://github.com/autonomys/subspace/issues/3466
419                //  is resolved. We need the issue to be resolved before phase 2 launch
420                if !SkipBalanceChecks::<T>::get().contains(&domain_id) {
421                    update_domain_transfers::<T>(
422                        domain_id,
423                        &execution_receipt.transfers,
424                        block_fees,
425                    )
426                    .map_err(|_| Error::DomainTransfersTracking)?;
427                }
428
429                update_domain_runtime_upgrade_records::<T>(
430                    domain_id,
431                    execution_receipt.consensus_block_number,
432                )?;
433
434                // handle chain rewards from the domain
435                execution_receipt
436                    .block_fees
437                    .chain_rewards
438                    .into_iter()
439                    .for_each(|(chain_id, reward)| {
440                        T::OnChainRewards::on_chain_rewards(chain_id, reward)
441                    });
442
443                return Ok(Some(ConfirmedDomainBlockInfo {
444                    consensus_block_number: execution_receipt.consensus_block_number,
445                    domain_block_number: to_prune,
446                    operator_ids,
447                    rewards: execution_receipt.block_fees.domain_execution_fee,
448                    invalid_bundle_authors,
449                    total_storage_fee: execution_receipt.block_fees.consensus_storage_fee,
450                    paid_bundle_storage_fees,
451                }));
452            }
453        }
454        AcceptedReceiptType::CurrentHead => {
455            // Add confirmation to the current head receipt
456            BlockTreeNodes::<T>::mutate(er_hash, |maybe_node| {
457                let node = maybe_node.as_mut().expect(
458                    "The domain block of `CurrentHead` receipt is checked to be exist in `execution_receipt_type`; qed"
459                );
460                node.operator_ids.push(submitter);
461            });
462        }
463    }
464
465    // Update the `LatestSubmittedER` for the operator
466    let key = (domain_id, submitter);
467    if receipt_block_number > Pallet::<T>::latest_submitted_er(key) {
468        LatestSubmittedER::<T>::insert(key, receipt_block_number)
469    }
470
471    Ok(None)
472}
473
474type TransferTrackerError<T> =
475    <<T as Config>::DomainsTransfersTracker as DomainsTransfersTracker<BalanceOf<T>>>::Error;
476
477/// Updates domain transfers for following scenarios
478/// 1. Block fees are burned on domain
479/// 2. Confirming incoming XDM transfers to the Domain
480/// 3. Noting outgoing transfers from the domain
481/// 4. Cancelling outgoing transfers from the domain.
482fn update_domain_transfers<T: Config>(
483    domain_id: DomainId,
484    transfers: &Transfers<BalanceOf<T>>,
485    block_fees: BalanceOf<T>,
486) -> Result<(), TransferTrackerError<T>> {
487    let Transfers {
488        transfers_in,
489        transfers_out,
490        transfers_rejected,
491        rejected_transfers_claimed,
492    } = transfers;
493
494    // confirm incoming transfers
495    let er_chain_id = ChainId::Domain(domain_id);
496    transfers_in
497        .iter()
498        .try_for_each(|(from_chain_id, amount)| {
499            T::DomainsTransfersTracker::confirm_transfer(*from_chain_id, er_chain_id, *amount)
500        })?;
501
502    // note outgoing transfers
503    transfers_out.iter().try_for_each(|(to_chain_id, amount)| {
504        T::DomainsTransfersTracker::note_transfer(er_chain_id, *to_chain_id, *amount)
505    })?;
506
507    // note rejected transfers
508    transfers_rejected
509        .iter()
510        .try_for_each(|(from_chain_id, amount)| {
511            T::DomainsTransfersTracker::reject_transfer(*from_chain_id, er_chain_id, *amount)
512        })?;
513
514    // claim rejected transfers
515    rejected_transfers_claimed
516        .iter()
517        .try_for_each(|(to_chain_id, amount)| {
518            T::DomainsTransfersTracker::claim_rejected_transfer(er_chain_id, *to_chain_id, *amount)
519        })?;
520
521    // deduct execution fees from domain
522    T::DomainsTransfersTracker::reduce_domain_balance(domain_id, block_fees)?;
523
524    Ok(())
525}
526
527// Update the domain runtime upgrade record at `consensus_number` if there is one
528fn update_domain_runtime_upgrade_records<T: Config>(
529    domain_id: DomainId,
530    consensus_number: BlockNumberFor<T>,
531) -> Result<(), Error> {
532    let runtime_id = Pallet::<T>::runtime_id(domain_id).ok_or(Error::RuntimeNotFound)?;
533    let mut domain_runtime_upgrade_records = DomainRuntimeUpgradeRecords::<T>::get(runtime_id);
534
535    if let Some(upgrade_entry) = domain_runtime_upgrade_records.get_mut(&consensus_number) {
536        // Decrease the `reference_count` by one and remove the whole entry if it drop to zero
537        if upgrade_entry.reference_count > One::one() {
538            upgrade_entry.reference_count =
539                upgrade_entry.reference_count.saturating_sub(One::one());
540        } else {
541            domain_runtime_upgrade_records.remove(&consensus_number);
542        }
543
544        if !domain_runtime_upgrade_records.is_empty() {
545            DomainRuntimeUpgradeRecords::<T>::set(runtime_id, domain_runtime_upgrade_records);
546        } else {
547            DomainRuntimeUpgradeRecords::<T>::remove(runtime_id);
548        }
549    }
550    Ok(())
551}
552
553fn add_new_receipt_to_block_tree<T: Config>(
554    domain_id: DomainId,
555    submitter: OperatorId,
556    execution_receipt: ExecutionReceiptOf<T>,
557) -> Result<(), Error> {
558    // Construct and add a new domain block to the block tree
559    let er_hash = execution_receipt.hash::<DomainHashingFor<T>>();
560    let domain_block_number = execution_receipt.domain_block_number;
561
562    ensure!(
563        !BlockTree::<T>::contains_key(domain_id, domain_block_number),
564        Error::OverwritingER,
565    );
566
567    BlockTree::<T>::insert(domain_id, domain_block_number, er_hash);
568    let block_tree_node = BlockTreeNode {
569        execution_receipt,
570        operator_ids: sp_std::vec![submitter],
571    };
572    BlockTreeNodes::<T>::insert(er_hash, block_tree_node);
573
574    Ok(())
575}
576
577/// Import the genesis receipt to the block tree
578pub(crate) fn import_genesis_receipt<T: Config>(
579    domain_id: DomainId,
580    genesis_receipt: ExecutionReceiptOf<T>,
581) {
582    let er_hash = genesis_receipt.hash::<DomainHashingFor<T>>();
583    let domain_block_number = genesis_receipt.domain_block_number;
584
585    LatestConfirmedDomainExecutionReceipt::<T>::insert(domain_id, genesis_receipt.clone());
586    DomainGenesisBlockExecutionReceipt::<T>::insert(domain_id, genesis_receipt.clone());
587
588    let block_tree_node = BlockTreeNode {
589        execution_receipt: genesis_receipt,
590        operator_ids: sp_std::vec![],
591    };
592    // NOTE: no need to update the head receipt number as `HeadReceiptNumber` is using `ValueQuery`
593    BlockTree::<T>::insert(domain_id, domain_block_number, er_hash);
594    BlockTreeNodes::<T>::insert(er_hash, block_tree_node);
595}
596
597pub(crate) fn prune_receipt<T: Config>(
598    domain_id: DomainId,
599    receipt_number: DomainBlockNumberFor<T>,
600) -> Result<Option<BlockTreeNodeFor<T>>, Error> {
601    let receipt_hash = match BlockTree::<T>::take(domain_id, receipt_number) {
602        Some(er_hash) => er_hash,
603        None => return Ok(None),
604    };
605    let block_tree_node =
606        BlockTreeNodes::<T>::take(receipt_hash).ok_or(Error::MissingDomainBlock)?;
607
608    // If the pruned ER is the operator's `latest_submitted_er` for this domain, it means either:
609    //
610    // - All the ER the operator submitted for this domain are confirmed and pruned, so the operator
611    //   can't be targeted by fraud proof later unless it submit other new ERs.
612    //
613    // - All the bad ER the operator submitted for this domain are pruned and the operator is already
614    //   slashed, so wwe don't need `LatestSubmittedER` to determine if the operator is pending slash.
615    //
616    // In both cases, it is safe to remove the `LatestSubmittedER` for the operator in this domain
617    for operator_id in block_tree_node.operator_ids.iter() {
618        let key = (domain_id, operator_id);
619        let latest_submitted_er = Pallet::<T>::latest_submitted_er(key);
620        if block_tree_node.execution_receipt.domain_block_number == latest_submitted_er {
621            LatestSubmittedER::<T>::remove(key);
622        }
623    }
624
625    Ok(Some(block_tree_node))
626}
627
628#[cfg(test)]
629mod tests {
630    use super::*;
631    use crate::tests::{
632        BlockTreePruningDepth, Domains, Test, create_dummy_bundle_with_receipts,
633        create_dummy_receipt, extend_block_tree, extend_block_tree_from_zero,
634        get_block_tree_node_at, new_test_ext_with_extensions, register_genesis_domain,
635        run_to_block,
636    };
637    use crate::{FrozenDomains, RawOrigin as DomainOrigin};
638    use frame_support::dispatch::RawOrigin;
639    use frame_support::{assert_err, assert_ok};
640    use frame_system::Origin;
641    use sp_core::H256;
642    use sp_domains::{BundleDigest, InboxedBundle, InvalidBundleType};
643
644    #[test]
645    fn test_genesis_receipt() {
646        let mut ext = new_test_ext_with_extensions();
647        ext.execute_with(|| {
648            let domain_id = register_genesis_domain(0u128, vec![0u64]);
649
650            // The genesis receipt should be added to the block tree
651            let block_tree_node_at_0 = BlockTree::<Test>::get(domain_id, 0).unwrap();
652
653            let genesis_node = BlockTreeNodes::<Test>::get(block_tree_node_at_0).unwrap();
654            assert!(genesis_node.operator_ids.is_empty());
655            assert_eq!(HeadReceiptNumber::<Test>::get(domain_id), 0);
656
657            // The genesis receipt should be able pass the verification and is unchallengeable
658            let genesis_receipt = genesis_node.execution_receipt;
659            let invalid_genesis_receipt = {
660                let mut receipt = genesis_receipt.clone();
661                receipt.final_state_root = H256::random();
662                receipt
663            };
664            assert_ok!(verify_execution_receipt::<Test>(
665                domain_id,
666                &genesis_receipt
667            ));
668            // Submitting an invalid genesis ER will result in `NewBranchReceipt` because the operator
669            // need to submit fraud proof to pruned a ER first before submitting an ER at the same height
670            assert_err!(
671                verify_execution_receipt::<Test>(domain_id, &invalid_genesis_receipt),
672                Error::NewBranchReceipt
673            );
674        });
675    }
676
677    #[test]
678    fn test_new_head_receipt() {
679        let creator = 0u128;
680        let operator_id = 1u64;
681        let block_tree_pruning_depth = <Test as Config>::BlockTreePruningDepth::get();
682
683        let mut ext = new_test_ext_with_extensions();
684        ext.execute_with(|| {
685            let domain_id = register_genesis_domain(creator, vec![operator_id]);
686
687            // The genesis node of the block tree
688            let genesis_node = get_block_tree_node_at::<Test>(domain_id, 0).unwrap();
689            let mut receipt = genesis_node.execution_receipt;
690            assert_eq!(
691                receipt.consensus_block_number,
692                frame_system::Pallet::<Test>::current_block_number()
693            );
694            let mut receipt_of_block_1 = None;
695            let mut bundle_header_hash_of_block_1 = None;
696            for block_number in 1..=(block_tree_pruning_depth + 3) {
697                // Finilize parent block and initialize block at `block_number`
698                run_to_block::<Test>(block_number, receipt.consensus_block_hash);
699
700                if block_number != 1 {
701                    // `ConsensusBlockHash` should be set to `Some` since last consensus block contains bundle
702                    assert_eq!(
703                        ConsensusBlockHash::<Test>::get(domain_id, block_number - 1),
704                        Some(frame_system::Pallet::<Test>::block_hash(block_number - 1))
705                    );
706                    // ER point to last consensus block should have `NewHead` type
707                    assert_eq!(
708                        execution_receipt_type::<Test>(domain_id, &receipt),
709                        ReceiptType::Accepted(AcceptedReceiptType::NewHead)
710                    );
711                    assert_ok!(verify_execution_receipt::<Test>(domain_id, &receipt));
712                }
713
714                // Submit a bundle with the receipt of the last block
715                let bundle_extrinsics_root = H256::random();
716                let bundle = create_dummy_bundle_with_receipts(
717                    domain_id,
718                    operator_id,
719                    bundle_extrinsics_root,
720                    receipt,
721                );
722                let bundle_header_hash = bundle.sealed_header.pre_hash();
723                let bundle_size = bundle.size();
724                assert_ok!(crate::Pallet::<Test>::submit_bundle(
725                    DomainOrigin::ValidatedUnsigned.into(),
726                    bundle,
727                ));
728                // `bundle_extrinsics_root` should be tracked in `ExecutionInbox`
729                assert_eq!(
730                    ExecutionInbox::<Test>::get((domain_id, block_number, block_number)),
731                    vec![BundleDigest {
732                        header_hash: bundle_header_hash,
733                        extrinsics_root: bundle_extrinsics_root,
734                        size: bundle_size,
735                    }]
736                );
737                assert!(InboxedBundleAuthor::<Test>::contains_key(
738                    bundle_header_hash
739                ));
740
741                // Head receipt number should be updated
742                let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
743                assert_eq!(head_receipt_number, block_number - 1);
744
745                // As we only extending the block tree there should be no fork
746                let parent_domain_block_receipt =
747                    BlockTree::<Test>::get(domain_id, head_receipt_number).unwrap();
748
749                // The submitter should be added to `operator_ids`
750                let parent_node = BlockTreeNodes::<Test>::get(parent_domain_block_receipt).unwrap();
751                assert_eq!(parent_node.operator_ids.len(), 1);
752                assert_eq!(parent_node.operator_ids[0], operator_id);
753
754                // Construct a `NewHead` receipt of the just submitted bundle, which will be included
755                // in the next bundle
756                receipt = create_dummy_receipt(
757                    block_number,
758                    H256::random(),
759                    parent_domain_block_receipt,
760                    vec![bundle_extrinsics_root],
761                );
762
763                // Record receipt of block #1 for later use
764                if block_number == 1 {
765                    receipt_of_block_1.replace(receipt.clone());
766                    bundle_header_hash_of_block_1.replace(bundle_header_hash);
767                }
768            }
769
770            // The receipt of the block 1 is pruned at the last iteration, verify it will result in
771            // `PrunedReceipt` error
772            let pruned_receipt = receipt_of_block_1.unwrap();
773            let pruned_bundle = bundle_header_hash_of_block_1.unwrap();
774            assert!(BlockTree::<Test>::get(domain_id, 1).is_none());
775            assert!(ExecutionInbox::<Test>::get((domain_id, 1, 1)).is_empty());
776            assert!(!InboxedBundleAuthor::<Test>::contains_key(pruned_bundle));
777            assert_eq!(
778                execution_receipt_type::<Test>(domain_id, &pruned_receipt),
779                ReceiptType::Rejected(RejectedReceiptType::Pruned)
780            );
781            assert_err!(
782                verify_execution_receipt::<Test>(domain_id, &pruned_receipt),
783                Error::PrunedReceipt
784            );
785            assert!(
786                ConsensusBlockHash::<Test>::get(domain_id, pruned_receipt.consensus_block_number,)
787                    .is_none()
788            );
789        });
790    }
791
792    #[test]
793    fn test_confirm_current_head_receipt() {
794        let creator = 0u128;
795        let operator_id1 = 1u64;
796        let operator_id2 = 2u64;
797        let mut ext = new_test_ext_with_extensions();
798        ext.execute_with(|| {
799            let domain_id = register_genesis_domain(creator, vec![operator_id1, operator_id2]);
800            let next_head_receipt = extend_block_tree_from_zero(domain_id, operator_id1, 3);
801
802            // Submit the new head receipt
803            assert_eq!(
804                execution_receipt_type::<Test>(domain_id, &next_head_receipt),
805                ReceiptType::Accepted(AcceptedReceiptType::NewHead)
806            );
807            assert_ok!(verify_execution_receipt::<Test>(
808                domain_id,
809                &next_head_receipt
810            ));
811            let bundle = create_dummy_bundle_with_receipts(
812                domain_id,
813                operator_id1,
814                H256::random(),
815                next_head_receipt.clone(),
816            );
817            assert_ok!(crate::Pallet::<Test>::submit_bundle(
818                DomainOrigin::ValidatedUnsigned.into(),
819                bundle,
820            ));
821
822            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
823            let current_head_receipt =
824                get_block_tree_node_at::<Test>(domain_id, head_receipt_number)
825                    .unwrap()
826                    .execution_receipt;
827
828            // Now `next_head_receipt` become the head receipt
829            assert_eq!(next_head_receipt, current_head_receipt);
830
831            // Head receipt added in the current block is consider valid
832            assert_eq!(
833                execution_receipt_type::<Test>(domain_id, &current_head_receipt),
834                ReceiptType::Accepted(AcceptedReceiptType::CurrentHead)
835            );
836            assert_ok!(verify_execution_receipt::<Test>(
837                domain_id,
838                &current_head_receipt
839            ));
840
841            // Re-submit the head receipt by a different operator is okay
842            let bundle = create_dummy_bundle_with_receipts(
843                domain_id,
844                operator_id2,
845                H256::random(),
846                current_head_receipt,
847            );
848            assert_ok!(crate::Pallet::<Test>::submit_bundle(
849                DomainOrigin::ValidatedUnsigned.into(),
850                bundle,
851            ));
852
853            let head_node = get_block_tree_node_at::<Test>(domain_id, head_receipt_number).unwrap();
854            assert_eq!(head_node.operator_ids, vec![operator_id1, operator_id2]);
855        });
856    }
857
858    #[test]
859    fn test_non_head_receipt() {
860        let creator = 0u128;
861        let operator_id1 = 1u64;
862        let operator_id2 = 2u64;
863        let mut ext = new_test_ext_with_extensions();
864        ext.execute_with(|| {
865            let domain_id = register_genesis_domain(creator, vec![operator_id1, operator_id2]);
866            extend_block_tree_from_zero(domain_id, operator_id1, 3);
867
868            // Receipt that confirm a non-head receipt is stale receipt
869            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
870            let stale_receipt = get_block_tree_node_at::<Test>(domain_id, head_receipt_number - 1)
871                .unwrap()
872                .execution_receipt;
873            let stale_receipt_hash = stale_receipt.hash::<DomainHashingFor<Test>>();
874
875            // Stale receipt can pass the verification
876            assert_eq!(
877                execution_receipt_type::<Test>(domain_id, &stale_receipt),
878                ReceiptType::Rejected(RejectedReceiptType::Stale)
879            );
880            assert_err!(
881                verify_execution_receipt::<Test>(domain_id, &stale_receipt),
882                Error::StaleReceipt
883            );
884
885            // Stale receipt will be rejected and won't be added to the block tree
886            let bundle = create_dummy_bundle_with_receipts(
887                domain_id,
888                operator_id2,
889                H256::random(),
890                stale_receipt,
891            );
892            assert!(crate::Pallet::<Test>::submit_bundle(RawOrigin::None.into(), bundle).is_err());
893
894            assert_eq!(
895                BlockTreeNodes::<Test>::get(stale_receipt_hash)
896                    .unwrap()
897                    .operator_ids,
898                vec![operator_id1]
899            );
900        });
901    }
902
903    #[test]
904    fn test_previous_head_receipt() {
905        let creator = 0u128;
906        let operator_id1 = 1u64;
907        let operator_id2 = 2u64;
908        let mut ext = new_test_ext_with_extensions();
909        ext.execute_with(|| {
910            let domain_id = register_genesis_domain(creator, vec![operator_id1, operator_id2]);
911            extend_block_tree_from_zero(domain_id, operator_id1, 3);
912
913            // No new receipt submitted in current block
914            assert!(NewAddedHeadReceipt::<Test>::get(domain_id).is_none());
915
916            // Receipt that confirm a head receipt of the previous block is stale receipt
917            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
918            let previous_head_receipt =
919                get_block_tree_node_at::<Test>(domain_id, head_receipt_number)
920                    .unwrap()
921                    .execution_receipt;
922
923            // Stale receipt can not pass the verification
924            assert_eq!(
925                execution_receipt_type::<Test>(domain_id, &previous_head_receipt),
926                ReceiptType::Rejected(RejectedReceiptType::Stale)
927            );
928            assert_err!(
929                verify_execution_receipt::<Test>(domain_id, &previous_head_receipt),
930                Error::StaleReceipt
931            );
932
933            // Stale receipt will be rejected and won't be added to the block tree
934            let bundle = create_dummy_bundle_with_receipts(
935                domain_id,
936                operator_id2,
937                H256::random(),
938                previous_head_receipt,
939            );
940            assert!(crate::Pallet::<Test>::submit_bundle(RawOrigin::None.into(), bundle).is_err());
941        });
942    }
943
944    #[test]
945    fn test_new_branch_receipt() {
946        let creator = 0u128;
947        let operator_id1 = 1u64;
948        let operator_id2 = 2u64;
949        let mut ext = new_test_ext_with_extensions();
950        ext.execute_with(|| {
951            let domain_id = register_genesis_domain(creator, vec![operator_id1, operator_id2]);
952            extend_block_tree_from_zero(domain_id, operator_id1, 3);
953
954            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
955            assert!(BlockTree::<Test>::get(domain_id, head_receipt_number).is_some());
956
957            // Construct new branch receipt that fork away from an existing node of
958            // the block tree
959            let new_branch_receipt = {
960                let mut head_receipt =
961                    get_block_tree_node_at::<Test>(domain_id, head_receipt_number)
962                        .unwrap()
963                        .execution_receipt;
964                head_receipt.final_state_root = H256::random();
965                head_receipt
966            };
967            let new_branch_receipt_hash = new_branch_receipt.hash::<DomainHashingFor<Test>>();
968
969            // New branch receipt can pass the verification
970            assert_eq!(
971                execution_receipt_type::<Test>(domain_id, &new_branch_receipt),
972                ReceiptType::Rejected(RejectedReceiptType::NewBranch)
973            );
974            assert_err!(
975                verify_execution_receipt::<Test>(domain_id, &new_branch_receipt),
976                Error::NewBranchReceipt
977            );
978
979            // Submit the new branch receipt will will be rejected
980            let bundle = create_dummy_bundle_with_receipts(
981                domain_id,
982                operator_id2,
983                H256::random(),
984                new_branch_receipt,
985            );
986            assert!(crate::Pallet::<Test>::submit_bundle(RawOrigin::None.into(), bundle).is_err());
987            assert!(BlockTreeNodes::<Test>::get(new_branch_receipt_hash).is_none());
988        });
989    }
990
991    #[test]
992    fn test_prune_domain_execution_receipt() {
993        let creator = 0u128;
994        let operator_id = 1u64;
995        let mut ext = new_test_ext_with_extensions();
996        ext.execute_with(|| {
997            let domain_id = register_genesis_domain(creator, vec![operator_id]);
998            let _next_receipt = extend_block_tree_from_zero(domain_id, operator_id, 3);
999            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
1000
1001            // freeze domain
1002            assert!(!FrozenDomains::<Test>::get().contains(&domain_id));
1003            Domains::freeze_domain(Origin::<Test>::Root.into(), domain_id).unwrap();
1004            assert!(FrozenDomains::<Test>::get().contains(&domain_id));
1005
1006            // prune execution recept
1007            let head_receipt_hash = BlockTree::<Test>::get(domain_id, head_receipt_number).unwrap();
1008            Domains::prune_domain_execution_receipt(
1009                Origin::<Test>::Root.into(),
1010                domain_id,
1011                head_receipt_hash,
1012            )
1013            .unwrap();
1014            assert_eq!(
1015                HeadReceiptNumber::<Test>::get(domain_id),
1016                head_receipt_number - 1
1017            );
1018
1019            // unfreeze domain
1020            Domains::unfreeze_domain(Origin::<Test>::Root.into(), domain_id).unwrap();
1021            assert!(!FrozenDomains::<Test>::get().contains(&domain_id));
1022        })
1023    }
1024
1025    #[test]
1026    fn test_invalid_receipt() {
1027        let creator = 0u128;
1028        let operator_id = 1u64;
1029        let mut ext = new_test_ext_with_extensions();
1030        ext.execute_with(|| {
1031            let domain_id = register_genesis_domain(creator, vec![operator_id]);
1032            let next_receipt = extend_block_tree_from_zero(domain_id, operator_id, 3);
1033            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
1034
1035            // Construct a future receipt
1036            let mut future_receipt = next_receipt.clone();
1037            future_receipt.domain_block_number = head_receipt_number + 2;
1038            future_receipt.consensus_block_number = head_receipt_number as u32 + 2;
1039            ExecutionInbox::<Test>::insert(
1040                (
1041                    domain_id,
1042                    future_receipt.domain_block_number,
1043                    future_receipt.consensus_block_number,
1044                ),
1045                future_receipt
1046                    .inboxed_bundles
1047                    .clone()
1048                    .into_iter()
1049                    .map(|b| BundleDigest {
1050                        header_hash: H256::random(),
1051                        extrinsics_root: b.extrinsics_root,
1052                        size: 0,
1053                    })
1054                    .collect::<Vec<_>>(),
1055            );
1056            assert_eq!(
1057                execution_receipt_type::<Test>(domain_id, &future_receipt),
1058                ReceiptType::Rejected(RejectedReceiptType::InFuture)
1059            );
1060            assert_err!(
1061                verify_execution_receipt::<Test>(domain_id, &future_receipt),
1062                Error::InFutureReceipt
1063            );
1064
1065            // Receipt with unknown extrinsics roots
1066            let mut unknown_extrinsics_roots_receipt = next_receipt.clone();
1067            unknown_extrinsics_roots_receipt.inboxed_bundles =
1068                vec![InboxedBundle::valid(H256::random(), H256::random())];
1069            assert_err!(
1070                verify_execution_receipt::<Test>(domain_id, &unknown_extrinsics_roots_receipt),
1071                Error::InvalidExtrinsicsRoots
1072            );
1073
1074            // Receipt with unknown consensus block hash
1075            let mut unknown_consensus_block_receipt = next_receipt.clone();
1076            unknown_consensus_block_receipt.consensus_block_hash = H256::random();
1077            assert_err!(
1078                verify_execution_receipt::<Test>(domain_id, &unknown_consensus_block_receipt),
1079                Error::BuiltOnUnknownConsensusBlock
1080            );
1081
1082            // Receipt with unknown parent receipt
1083            let mut unknown_parent_receipt = next_receipt.clone();
1084            unknown_parent_receipt.parent_domain_block_receipt_hash = H256::random();
1085            assert_err!(
1086                verify_execution_receipt::<Test>(domain_id, &unknown_parent_receipt),
1087                Error::UnknownParentBlockReceipt
1088            );
1089
1090            // Receipt with execution_trace length less than two
1091            let mut invalid_execution_trace_receipt = next_receipt;
1092
1093            // Receipt with only one element in execution trace vector
1094            invalid_execution_trace_receipt.execution_trace = vec![
1095                invalid_execution_trace_receipt
1096                    .execution_trace
1097                    .first()
1098                    .cloned()
1099                    .expect("First element should be there; qed"),
1100            ];
1101            assert_err!(
1102                verify_execution_receipt::<Test>(domain_id, &invalid_execution_trace_receipt),
1103                Error::InvalidExecutionTrace
1104            );
1105
1106            // Receipt with zero element in execution trace vector
1107            invalid_execution_trace_receipt.execution_trace = vec![];
1108            assert_err!(
1109                verify_execution_receipt::<Test>(domain_id, &invalid_execution_trace_receipt),
1110                Error::InvalidExecutionTrace
1111            );
1112        });
1113    }
1114
1115    #[test]
1116    fn test_invalid_receipt_with_head_receipt_already_extended() {
1117        let creator = 0u128;
1118        let operator_id = 1u64;
1119        let mut ext = new_test_ext_with_extensions();
1120        ext.execute_with(|| {
1121            let domain_id = register_genesis_domain(creator, vec![operator_id]);
1122            let next_receipt = extend_block_tree_from_zero(domain_id, operator_id, 3);
1123            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
1124
1125            // reject extending receipt if the HeadReceiptNumber is already extended
1126            assert!(NewAddedHeadReceipt::<Test>::get(domain_id).is_none());
1127            NewAddedHeadReceipt::<Test>::set(domain_id, Some(H256::random()));
1128
1129            // Construct a future receipt
1130            let mut future_receipt = next_receipt.clone();
1131            future_receipt.domain_block_number = head_receipt_number + 1;
1132            future_receipt.consensus_block_number = head_receipt_number as u32 + 1;
1133
1134            ExecutionInbox::<Test>::insert(
1135                (
1136                    domain_id,
1137                    future_receipt.domain_block_number,
1138                    future_receipt.consensus_block_number,
1139                ),
1140                future_receipt
1141                    .inboxed_bundles
1142                    .clone()
1143                    .into_iter()
1144                    .map(|b| BundleDigest {
1145                        header_hash: H256::random(),
1146                        extrinsics_root: b.extrinsics_root,
1147                        size: 0,
1148                    })
1149                    .collect::<Vec<_>>(),
1150            );
1151            assert_eq!(
1152                execution_receipt_type::<Test>(domain_id, &future_receipt),
1153                ReceiptType::Rejected(RejectedReceiptType::InFuture)
1154            );
1155            assert_err!(
1156                verify_execution_receipt::<Test>(domain_id, &future_receipt),
1157                Error::InFutureReceipt
1158            );
1159        });
1160    }
1161
1162    #[test]
1163    fn test_invalid_trace_root_receipt() {
1164        let creator = 0u128;
1165        let operator_id1 = 1u64;
1166        let operator_id2 = 2u64;
1167        let mut ext = new_test_ext_with_extensions();
1168        ext.execute_with(|| {
1169            let domain_id = register_genesis_domain(creator, vec![operator_id1, operator_id2]);
1170            let mut next_receipt = extend_block_tree_from_zero(domain_id, operator_id1, 3);
1171            next_receipt.execution_trace.push(H256::random());
1172            next_receipt.final_state_root = *next_receipt.execution_trace.last().unwrap();
1173
1174            let mut trace = Vec::with_capacity(next_receipt.execution_trace.len());
1175            for root in &next_receipt.execution_trace {
1176                trace.push(
1177                    root.encode()
1178                        .try_into()
1179                        .map_err(|_| Error::InvalidTraceRoot)
1180                        .expect("H256 to Blake3Hash should be successful; qed"),
1181                );
1182            }
1183            let new_execution_trace_root = MerkleTree::from_leaves(trace.as_slice())
1184                .root()
1185                .expect("Compute merkle root of trace should success")
1186                .into();
1187            next_receipt.execution_trace_root = new_execution_trace_root;
1188            assert_ok!(verify_execution_receipt::<Test>(domain_id, &next_receipt));
1189
1190            // Receipt with wrong value of `execution_trace_root`
1191            let mut invalid_receipt = next_receipt.clone();
1192            invalid_receipt.execution_trace_root = H256::random();
1193            assert_err!(
1194                verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
1195                Error::InvalidTraceRoot
1196            );
1197
1198            // Receipt with wrong value of trace
1199            let mut invalid_receipt = next_receipt.clone();
1200            invalid_receipt.execution_trace[0] = H256::random();
1201            assert_err!(
1202                verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
1203                Error::InvalidTraceRoot
1204            );
1205
1206            // Receipt with additional trace
1207            let mut invalid_receipt = next_receipt.clone();
1208            invalid_receipt.execution_trace.push(H256::random());
1209            assert_err!(
1210                verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
1211                Error::InvalidTraceRoot
1212            );
1213
1214            // Receipt with missing trace
1215            let mut invalid_receipt = next_receipt;
1216            invalid_receipt.execution_trace.pop();
1217            assert_err!(
1218                verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
1219                Error::InvalidTraceRoot
1220            );
1221        });
1222    }
1223
1224    #[test]
1225    fn test_collect_invalid_bundle_author() {
1226        let creator = 0u128;
1227        let challenge_period = BlockTreePruningDepth::get();
1228        let operator_set: Vec<_> = (1..15).collect();
1229        let mut ext = new_test_ext_with_extensions();
1230        ext.execute_with(|| {
1231            let domain_id = register_genesis_domain(creator, operator_set.clone());
1232            let next_receipt = extend_block_tree_from_zero(domain_id, operator_set[0], 3);
1233
1234            // Submit bundle for every operator
1235            for operator_id in operator_set.iter() {
1236                let bundle = create_dummy_bundle_with_receipts(
1237                    domain_id,
1238                    *operator_id,
1239                    H256::random(),
1240                    next_receipt.clone(),
1241                );
1242                assert_ok!(crate::Pallet::<Test>::submit_bundle(
1243                    DomainOrigin::ValidatedUnsigned.into(),
1244                    bundle,
1245                ));
1246            }
1247            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
1248            let head_node = get_block_tree_node_at::<Test>(domain_id, head_receipt_number).unwrap();
1249            assert_eq!(head_node.operator_ids, operator_set);
1250
1251            // Get the `bundles_extrinsics_roots` that contains all the submitted bundles
1252            let current_block_number = frame_system::Pallet::<Test>::current_block_number();
1253            let execution_inbox = ExecutionInbox::<Test>::get((
1254                domain_id,
1255                current_block_number,
1256                current_block_number,
1257            ));
1258            let bundles_extrinsics_roots: Vec<_> = execution_inbox
1259                .into_iter()
1260                .map(|b| b.extrinsics_root)
1261                .collect();
1262            assert_eq!(bundles_extrinsics_roots.len(), operator_set.len());
1263
1264            // Prepare the invalid bundles and invalid bundle authors
1265            let mut bundles = vec![];
1266            let mut invalid_bundle_authors = vec![];
1267            for (i, (operator, extrinsics_root)) in operator_set
1268                .iter()
1269                .zip(bundles_extrinsics_roots)
1270                .enumerate()
1271            {
1272                if i % 2 == 0 {
1273                    invalid_bundle_authors.push(*operator);
1274                    bundles.push(InboxedBundle::invalid(
1275                        InvalidBundleType::OutOfRangeTx(0),
1276                        extrinsics_root,
1277                    ));
1278                } else {
1279                    bundles.push(InboxedBundle::valid(H256::random(), extrinsics_root));
1280                }
1281            }
1282            let mut target_receipt = create_dummy_receipt(
1283                current_block_number,
1284                H256::random(),
1285                next_receipt.hash::<DomainHashingFor<Test>>(),
1286                vec![],
1287            );
1288            target_receipt.inboxed_bundles = bundles;
1289
1290            // Extend the block tree by `challenge_period + 1` blocks
1291            let next_receipt = extend_block_tree(
1292                domain_id,
1293                operator_set[0],
1294                current_block_number + challenge_period + 1u32,
1295                target_receipt,
1296            );
1297            // Confirm `target_receipt`
1298            let confirmed_domain_block = process_execution_receipt::<Test>(
1299                domain_id,
1300                operator_set[0],
1301                next_receipt,
1302                AcceptedReceiptType::NewHead,
1303            )
1304            .unwrap()
1305            .unwrap();
1306
1307            // Invalid bundle authors should be collected correctly
1308            assert_eq!(
1309                confirmed_domain_block.invalid_bundle_authors,
1310                invalid_bundle_authors
1311            );
1312        });
1313    }
1314}