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