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
628pub(crate) fn invalid_bundle_authors_for_receipt<T: Config>(
629    domain_id: DomainId,
630    er: &ExecutionReceiptOf<T>,
631) -> Vec<OperatorId> {
632    let bundle_digests =
633        ExecutionInbox::<T>::get((domain_id, er.domain_block_number, er.consensus_block_number));
634    bundle_digests
635        .into_iter()
636        .enumerate()
637        .filter_map(|(index, digest)| {
638            let bundle_author = InboxedBundleAuthor::<T>::get(digest.header_hash)?;
639            if er.inboxed_bundles[index].is_invalid() {
640                Some(bundle_author)
641            } else {
642                None
643            }
644        })
645        .collect()
646}
647
648#[cfg(test)]
649mod tests {
650    use super::*;
651    use crate::tests::{
652        BlockTreePruningDepth, Domains, Test, create_dummy_bundle_with_receipts,
653        create_dummy_receipt, extend_block_tree, extend_block_tree_from_zero,
654        get_block_tree_node_at, new_test_ext_with_extensions, register_genesis_domain,
655        run_to_block,
656    };
657    use crate::{FrozenDomains, RawOrigin as DomainOrigin};
658    use frame_support::dispatch::RawOrigin;
659    use frame_support::{assert_err, assert_ok};
660    use frame_system::Origin;
661    use sp_core::H256;
662    use sp_domains::{BundleDigest, InboxedBundle, InvalidBundleType};
663
664    #[test]
665    fn test_genesis_receipt() {
666        let mut ext = new_test_ext_with_extensions();
667        ext.execute_with(|| {
668            let domain_id = register_genesis_domain(0u128, vec![0u64]);
669
670            // The genesis receipt should be added to the block tree
671            let block_tree_node_at_0 = BlockTree::<Test>::get(domain_id, 0).unwrap();
672
673            let genesis_node = BlockTreeNodes::<Test>::get(block_tree_node_at_0).unwrap();
674            assert!(genesis_node.operator_ids.is_empty());
675            assert_eq!(HeadReceiptNumber::<Test>::get(domain_id), 0);
676
677            // The genesis receipt should be able pass the verification and is unchallengeable
678            let genesis_receipt = genesis_node.execution_receipt;
679            let invalid_genesis_receipt = {
680                let mut receipt = genesis_receipt.clone();
681                receipt.final_state_root = H256::random();
682                receipt
683            };
684            assert_ok!(verify_execution_receipt::<Test>(
685                domain_id,
686                &genesis_receipt
687            ));
688            // Submitting an invalid genesis ER will result in `NewBranchReceipt` because the operator
689            // need to submit fraud proof to pruned a ER first before submitting an ER at the same height
690            assert_err!(
691                verify_execution_receipt::<Test>(domain_id, &invalid_genesis_receipt),
692                Error::NewBranchReceipt
693            );
694        });
695    }
696
697    #[test]
698    fn test_new_head_receipt() {
699        let creator = 0u128;
700        let operator_id = 1u64;
701        let block_tree_pruning_depth = <Test as Config>::BlockTreePruningDepth::get();
702
703        let mut ext = new_test_ext_with_extensions();
704        ext.execute_with(|| {
705            let domain_id = register_genesis_domain(creator, vec![operator_id]);
706
707            // The genesis node of the block tree
708            let genesis_node = get_block_tree_node_at::<Test>(domain_id, 0).unwrap();
709            let mut receipt = genesis_node.execution_receipt;
710            assert_eq!(
711                receipt.consensus_block_number,
712                frame_system::Pallet::<Test>::current_block_number()
713            );
714            let mut receipt_of_block_1 = None;
715            let mut bundle_header_hash_of_block_1 = None;
716            for block_number in 1..=(block_tree_pruning_depth + 3) {
717                // Finilize parent block and initialize block at `block_number`
718                run_to_block::<Test>(block_number, receipt.consensus_block_hash);
719
720                if block_number != 1 {
721                    // `ConsensusBlockHash` should be set to `Some` since last consensus block contains bundle
722                    assert_eq!(
723                        ConsensusBlockHash::<Test>::get(domain_id, block_number - 1),
724                        Some(frame_system::Pallet::<Test>::block_hash(block_number - 1))
725                    );
726                    // ER point to last consensus block should have `NewHead` type
727                    assert_eq!(
728                        execution_receipt_type::<Test>(domain_id, &receipt),
729                        ReceiptType::Accepted(AcceptedReceiptType::NewHead)
730                    );
731                    assert_ok!(verify_execution_receipt::<Test>(domain_id, &receipt));
732                }
733
734                // Submit a bundle with the receipt of the last block
735                let bundle_extrinsics_root = H256::random();
736                let bundle = create_dummy_bundle_with_receipts(
737                    domain_id,
738                    operator_id,
739                    bundle_extrinsics_root,
740                    receipt,
741                );
742                let bundle_header_hash = bundle.sealed_header.pre_hash();
743                let bundle_size = bundle.size();
744                assert_ok!(crate::Pallet::<Test>::submit_bundle(
745                    DomainOrigin::ValidatedUnsigned.into(),
746                    bundle,
747                ));
748                // `bundle_extrinsics_root` should be tracked in `ExecutionInbox`
749                assert_eq!(
750                    ExecutionInbox::<Test>::get((domain_id, block_number, block_number)),
751                    vec![BundleDigest {
752                        header_hash: bundle_header_hash,
753                        extrinsics_root: bundle_extrinsics_root,
754                        size: bundle_size,
755                    }]
756                );
757                assert!(InboxedBundleAuthor::<Test>::contains_key(
758                    bundle_header_hash
759                ));
760
761                // Head receipt number should be updated
762                let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
763                assert_eq!(head_receipt_number, block_number - 1);
764
765                // As we only extending the block tree there should be no fork
766                let parent_domain_block_receipt =
767                    BlockTree::<Test>::get(domain_id, head_receipt_number).unwrap();
768
769                // The submitter should be added to `operator_ids`
770                let parent_node = BlockTreeNodes::<Test>::get(parent_domain_block_receipt).unwrap();
771                assert_eq!(parent_node.operator_ids.len(), 1);
772                assert_eq!(parent_node.operator_ids[0], operator_id);
773
774                // Construct a `NewHead` receipt of the just submitted bundle, which will be included
775                // in the next bundle
776                receipt = create_dummy_receipt(
777                    block_number,
778                    H256::random(),
779                    parent_domain_block_receipt,
780                    vec![bundle_extrinsics_root],
781                );
782
783                // Record receipt of block #1 for later use
784                if block_number == 1 {
785                    receipt_of_block_1.replace(receipt.clone());
786                    bundle_header_hash_of_block_1.replace(bundle_header_hash);
787                }
788            }
789
790            // The receipt of the block 1 is pruned at the last iteration, verify it will result in
791            // `PrunedReceipt` error
792            let pruned_receipt = receipt_of_block_1.unwrap();
793            let pruned_bundle = bundle_header_hash_of_block_1.unwrap();
794            assert!(BlockTree::<Test>::get(domain_id, 1).is_none());
795            assert!(ExecutionInbox::<Test>::get((domain_id, 1, 1)).is_empty());
796            assert!(!InboxedBundleAuthor::<Test>::contains_key(pruned_bundle));
797            assert_eq!(
798                execution_receipt_type::<Test>(domain_id, &pruned_receipt),
799                ReceiptType::Rejected(RejectedReceiptType::Pruned)
800            );
801            assert_err!(
802                verify_execution_receipt::<Test>(domain_id, &pruned_receipt),
803                Error::PrunedReceipt
804            );
805            assert!(
806                ConsensusBlockHash::<Test>::get(domain_id, pruned_receipt.consensus_block_number,)
807                    .is_none()
808            );
809        });
810    }
811
812    #[test]
813    fn test_confirm_current_head_receipt() {
814        let creator = 0u128;
815        let operator_id1 = 1u64;
816        let operator_id2 = 2u64;
817        let mut ext = new_test_ext_with_extensions();
818        ext.execute_with(|| {
819            let domain_id = register_genesis_domain(creator, vec![operator_id1, operator_id2]);
820            let next_head_receipt = extend_block_tree_from_zero(domain_id, operator_id1, 3);
821
822            // Submit the new head receipt
823            assert_eq!(
824                execution_receipt_type::<Test>(domain_id, &next_head_receipt),
825                ReceiptType::Accepted(AcceptedReceiptType::NewHead)
826            );
827            assert_ok!(verify_execution_receipt::<Test>(
828                domain_id,
829                &next_head_receipt
830            ));
831            let bundle = create_dummy_bundle_with_receipts(
832                domain_id,
833                operator_id1,
834                H256::random(),
835                next_head_receipt.clone(),
836            );
837            assert_ok!(crate::Pallet::<Test>::submit_bundle(
838                DomainOrigin::ValidatedUnsigned.into(),
839                bundle,
840            ));
841
842            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
843            let current_head_receipt =
844                get_block_tree_node_at::<Test>(domain_id, head_receipt_number)
845                    .unwrap()
846                    .execution_receipt;
847
848            // Now `next_head_receipt` become the head receipt
849            assert_eq!(next_head_receipt, current_head_receipt);
850
851            // Head receipt added in the current block is consider valid
852            assert_eq!(
853                execution_receipt_type::<Test>(domain_id, &current_head_receipt),
854                ReceiptType::Accepted(AcceptedReceiptType::CurrentHead)
855            );
856            assert_ok!(verify_execution_receipt::<Test>(
857                domain_id,
858                &current_head_receipt
859            ));
860
861            // Re-submit the head receipt by a different operator is okay
862            let bundle = create_dummy_bundle_with_receipts(
863                domain_id,
864                operator_id2,
865                H256::random(),
866                current_head_receipt,
867            );
868            assert_ok!(crate::Pallet::<Test>::submit_bundle(
869                DomainOrigin::ValidatedUnsigned.into(),
870                bundle,
871            ));
872
873            let head_node = get_block_tree_node_at::<Test>(domain_id, head_receipt_number).unwrap();
874            assert_eq!(head_node.operator_ids, vec![operator_id1, operator_id2]);
875        });
876    }
877
878    #[test]
879    fn test_non_head_receipt() {
880        let creator = 0u128;
881        let operator_id1 = 1u64;
882        let operator_id2 = 2u64;
883        let mut ext = new_test_ext_with_extensions();
884        ext.execute_with(|| {
885            let domain_id = register_genesis_domain(creator, vec![operator_id1, operator_id2]);
886            extend_block_tree_from_zero(domain_id, operator_id1, 3);
887
888            // Receipt that confirm a non-head receipt is stale receipt
889            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
890            let stale_receipt = get_block_tree_node_at::<Test>(domain_id, head_receipt_number - 1)
891                .unwrap()
892                .execution_receipt;
893            let stale_receipt_hash = stale_receipt.hash::<DomainHashingFor<Test>>();
894
895            // Stale receipt can pass the verification
896            assert_eq!(
897                execution_receipt_type::<Test>(domain_id, &stale_receipt),
898                ReceiptType::Rejected(RejectedReceiptType::Stale)
899            );
900            assert_err!(
901                verify_execution_receipt::<Test>(domain_id, &stale_receipt),
902                Error::StaleReceipt
903            );
904
905            // Stale receipt will be rejected and won't be added to the block tree
906            let bundle = create_dummy_bundle_with_receipts(
907                domain_id,
908                operator_id2,
909                H256::random(),
910                stale_receipt,
911            );
912            assert!(crate::Pallet::<Test>::submit_bundle(RawOrigin::None.into(), bundle).is_err());
913
914            assert_eq!(
915                BlockTreeNodes::<Test>::get(stale_receipt_hash)
916                    .unwrap()
917                    .operator_ids,
918                vec![operator_id1]
919            );
920        });
921    }
922
923    #[test]
924    fn test_previous_head_receipt() {
925        let creator = 0u128;
926        let operator_id1 = 1u64;
927        let operator_id2 = 2u64;
928        let mut ext = new_test_ext_with_extensions();
929        ext.execute_with(|| {
930            let domain_id = register_genesis_domain(creator, vec![operator_id1, operator_id2]);
931            extend_block_tree_from_zero(domain_id, operator_id1, 3);
932
933            // No new receipt submitted in current block
934            assert!(NewAddedHeadReceipt::<Test>::get(domain_id).is_none());
935
936            // Receipt that confirm a head receipt of the previous block is stale receipt
937            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
938            let previous_head_receipt =
939                get_block_tree_node_at::<Test>(domain_id, head_receipt_number)
940                    .unwrap()
941                    .execution_receipt;
942
943            // Stale receipt can not pass the verification
944            assert_eq!(
945                execution_receipt_type::<Test>(domain_id, &previous_head_receipt),
946                ReceiptType::Rejected(RejectedReceiptType::Stale)
947            );
948            assert_err!(
949                verify_execution_receipt::<Test>(domain_id, &previous_head_receipt),
950                Error::StaleReceipt
951            );
952
953            // Stale receipt will be rejected and won't be added to the block tree
954            let bundle = create_dummy_bundle_with_receipts(
955                domain_id,
956                operator_id2,
957                H256::random(),
958                previous_head_receipt,
959            );
960            assert!(crate::Pallet::<Test>::submit_bundle(RawOrigin::None.into(), bundle).is_err());
961        });
962    }
963
964    #[test]
965    fn test_new_branch_receipt() {
966        let creator = 0u128;
967        let operator_id1 = 1u64;
968        let operator_id2 = 2u64;
969        let mut ext = new_test_ext_with_extensions();
970        ext.execute_with(|| {
971            let domain_id = register_genesis_domain(creator, vec![operator_id1, operator_id2]);
972            extend_block_tree_from_zero(domain_id, operator_id1, 3);
973
974            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
975            assert!(BlockTree::<Test>::get(domain_id, head_receipt_number).is_some());
976
977            // Construct new branch receipt that fork away from an existing node of
978            // the block tree
979            let new_branch_receipt = {
980                let mut head_receipt =
981                    get_block_tree_node_at::<Test>(domain_id, head_receipt_number)
982                        .unwrap()
983                        .execution_receipt;
984                head_receipt.final_state_root = H256::random();
985                head_receipt
986            };
987            let new_branch_receipt_hash = new_branch_receipt.hash::<DomainHashingFor<Test>>();
988
989            // New branch receipt can pass the verification
990            assert_eq!(
991                execution_receipt_type::<Test>(domain_id, &new_branch_receipt),
992                ReceiptType::Rejected(RejectedReceiptType::NewBranch)
993            );
994            assert_err!(
995                verify_execution_receipt::<Test>(domain_id, &new_branch_receipt),
996                Error::NewBranchReceipt
997            );
998
999            // Submit the new branch receipt will will be rejected
1000            let bundle = create_dummy_bundle_with_receipts(
1001                domain_id,
1002                operator_id2,
1003                H256::random(),
1004                new_branch_receipt,
1005            );
1006            assert!(crate::Pallet::<Test>::submit_bundle(RawOrigin::None.into(), bundle).is_err());
1007            assert!(BlockTreeNodes::<Test>::get(new_branch_receipt_hash).is_none());
1008        });
1009    }
1010
1011    #[test]
1012    fn test_prune_domain_execution_receipt() {
1013        let creator = 0u128;
1014        let operator_id = 1u64;
1015        let mut ext = new_test_ext_with_extensions();
1016        ext.execute_with(|| {
1017            let domain_id = register_genesis_domain(creator, vec![operator_id]);
1018            let _next_receipt = extend_block_tree_from_zero(domain_id, operator_id, 3);
1019            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
1020
1021            // freeze domain
1022            assert!(!FrozenDomains::<Test>::get().contains(&domain_id));
1023            Domains::freeze_domain(Origin::<Test>::Root.into(), domain_id).unwrap();
1024            assert!(FrozenDomains::<Test>::get().contains(&domain_id));
1025
1026            // prune execution recept
1027            let head_receipt_hash = BlockTree::<Test>::get(domain_id, head_receipt_number).unwrap();
1028            Domains::prune_domain_execution_receipt(
1029                Origin::<Test>::Root.into(),
1030                domain_id,
1031                head_receipt_hash,
1032            )
1033            .unwrap();
1034            assert_eq!(
1035                HeadReceiptNumber::<Test>::get(domain_id),
1036                head_receipt_number - 1
1037            );
1038
1039            // unfreeze domain
1040            Domains::unfreeze_domain(Origin::<Test>::Root.into(), domain_id).unwrap();
1041            assert!(!FrozenDomains::<Test>::get().contains(&domain_id));
1042        })
1043    }
1044
1045    #[test]
1046    fn test_invalid_receipt() {
1047        let creator = 0u128;
1048        let operator_id = 1u64;
1049        let mut ext = new_test_ext_with_extensions();
1050        ext.execute_with(|| {
1051            let domain_id = register_genesis_domain(creator, vec![operator_id]);
1052            let next_receipt = extend_block_tree_from_zero(domain_id, operator_id, 3);
1053            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
1054
1055            // Construct a future receipt
1056            let mut future_receipt = next_receipt.clone();
1057            future_receipt.domain_block_number = head_receipt_number + 2;
1058            future_receipt.consensus_block_number = head_receipt_number as u32 + 2;
1059            ExecutionInbox::<Test>::insert(
1060                (
1061                    domain_id,
1062                    future_receipt.domain_block_number,
1063                    future_receipt.consensus_block_number,
1064                ),
1065                future_receipt
1066                    .inboxed_bundles
1067                    .clone()
1068                    .into_iter()
1069                    .map(|b| BundleDigest {
1070                        header_hash: H256::random(),
1071                        extrinsics_root: b.extrinsics_root,
1072                        size: 0,
1073                    })
1074                    .collect::<Vec<_>>(),
1075            );
1076            assert_eq!(
1077                execution_receipt_type::<Test>(domain_id, &future_receipt),
1078                ReceiptType::Rejected(RejectedReceiptType::InFuture)
1079            );
1080            assert_err!(
1081                verify_execution_receipt::<Test>(domain_id, &future_receipt),
1082                Error::InFutureReceipt
1083            );
1084
1085            // Receipt with unknown extrinsics roots
1086            let mut unknown_extrinsics_roots_receipt = next_receipt.clone();
1087            unknown_extrinsics_roots_receipt.inboxed_bundles =
1088                vec![InboxedBundle::valid(H256::random(), H256::random())];
1089            assert_err!(
1090                verify_execution_receipt::<Test>(domain_id, &unknown_extrinsics_roots_receipt),
1091                Error::InvalidExtrinsicsRoots
1092            );
1093
1094            // Receipt with unknown consensus block hash
1095            let mut unknown_consensus_block_receipt = next_receipt.clone();
1096            unknown_consensus_block_receipt.consensus_block_hash = H256::random();
1097            assert_err!(
1098                verify_execution_receipt::<Test>(domain_id, &unknown_consensus_block_receipt),
1099                Error::BuiltOnUnknownConsensusBlock
1100            );
1101
1102            // Receipt with unknown parent receipt
1103            let mut unknown_parent_receipt = next_receipt.clone();
1104            unknown_parent_receipt.parent_domain_block_receipt_hash = H256::random();
1105            assert_err!(
1106                verify_execution_receipt::<Test>(domain_id, &unknown_parent_receipt),
1107                Error::UnknownParentBlockReceipt
1108            );
1109
1110            // Receipt with execution_trace length less than two
1111            let mut invalid_execution_trace_receipt = next_receipt;
1112
1113            // Receipt with only one element in execution trace vector
1114            invalid_execution_trace_receipt.execution_trace = vec![
1115                invalid_execution_trace_receipt
1116                    .execution_trace
1117                    .first()
1118                    .cloned()
1119                    .expect("First element should be there; qed"),
1120            ];
1121            assert_err!(
1122                verify_execution_receipt::<Test>(domain_id, &invalid_execution_trace_receipt),
1123                Error::InvalidExecutionTrace
1124            );
1125
1126            // Receipt with zero element in execution trace vector
1127            invalid_execution_trace_receipt.execution_trace = vec![];
1128            assert_err!(
1129                verify_execution_receipt::<Test>(domain_id, &invalid_execution_trace_receipt),
1130                Error::InvalidExecutionTrace
1131            );
1132        });
1133    }
1134
1135    #[test]
1136    fn test_invalid_receipt_with_head_receipt_already_extended() {
1137        let creator = 0u128;
1138        let operator_id = 1u64;
1139        let mut ext = new_test_ext_with_extensions();
1140        ext.execute_with(|| {
1141            let domain_id = register_genesis_domain(creator, vec![operator_id]);
1142            let next_receipt = extend_block_tree_from_zero(domain_id, operator_id, 3);
1143            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
1144
1145            // reject extending receipt if the HeadReceiptNumber is already extended
1146            assert!(NewAddedHeadReceipt::<Test>::get(domain_id).is_none());
1147            NewAddedHeadReceipt::<Test>::set(domain_id, Some(H256::random()));
1148
1149            // Construct a future receipt
1150            let mut future_receipt = next_receipt.clone();
1151            future_receipt.domain_block_number = head_receipt_number + 1;
1152            future_receipt.consensus_block_number = head_receipt_number as u32 + 1;
1153
1154            ExecutionInbox::<Test>::insert(
1155                (
1156                    domain_id,
1157                    future_receipt.domain_block_number,
1158                    future_receipt.consensus_block_number,
1159                ),
1160                future_receipt
1161                    .inboxed_bundles
1162                    .clone()
1163                    .into_iter()
1164                    .map(|b| BundleDigest {
1165                        header_hash: H256::random(),
1166                        extrinsics_root: b.extrinsics_root,
1167                        size: 0,
1168                    })
1169                    .collect::<Vec<_>>(),
1170            );
1171            assert_eq!(
1172                execution_receipt_type::<Test>(domain_id, &future_receipt),
1173                ReceiptType::Rejected(RejectedReceiptType::InFuture)
1174            );
1175            assert_err!(
1176                verify_execution_receipt::<Test>(domain_id, &future_receipt),
1177                Error::InFutureReceipt
1178            );
1179        });
1180    }
1181
1182    #[test]
1183    fn test_invalid_trace_root_receipt() {
1184        let creator = 0u128;
1185        let operator_id1 = 1u64;
1186        let operator_id2 = 2u64;
1187        let mut ext = new_test_ext_with_extensions();
1188        ext.execute_with(|| {
1189            let domain_id = register_genesis_domain(creator, vec![operator_id1, operator_id2]);
1190            let mut next_receipt = extend_block_tree_from_zero(domain_id, operator_id1, 3);
1191            next_receipt.execution_trace.push(H256::random());
1192            next_receipt.final_state_root = *next_receipt.execution_trace.last().unwrap();
1193
1194            let mut trace = Vec::with_capacity(next_receipt.execution_trace.len());
1195            for root in &next_receipt.execution_trace {
1196                trace.push(
1197                    root.encode()
1198                        .try_into()
1199                        .map_err(|_| Error::InvalidTraceRoot)
1200                        .expect("H256 to Blake3Hash should be successful; qed"),
1201                );
1202            }
1203            let new_execution_trace_root = MerkleTree::from_leaves(trace.as_slice())
1204                .root()
1205                .expect("Compute merkle root of trace should success")
1206                .into();
1207            next_receipt.execution_trace_root = new_execution_trace_root;
1208            assert_ok!(verify_execution_receipt::<Test>(domain_id, &next_receipt));
1209
1210            // Receipt with wrong value of `execution_trace_root`
1211            let mut invalid_receipt = next_receipt.clone();
1212            invalid_receipt.execution_trace_root = H256::random();
1213            assert_err!(
1214                verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
1215                Error::InvalidTraceRoot
1216            );
1217
1218            // Receipt with wrong value of trace
1219            let mut invalid_receipt = next_receipt.clone();
1220            invalid_receipt.execution_trace[0] = H256::random();
1221            assert_err!(
1222                verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
1223                Error::InvalidTraceRoot
1224            );
1225
1226            // Receipt with additional trace
1227            let mut invalid_receipt = next_receipt.clone();
1228            invalid_receipt.execution_trace.push(H256::random());
1229            assert_err!(
1230                verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
1231                Error::InvalidTraceRoot
1232            );
1233
1234            // Receipt with missing trace
1235            let mut invalid_receipt = next_receipt;
1236            invalid_receipt.execution_trace.pop();
1237            assert_err!(
1238                verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
1239                Error::InvalidTraceRoot
1240            );
1241        });
1242    }
1243
1244    #[test]
1245    fn test_collect_invalid_bundle_author() {
1246        let creator = 0u128;
1247        let challenge_period = BlockTreePruningDepth::get();
1248        let operator_set: Vec<_> = (1..15).collect();
1249        let mut ext = new_test_ext_with_extensions();
1250        ext.execute_with(|| {
1251            let domain_id = register_genesis_domain(creator, operator_set.clone());
1252            let next_receipt = extend_block_tree_from_zero(domain_id, operator_set[0], 3);
1253
1254            // Submit bundle for every operator
1255            for operator_id in operator_set.iter() {
1256                let bundle = create_dummy_bundle_with_receipts(
1257                    domain_id,
1258                    *operator_id,
1259                    H256::random(),
1260                    next_receipt.clone(),
1261                );
1262                assert_ok!(crate::Pallet::<Test>::submit_bundle(
1263                    DomainOrigin::ValidatedUnsigned.into(),
1264                    bundle,
1265                ));
1266            }
1267            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
1268            let head_node = get_block_tree_node_at::<Test>(domain_id, head_receipt_number).unwrap();
1269            assert_eq!(head_node.operator_ids, operator_set);
1270
1271            // Get the `bundles_extrinsics_roots` that contains all the submitted bundles
1272            let current_block_number = frame_system::Pallet::<Test>::current_block_number();
1273            let execution_inbox = ExecutionInbox::<Test>::get((
1274                domain_id,
1275                current_block_number,
1276                current_block_number,
1277            ));
1278            let bundles_extrinsics_roots: Vec<_> = execution_inbox
1279                .into_iter()
1280                .map(|b| b.extrinsics_root)
1281                .collect();
1282            assert_eq!(bundles_extrinsics_roots.len(), operator_set.len());
1283
1284            // Prepare the invalid bundles and invalid bundle authors
1285            let mut bundles = vec![];
1286            let mut invalid_bundle_authors = vec![];
1287            for (i, (operator, extrinsics_root)) in operator_set
1288                .iter()
1289                .zip(bundles_extrinsics_roots)
1290                .enumerate()
1291            {
1292                if i % 2 == 0 {
1293                    invalid_bundle_authors.push(*operator);
1294                    bundles.push(InboxedBundle::invalid(
1295                        InvalidBundleType::OutOfRangeTx(0),
1296                        extrinsics_root,
1297                    ));
1298                } else {
1299                    bundles.push(InboxedBundle::valid(H256::random(), extrinsics_root));
1300                }
1301            }
1302            let mut target_receipt = create_dummy_receipt(
1303                current_block_number,
1304                H256::random(),
1305                next_receipt.hash::<DomainHashingFor<Test>>(),
1306                vec![],
1307            );
1308            target_receipt.inboxed_bundles = bundles;
1309
1310            // Extend the block tree by `challenge_period + 1` blocks
1311            let next_receipt = extend_block_tree(
1312                domain_id,
1313                operator_set[0],
1314                current_block_number + challenge_period + 1u32,
1315                target_receipt,
1316            );
1317            // Confirm `target_receipt`
1318            let confirmed_domain_block = process_execution_receipt::<Test>(
1319                domain_id,
1320                operator_set[0],
1321                next_receipt,
1322                AcceptedReceiptType::NewHead,
1323            )
1324            .unwrap()
1325            .unwrap();
1326
1327            // Invalid bundle authors should be collected correctly
1328            assert_eq!(
1329                confirmed_domain_block.invalid_bundle_authors,
1330                invalid_bundle_authors
1331            );
1332        });
1333    }
1334}