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::{ensure, PalletError};
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        create_dummy_bundle_with_receipts, create_dummy_receipt, extend_block_tree,
633        extend_block_tree_from_zero, get_block_tree_node_at, new_test_ext_with_extensions,
634        register_genesis_domain, run_to_block, BlockTreePruningDepth, Domains, Test,
635    };
636    use crate::{FrozenDomains, RawOrigin as DomainOrigin};
637    use frame_support::dispatch::RawOrigin;
638    use frame_support::{assert_err, assert_ok};
639    use frame_system::Origin;
640    use sp_core::H256;
641    use sp_domains::{BundleDigest, InboxedBundle, InvalidBundleType};
642
643    #[test]
644    fn test_genesis_receipt() {
645        let mut ext = new_test_ext_with_extensions();
646        ext.execute_with(|| {
647            let domain_id = register_genesis_domain(0u128, vec![0u64]);
648
649            // The genesis receipt should be added to the block tree
650            let block_tree_node_at_0 = BlockTree::<Test>::get(domain_id, 0).unwrap();
651
652            let genesis_node = BlockTreeNodes::<Test>::get(block_tree_node_at_0).unwrap();
653            assert!(genesis_node.operator_ids.is_empty());
654            assert_eq!(HeadReceiptNumber::<Test>::get(domain_id), 0);
655
656            // The genesis receipt should be able pass the verification and is unchallengeable
657            let genesis_receipt = genesis_node.execution_receipt;
658            let invalid_genesis_receipt = {
659                let mut receipt = genesis_receipt.clone();
660                receipt.final_state_root = H256::random();
661                receipt
662            };
663            assert_ok!(verify_execution_receipt::<Test>(
664                domain_id,
665                &genesis_receipt
666            ));
667            // Submitting an invalid genesis ER will result in `NewBranchReceipt` because the operator
668            // need to submit fraud proof to pruned a ER first before submitting an ER at the same height
669            assert_err!(
670                verify_execution_receipt::<Test>(domain_id, &invalid_genesis_receipt),
671                Error::NewBranchReceipt
672            );
673        });
674    }
675
676    #[test]
677    fn test_new_head_receipt() {
678        let creator = 0u128;
679        let operator_id = 1u64;
680        let block_tree_pruning_depth = <Test as Config>::BlockTreePruningDepth::get();
681
682        let mut ext = new_test_ext_with_extensions();
683        ext.execute_with(|| {
684            let domain_id = register_genesis_domain(creator, vec![operator_id]);
685
686            // The genesis node of the block tree
687            let genesis_node = get_block_tree_node_at::<Test>(domain_id, 0).unwrap();
688            let mut receipt = genesis_node.execution_receipt;
689            assert_eq!(
690                receipt.consensus_block_number,
691                frame_system::Pallet::<Test>::current_block_number()
692            );
693            let mut receipt_of_block_1 = None;
694            let mut bundle_header_hash_of_block_1 = None;
695            for block_number in 1..=(block_tree_pruning_depth as u64 + 3) {
696                // Finilize parent block and initialize block at `block_number`
697                run_to_block::<Test>(block_number, receipt.consensus_block_hash);
698
699                if block_number != 1 {
700                    // `ConsensusBlockHash` should be set to `Some` since last consensus block contains bundle
701                    assert_eq!(
702                        ConsensusBlockHash::<Test>::get(domain_id, block_number - 1),
703                        Some(frame_system::Pallet::<Test>::block_hash(block_number - 1))
704                    );
705                    // ER point to last consensus block should have `NewHead` type
706                    assert_eq!(
707                        execution_receipt_type::<Test>(domain_id, &receipt),
708                        ReceiptType::Accepted(AcceptedReceiptType::NewHead)
709                    );
710                    assert_ok!(verify_execution_receipt::<Test>(domain_id, &receipt));
711                }
712
713                // Submit a bundle with the receipt of the last block
714                let bundle_extrinsics_root = H256::random();
715                let bundle = create_dummy_bundle_with_receipts(
716                    domain_id,
717                    operator_id,
718                    bundle_extrinsics_root,
719                    receipt,
720                );
721                let bundle_header_hash = bundle.sealed_header.pre_hash();
722                let bundle_size = bundle.size();
723                assert_ok!(crate::Pallet::<Test>::submit_bundle(
724                    DomainOrigin::ValidatedUnsigned.into(),
725                    bundle,
726                ));
727                // `bundle_extrinsics_root` should be tracked in `ExecutionInbox`
728                assert_eq!(
729                    ExecutionInbox::<Test>::get((domain_id, block_number as u32, block_number)),
730                    vec![BundleDigest {
731                        header_hash: bundle_header_hash,
732                        extrinsics_root: bundle_extrinsics_root,
733                        size: bundle_size,
734                    }]
735                );
736                assert!(InboxedBundleAuthor::<Test>::contains_key(
737                    bundle_header_hash
738                ));
739
740                // Head receipt number should be updated
741                let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
742                assert_eq!(head_receipt_number, block_number as u32 - 1);
743
744                // As we only extending the block tree there should be no fork
745                let parent_domain_block_receipt =
746                    BlockTree::<Test>::get(domain_id, head_receipt_number).unwrap();
747
748                // The submitter should be added to `operator_ids`
749                let parent_node = BlockTreeNodes::<Test>::get(parent_domain_block_receipt).unwrap();
750                assert_eq!(parent_node.operator_ids.len(), 1);
751                assert_eq!(parent_node.operator_ids[0], operator_id);
752
753                // Construct a `NewHead` receipt of the just submitted bundle, which will be included
754                // in the next bundle
755                receipt = create_dummy_receipt(
756                    block_number,
757                    H256::random(),
758                    parent_domain_block_receipt,
759                    vec![bundle_extrinsics_root],
760                );
761
762                // Record receipt of block #1 for later use
763                if block_number == 1 {
764                    receipt_of_block_1.replace(receipt.clone());
765                    bundle_header_hash_of_block_1.replace(bundle_header_hash);
766                }
767            }
768
769            // The receipt of the block 1 is pruned at the last iteration, verify it will result in
770            // `PrunedReceipt` error
771            let pruned_receipt = receipt_of_block_1.unwrap();
772            let pruned_bundle = bundle_header_hash_of_block_1.unwrap();
773            assert!(BlockTree::<Test>::get(domain_id, 1).is_none());
774            assert!(ExecutionInbox::<Test>::get((domain_id, 1, 1)).is_empty());
775            assert!(!InboxedBundleAuthor::<Test>::contains_key(pruned_bundle));
776            assert_eq!(
777                execution_receipt_type::<Test>(domain_id, &pruned_receipt),
778                ReceiptType::Rejected(RejectedReceiptType::Pruned)
779            );
780            assert_err!(
781                verify_execution_receipt::<Test>(domain_id, &pruned_receipt),
782                Error::PrunedReceipt
783            );
784            assert!(ConsensusBlockHash::<Test>::get(
785                domain_id,
786                pruned_receipt.consensus_block_number,
787            )
788            .is_none());
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 u64 + 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![invalid_execution_trace_receipt
1095                .execution_trace
1096                .first()
1097                .cloned()
1098                .expect("First element should be there; qed")];
1099            assert_err!(
1100                verify_execution_receipt::<Test>(domain_id, &invalid_execution_trace_receipt),
1101                Error::InvalidExecutionTrace
1102            );
1103
1104            // Receipt with zero element in execution trace vector
1105            invalid_execution_trace_receipt.execution_trace = vec![];
1106            assert_err!(
1107                verify_execution_receipt::<Test>(domain_id, &invalid_execution_trace_receipt),
1108                Error::InvalidExecutionTrace
1109            );
1110        });
1111    }
1112
1113    #[test]
1114    fn test_invalid_receipt_with_head_receipt_already_extended() {
1115        let creator = 0u128;
1116        let operator_id = 1u64;
1117        let mut ext = new_test_ext_with_extensions();
1118        ext.execute_with(|| {
1119            let domain_id = register_genesis_domain(creator, vec![operator_id]);
1120            let next_receipt = extend_block_tree_from_zero(domain_id, operator_id, 3);
1121            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
1122
1123            // reject extending receipt if the HeadReceiptNumber is already extended
1124            assert!(NewAddedHeadReceipt::<Test>::get(domain_id).is_none());
1125            NewAddedHeadReceipt::<Test>::set(domain_id, Some(H256::random()));
1126
1127            // Construct a future receipt
1128            let mut future_receipt = next_receipt.clone();
1129            future_receipt.domain_block_number = head_receipt_number + 1;
1130            future_receipt.consensus_block_number = head_receipt_number as u64 + 1;
1131
1132            ExecutionInbox::<Test>::insert(
1133                (
1134                    domain_id,
1135                    future_receipt.domain_block_number,
1136                    future_receipt.consensus_block_number,
1137                ),
1138                future_receipt
1139                    .inboxed_bundles
1140                    .clone()
1141                    .into_iter()
1142                    .map(|b| BundleDigest {
1143                        header_hash: H256::random(),
1144                        extrinsics_root: b.extrinsics_root,
1145                        size: 0,
1146                    })
1147                    .collect::<Vec<_>>(),
1148            );
1149            assert_eq!(
1150                execution_receipt_type::<Test>(domain_id, &future_receipt),
1151                ReceiptType::Rejected(RejectedReceiptType::InFuture)
1152            );
1153            assert_err!(
1154                verify_execution_receipt::<Test>(domain_id, &future_receipt),
1155                Error::InFutureReceipt
1156            );
1157        });
1158    }
1159
1160    #[test]
1161    fn test_invalid_trace_root_receipt() {
1162        let creator = 0u128;
1163        let operator_id1 = 1u64;
1164        let operator_id2 = 2u64;
1165        let mut ext = new_test_ext_with_extensions();
1166        ext.execute_with(|| {
1167            let domain_id = register_genesis_domain(creator, vec![operator_id1, operator_id2]);
1168            let mut next_receipt = extend_block_tree_from_zero(domain_id, operator_id1, 3);
1169            next_receipt.execution_trace.push(H256::random());
1170            next_receipt.final_state_root = *next_receipt.execution_trace.last().unwrap();
1171
1172            let mut trace = Vec::with_capacity(next_receipt.execution_trace.len());
1173            for root in &next_receipt.execution_trace {
1174                trace.push(
1175                    root.encode()
1176                        .try_into()
1177                        .map_err(|_| Error::InvalidTraceRoot)
1178                        .expect("H256 to Blake3Hash should be successful; qed"),
1179                );
1180            }
1181            let new_execution_trace_root = MerkleTree::from_leaves(trace.as_slice())
1182                .root()
1183                .expect("Compute merkle root of trace should success")
1184                .into();
1185            next_receipt.execution_trace_root = new_execution_trace_root;
1186            assert_ok!(verify_execution_receipt::<Test>(domain_id, &next_receipt));
1187
1188            // Receipt with wrong value of `execution_trace_root`
1189            let mut invalid_receipt = next_receipt.clone();
1190            invalid_receipt.execution_trace_root = H256::random();
1191            assert_err!(
1192                verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
1193                Error::InvalidTraceRoot
1194            );
1195
1196            // Receipt with wrong value of trace
1197            let mut invalid_receipt = next_receipt.clone();
1198            invalid_receipt.execution_trace[0] = H256::random();
1199            assert_err!(
1200                verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
1201                Error::InvalidTraceRoot
1202            );
1203
1204            // Receipt with additional trace
1205            let mut invalid_receipt = next_receipt.clone();
1206            invalid_receipt.execution_trace.push(H256::random());
1207            assert_err!(
1208                verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
1209                Error::InvalidTraceRoot
1210            );
1211
1212            // Receipt with missing trace
1213            let mut invalid_receipt = next_receipt;
1214            invalid_receipt.execution_trace.pop();
1215            assert_err!(
1216                verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
1217                Error::InvalidTraceRoot
1218            );
1219        });
1220    }
1221
1222    #[test]
1223    fn test_collect_invalid_bundle_author() {
1224        let creator = 0u128;
1225        let challenge_period = BlockTreePruningDepth::get() as u64;
1226        let operator_set: Vec<_> = (1..15).collect();
1227        let mut ext = new_test_ext_with_extensions();
1228        ext.execute_with(|| {
1229            let domain_id = register_genesis_domain(creator, operator_set.clone());
1230            let next_receipt = extend_block_tree_from_zero(domain_id, operator_set[0], 3);
1231
1232            // Submit bundle for every operator
1233            for operator_id in operator_set.iter() {
1234                let bundle = create_dummy_bundle_with_receipts(
1235                    domain_id,
1236                    *operator_id,
1237                    H256::random(),
1238                    next_receipt.clone(),
1239                );
1240                assert_ok!(crate::Pallet::<Test>::submit_bundle(
1241                    DomainOrigin::ValidatedUnsigned.into(),
1242                    bundle,
1243                ));
1244            }
1245            let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
1246            let head_node = get_block_tree_node_at::<Test>(domain_id, head_receipt_number).unwrap();
1247            assert_eq!(head_node.operator_ids, operator_set);
1248
1249            // Get the `bundles_extrinsics_roots` that contains all the submitted bundles
1250            let current_block_number = frame_system::Pallet::<Test>::current_block_number();
1251            let execution_inbox = ExecutionInbox::<Test>::get((
1252                domain_id,
1253                current_block_number as u32,
1254                current_block_number,
1255            ));
1256            let bundles_extrinsics_roots: Vec<_> = execution_inbox
1257                .into_iter()
1258                .map(|b| b.extrinsics_root)
1259                .collect();
1260            assert_eq!(bundles_extrinsics_roots.len(), operator_set.len());
1261
1262            // Prepare the invalid bundles and invalid bundle authors
1263            let mut bundles = vec![];
1264            let mut invalid_bundle_authors = vec![];
1265            for (i, (operator, extrinsics_root)) in operator_set
1266                .iter()
1267                .zip(bundles_extrinsics_roots)
1268                .enumerate()
1269            {
1270                if i % 2 == 0 {
1271                    invalid_bundle_authors.push(*operator);
1272                    bundles.push(InboxedBundle::invalid(
1273                        InvalidBundleType::OutOfRangeTx(0),
1274                        extrinsics_root,
1275                    ));
1276                } else {
1277                    bundles.push(InboxedBundle::valid(H256::random(), extrinsics_root));
1278                }
1279            }
1280            let mut target_receipt = create_dummy_receipt(
1281                current_block_number,
1282                H256::random(),
1283                next_receipt.hash::<DomainHashingFor<Test>>(),
1284                vec![],
1285            );
1286            target_receipt.inboxed_bundles = bundles;
1287
1288            // Extend the block tree by `challenge_period + 1` blocks
1289            let next_receipt = extend_block_tree(
1290                domain_id,
1291                operator_set[0],
1292                (current_block_number + challenge_period) as u32 + 1,
1293                target_receipt,
1294            );
1295            // Confirm `target_receipt`
1296            let confirmed_domain_block = process_execution_receipt::<Test>(
1297                domain_id,
1298                operator_set[0],
1299                next_receipt,
1300                AcceptedReceiptType::NewHead,
1301            )
1302            .unwrap()
1303            .unwrap();
1304
1305            // Invalid bundle authors should be collected correctly
1306            assert_eq!(
1307                confirmed_domain_block.invalid_bundle_authors,
1308                invalid_bundle_authors
1309            );
1310        });
1311    }
1312}