domain_client_operator/
domain_block_processor.rs

1use crate::ExecutionReceiptFor;
2use crate::aux_schema::BundleMismatchType;
3use crate::fraud_proof::{FraudProofFor, FraudProofGenerator};
4use crate::utils::{DomainBlockImportNotification, DomainImportNotificationSinks};
5use domain_block_builder::{BlockBuilder, BuiltBlock, CollectedStorageChanges};
6use domain_block_preprocessor::PreprocessResult;
7use domain_block_preprocessor::inherents::get_inherent_data;
8use parity_scale_codec::Encode;
9use sc_client_api::{AuxStore, BlockBackend, ExecutorProvider, Finalizer, ProofProvider};
10use sc_consensus::{
11    BlockImportParams, BoxBlockImport, ForkChoiceStrategy, ImportResult, StateAction,
12    StorageChanges,
13};
14use sc_executor::RuntimeVersionOf;
15use sc_transaction_pool_api::OffchainTransactionPoolFactory;
16use sp_api::{ApiExt, ProvideRuntimeApi};
17use sp_blockchain::{HashAndNumber, HeaderBackend, HeaderMetadata};
18use sp_consensus::{BlockOrigin, SyncOracle};
19use sp_core::H256;
20use sp_core::traits::CodeExecutor;
21use sp_domains::bundle::BundleValidity;
22use sp_domains::core_api::DomainCoreApi;
23use sp_domains::execution_receipt::execution_receipt_v0::ExecutionReceiptV0;
24use sp_domains::execution_receipt::{ExecutionReceipt, ExecutionReceiptVersion};
25use sp_domains::merkle_tree::MerkleTree;
26use sp_domains::{DomainId, DomainsApi, HeaderHashingFor};
27use sp_domains_fraud_proof::FraudProofApi;
28use sp_messenger::MessengerApi;
29use sp_mmr_primitives::MmrApi;
30use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor, One, Zero};
31use sp_runtime::{Digest, Saturating};
32use std::cmp::Ordering;
33use std::collections::VecDeque;
34use std::sync::Arc;
35
36struct DomainBlockBuildResult<Block>
37where
38    Block: BlockT,
39{
40    header_number: NumberFor<Block>,
41    header_hash: Block::Hash,
42    state_root: Block::Hash,
43    extrinsics_root: Block::Hash,
44    intermediate_roots: Vec<Block::Hash>,
45}
46
47pub(crate) struct DomainBlockResult<Block, CBlock>
48where
49    Block: BlockT,
50    CBlock: BlockT,
51{
52    pub header_hash: Block::Hash,
53    pub header_number: NumberFor<Block>,
54    pub execution_receipt: ExecutionReceiptFor<Block, CBlock>,
55}
56
57/// An abstracted domain block processor.
58pub(crate) struct DomainBlockProcessor<Block, CBlock, Client, CClient, Backend, Executor>
59where
60    Block: BlockT,
61    CBlock: BlockT,
62{
63    pub(crate) domain_id: DomainId,
64    pub(crate) domain_created_at: NumberFor<CBlock>,
65    pub(crate) client: Arc<Client>,
66    pub(crate) consensus_client: Arc<CClient>,
67    pub(crate) backend: Arc<Backend>,
68    pub(crate) block_import: Arc<BoxBlockImport<Block>>,
69    pub(crate) import_notification_sinks: DomainImportNotificationSinks<Block, CBlock>,
70    pub(crate) domain_sync_oracle: Arc<dyn SyncOracle + Send + Sync>,
71    pub(crate) domain_executor: Arc<Executor>,
72    pub(crate) challenge_period: NumberFor<CBlock>,
73}
74
75impl<Block, CBlock, Client, CClient, Backend, Executor> Clone
76    for DomainBlockProcessor<Block, CBlock, Client, CClient, Backend, Executor>
77where
78    Block: BlockT,
79    CBlock: BlockT,
80{
81    fn clone(&self) -> Self {
82        Self {
83            domain_id: self.domain_id,
84            domain_created_at: self.domain_created_at,
85            client: self.client.clone(),
86            consensus_client: self.consensus_client.clone(),
87            backend: self.backend.clone(),
88            block_import: self.block_import.clone(),
89            import_notification_sinks: self.import_notification_sinks.clone(),
90            domain_sync_oracle: self.domain_sync_oracle.clone(),
91            domain_executor: self.domain_executor.clone(),
92            challenge_period: self.challenge_period,
93        }
94    }
95}
96
97/// A list of consensus blocks waiting to be processed by operator on each imported consensus block
98/// notification.
99///
100/// Usually, each new domain block is built on top of the current best domain block, with the block
101/// content extracted from the incoming consensus block. However, an incoming imported consensus block
102/// notification can also imply multiple pending consensus blocks in case of the consensus chain re-org.
103#[derive(Debug)]
104pub(crate) struct PendingConsensusBlocks<Block: BlockT, CBlock: BlockT> {
105    /// Base block used to build new domain blocks derived from the consensus blocks below.
106    pub initial_parent: (Block::Hash, NumberFor<Block>),
107    /// Pending consensus blocks that need to be processed sequentially.
108    pub consensus_imports: Vec<HashAndNumber<CBlock>>,
109}
110
111impl<Block, CBlock, Client, CClient, Backend, Executor>
112    DomainBlockProcessor<Block, CBlock, Client, CClient, Backend, Executor>
113where
114    Block: BlockT,
115    CBlock: BlockT,
116    NumberFor<CBlock>: Into<NumberFor<Block>>,
117    Client: HeaderBackend<Block>
118        + BlockBackend<Block>
119        + AuxStore
120        + ProvideRuntimeApi<Block>
121        + Finalizer<Block, Backend>
122        + ExecutorProvider<Block>
123        + 'static,
124    Client::Api:
125        DomainCoreApi<Block> + sp_block_builder::BlockBuilder<Block> + sp_api::ApiExt<Block>,
126    CClient: HeaderBackend<CBlock>
127        + HeaderMetadata<CBlock, Error = sp_blockchain::Error>
128        + BlockBackend<CBlock>
129        + ProvideRuntimeApi<CBlock>
130        + 'static,
131    CClient::Api: DomainsApi<CBlock, Block::Header>
132        + MessengerApi<CBlock, NumberFor<CBlock>, CBlock::Hash>
133        + 'static,
134    Backend: sc_client_api::Backend<Block> + 'static,
135    Executor: CodeExecutor,
136{
137    /// Returns a list of consensus blocks waiting to be processed if any.
138    ///
139    /// It's possible to have multiple pending consensus blocks that need to be processed in case
140    /// the consensus chain re-org occurs.
141    pub(crate) fn pending_imported_consensus_blocks(
142        &self,
143        consensus_block_hash: CBlock::Hash,
144        consensus_block_number: NumberFor<CBlock>,
145    ) -> sp_blockchain::Result<Option<PendingConsensusBlocks<Block, CBlock>>> {
146        match consensus_block_number.cmp(&(self.domain_created_at + One::one())) {
147            // Consensus block at `domain_created_at + 1` is the first block that possibly contains
148            // bundle, thus we can safely skip any consensus block that is smaller than this block.
149            Ordering::Less => return Ok(None),
150            // For consensus block at `domain_created_at + 1` use the genesis domain block as the
151            // initial parent block directly to avoid further processing.
152            Ordering::Equal => {
153                return Ok(Some(PendingConsensusBlocks {
154                    initial_parent: (self.client.info().genesis_hash, Zero::zero()),
155                    consensus_imports: vec![HashAndNumber {
156                        hash: consensus_block_hash,
157                        number: consensus_block_number,
158                    }],
159                }));
160            }
161            // For consensus block > `domain_created_at + 1`, there is potential existing fork
162            // thus we need to handle it carefully as following.
163            Ordering::Greater => {}
164        }
165
166        let best_hash = self.client.info().best_hash;
167        let best_number = self.client.info().best_number;
168
169        // When there are empty consensus blocks multiple consensus block could map to the same
170        // domain block, thus use `latest_consensus_block_hash_for` to find the latest consensus
171        // block that map to the best domain block.
172        let consensus_block_hash_for_best_domain_hash =
173            match crate::aux_schema::latest_consensus_block_hash_for(&*self.backend, &best_hash)? {
174                // If the auxiliary storage is empty and the best domain block is the genesis block
175                // this consensus block could be the first block being processed thus just use the
176                // consensus block at `domain_created_at` directly, otherwise the auxiliary storage
177                // is expected to be non-empty thus return an error.
178                //
179                // NOTE: it is important to check the auxiliary storage first before checking if it
180                // is the genesis block otherwise we may repeatedly processing the empty consensus
181                // block, see https://github.com/autonomys/subspace/issues/1763 for more detail.
182                None if best_number.is_zero() => self
183                    .consensus_client
184                    .hash(self.domain_created_at)?
185                    .ok_or_else(|| {
186                        sp_blockchain::Error::Backend(format!(
187                            "Consensus hash for block #{} not found",
188                            self.domain_created_at
189                        ))
190                    })?,
191                None => {
192                    return Err(sp_blockchain::Error::Backend(format!(
193                        "Consensus hash for domain hash #{best_number},{best_hash} not found"
194                    )));
195                }
196                Some(b) => b,
197            };
198
199        let consensus_from = consensus_block_hash_for_best_domain_hash;
200        let consensus_to = consensus_block_hash;
201
202        if consensus_from == consensus_to {
203            tracing::debug!("Consensus block {consensus_block_hash:?} has already been processed");
204            return Ok(None);
205        }
206
207        let route =
208            sp_blockchain::tree_route(&*self.consensus_client, consensus_from, consensus_to)?;
209
210        let retracted = route.retracted();
211        let enacted = route.enacted();
212
213        tracing::trace!(
214            ?retracted,
215            ?enacted,
216            common_block = ?route.common_block(),
217            "Calculating PendingConsensusBlocks on #{best_number},{best_hash:?}"
218        );
219
220        match (retracted.is_empty(), enacted.is_empty()) {
221            (true, false) => {
222                // New tip, A -> B
223                Ok(Some(PendingConsensusBlocks {
224                    initial_parent: (best_hash, best_number),
225                    consensus_imports: enacted.to_vec(),
226                }))
227            }
228            (false, true) => {
229                tracing::debug!("Consensus blocks {retracted:?} have been already processed");
230                Ok(None)
231            }
232            (true, true) => {
233                unreachable!(
234                    "Tree route is not empty as `consensus_from` and `consensus_to` in tree_route() \
235                    are checked above to be not the same; qed",
236                );
237            }
238            (false, false) => {
239                let (common_block_number, common_block_hash) =
240                    (route.common_block().number, route.common_block().hash);
241
242                // The `common_block` is smaller than the consensus block that the domain was created at, thus
243                // we can safely skip any consensus block that is smaller than `domain_created_at` and start at
244                // the consensus block `domain_created_at + 1`, which the first block that possibly contains bundle,
245                // and use the genesis domain block as the initial parent block.
246                if common_block_number <= self.domain_created_at {
247                    let consensus_imports = enacted
248                        .iter()
249                        .skip_while(|block| block.number <= self.domain_created_at)
250                        .cloned()
251                        .collect();
252                    return Ok(Some(PendingConsensusBlocks {
253                        initial_parent: (self.client.info().genesis_hash, Zero::zero()),
254                        consensus_imports,
255                    }));
256                }
257
258                // Get the domain block that is derived from the common consensus block and use it as
259                // the initial domain parent block
260                let domain_block_hash: Block::Hash = crate::aux_schema::best_domain_hash_for(
261                    &*self.client,
262                    &common_block_hash,
263                )?
264                    .ok_or_else(
265                        || {
266                            sp_blockchain::Error::Backend(format!(
267                                "Hash of domain block derived from consensus block #{common_block_number},{common_block_hash} not found"
268                            ))
269                        },
270                    )?;
271                let parent_header = self.client.header(domain_block_hash)?.ok_or_else(|| {
272                    sp_blockchain::Error::Backend(format!(
273                        "Domain block header for #{domain_block_hash:?} not found",
274                    ))
275                })?;
276
277                Ok(Some(PendingConsensusBlocks {
278                    initial_parent: (parent_header.hash(), *parent_header.number()),
279                    consensus_imports: enacted.to_vec(),
280                }))
281            }
282        }
283    }
284
285    pub(crate) async fn process_domain_block(
286        &self,
287        (consensus_block_hash, consensus_block_number): (CBlock::Hash, NumberFor<CBlock>),
288        (parent_hash, parent_number): (Block::Hash, NumberFor<Block>),
289        preprocess_result: PreprocessResult<Block>,
290        inherent_digests: Digest,
291    ) -> Result<DomainBlockResult<Block, CBlock>, sp_blockchain::Error> {
292        let PreprocessResult {
293            extrinsics,
294            bundles,
295        } = preprocess_result;
296
297        // The fork choice of the domain chain should follow exactly as the consensus chain,
298        // so always use `Custom(false)` here to not set the domain block as new best block
299        // immediately, and if the domain block is indeed derive from the best consensus fork,
300        // it will be reset as the best domain block, see `BundleProcessor::process_bundles`.
301        let fork_choice = ForkChoiceStrategy::Custom(false);
302        let inherent_data = get_inherent_data::<_, _, Block>(
303            self.consensus_client.clone(),
304            consensus_block_hash,
305            parent_hash,
306            self.domain_id,
307        )
308        .await?;
309
310        let DomainBlockBuildResult {
311            extrinsics_root,
312            state_root,
313            header_number,
314            header_hash,
315            intermediate_roots,
316        } = self
317            .build_and_import_block(
318                parent_hash,
319                parent_number,
320                extrinsics,
321                fork_choice,
322                inherent_digests,
323                inherent_data,
324            )
325            .await?;
326
327        tracing::debug!(
328            "Built new domain block #{header_number},{header_hash} from \
329            consensus block #{consensus_block_number},{consensus_block_hash} \
330            on top of parent domain block #{parent_number},{parent_hash}"
331        );
332
333        let roots: Vec<[u8; 32]> = intermediate_roots
334            .iter()
335            .map(|v| {
336                v.encode().try_into().expect(
337                    "State root uses the same Block hash type which must fit into [u8; 32]; qed",
338                )
339            })
340            .collect();
341        let trace_root = MerkleTree::from_leaves(&roots).root().ok_or_else(|| {
342            sp_blockchain::Error::Application(Box::from("Failed to get merkle root of trace"))
343        })?;
344
345        tracing::trace!(
346            ?intermediate_roots,
347            ?trace_root,
348            "Trace root calculated for #{header_number},{header_hash}"
349        );
350
351        let runtime_api = self.consensus_client.runtime_api();
352
353        let execution_receipt_version = runtime_api
354            .current_bundle_and_execution_receipt_version(consensus_block_hash)?
355            .execution_receipt_version;
356
357        let parent_receipt = if parent_number.is_zero() {
358            runtime_api
359                .genesis_execution_receipt(consensus_block_hash, self.domain_id)?
360                .ok_or_else(|| {
361                    sp_blockchain::Error::Backend("Genesis Execution receipt not found".to_string())
362                })?
363        } else {
364            crate::load_execution_receipt_by_domain_hash::<Block, CBlock, _>(
365                &*self.client,
366                parent_hash,
367                parent_number,
368            )?
369        };
370
371        // Get the accumulated transaction fee of all transactions included in the block
372        // and used as the operator reward
373        let runtime_api = self.client.runtime_api();
374        let block_fees = runtime_api.block_fees(header_hash)?;
375        let transfers = runtime_api.transfers(header_hash)?;
376
377        let execution_receipt = match execution_receipt_version {
378            ExecutionReceiptVersion::V0 => ExecutionReceipt::V0(ExecutionReceiptV0 {
379                domain_block_number: header_number,
380                domain_block_hash: header_hash,
381                domain_block_extrinsic_root: extrinsics_root,
382                parent_domain_block_receipt_hash: parent_receipt
383                    .hash::<HeaderHashingFor<Block::Header>>(),
384                consensus_block_number,
385                consensus_block_hash,
386                inboxed_bundles: bundles,
387                final_state_root: state_root,
388                execution_trace: intermediate_roots,
389                execution_trace_root: sp_core::H256(trace_root),
390                block_fees,
391                transfers,
392            }),
393        };
394
395        Ok(DomainBlockResult {
396            header_hash,
397            header_number,
398            execution_receipt,
399        })
400    }
401
402    async fn build_and_import_block(
403        &self,
404        parent_hash: Block::Hash,
405        parent_number: NumberFor<Block>,
406        extrinsics: VecDeque<Block::Extrinsic>,
407        fork_choice: ForkChoiceStrategy,
408        inherent_digests: Digest,
409        inherent_data: sp_inherents::InherentData,
410    ) -> Result<DomainBlockBuildResult<Block>, sp_blockchain::Error> {
411        let block_builder = BlockBuilder::new(
412            self.client.clone(),
413            parent_hash,
414            parent_number,
415            inherent_digests,
416            self.backend.clone(),
417            self.domain_executor.clone(),
418            extrinsics,
419            Some(inherent_data),
420        )?;
421
422        let BuiltBlock {
423            block,
424            storage_changes,
425        } = block_builder.build()?;
426
427        let CollectedStorageChanges {
428            storage_changes,
429            intermediate_roots,
430        } = storage_changes;
431
432        let (header, body) = block.deconstruct();
433        let state_root = *header.state_root();
434        let extrinsics_root = *header.extrinsics_root();
435        let header_hash = header.hash();
436        let header_number = *header.number();
437
438        let block_import_params = {
439            let block_origin = if self.domain_sync_oracle.is_major_syncing() {
440                // The domain block is derived from the consensus block, if the consensus chain is
441                // in major sync then we should also consider the domain block is `NetworkInitialSync`
442                BlockOrigin::NetworkInitialSync
443            } else {
444                BlockOrigin::Own
445            };
446            let mut import_block = BlockImportParams::new(block_origin, header);
447            import_block.body = Some(body);
448            import_block.state_action =
449                StateAction::ApplyChanges(StorageChanges::Changes(storage_changes));
450            import_block.fork_choice = Some(fork_choice);
451            import_block
452        };
453        self.import_domain_block(block_import_params).await?;
454
455        Ok(DomainBlockBuildResult {
456            header_hash,
457            header_number,
458            state_root,
459            extrinsics_root,
460            intermediate_roots,
461        })
462    }
463
464    pub(crate) async fn import_domain_block(
465        &self,
466        block_import_params: BlockImportParams<Block>,
467    ) -> Result<(), sp_blockchain::Error> {
468        let (header_number, header_hash, parent_hash) = (
469            *block_import_params.header.number(),
470            block_import_params.header.hash(),
471            *block_import_params.header.parent_hash(),
472        );
473
474        let import_result = self.block_import.import_block(block_import_params).await?;
475
476        match import_result {
477            ImportResult::Imported(..) => {}
478            ImportResult::AlreadyInChain => {
479                tracing::debug!("Block #{header_number},{header_hash:?} is already in chain");
480            }
481            ImportResult::KnownBad => {
482                return Err(sp_consensus::Error::ClientImport(format!(
483                    "Bad block #{header_number}({header_hash:?})"
484                ))
485                .into());
486            }
487            ImportResult::UnknownParent => {
488                return Err(sp_consensus::Error::ClientImport(format!(
489                    "Block #{header_number}({header_hash:?}) has an unknown parent: {parent_hash:?}"
490                ))
491                .into());
492            }
493            ImportResult::MissingState => {
494                return Err(sp_consensus::Error::ClientImport(format!(
495                    "Parent state of block #{header_number}({header_hash:?}) is missing, parent: {parent_hash:?}"
496                ))
497                    .into());
498            }
499        }
500
501        Ok(())
502    }
503
504    pub(crate) fn on_consensus_block_processed(
505        &self,
506        consensus_block_hash: CBlock::Hash,
507        domain_block_result: Option<DomainBlockResult<Block, CBlock>>,
508    ) -> sp_blockchain::Result<()> {
509        //clean up aux storage when domain imports a new block
510        let cleanup = domain_block_result.is_some();
511
512        let domain_hash = match domain_block_result {
513            Some(DomainBlockResult {
514                header_hash,
515                header_number: _,
516                execution_receipt,
517            }) => {
518                let oldest_unconfirmed_receipt_number = self
519                    .consensus_client
520                    .runtime_api()
521                    .oldest_unconfirmed_receipt_number(consensus_block_hash, self.domain_id)?;
522                crate::aux_schema::write_execution_receipt::<_, Block, CBlock>(
523                    &*self.client,
524                    oldest_unconfirmed_receipt_number,
525                    &execution_receipt,
526                    self.challenge_period,
527                )?;
528
529                // Notify the imported domain block when the receipt processing is done.
530                let domain_import_notification = DomainBlockImportNotification {
531                    domain_block_hash: header_hash,
532                    consensus_block_hash,
533                };
534
535                self.import_notification_sinks.lock().retain(|sink| {
536                    sink.unbounded_send(domain_import_notification.clone())
537                        .is_ok()
538                });
539
540                header_hash
541            }
542            None => {
543                // No new domain block produced, thus this consensus block should map to the same
544                // domain block as its parent block
545                let consensus_header = self
546                    .consensus_client
547                    .header(consensus_block_hash)?
548                    .ok_or_else(|| {
549                        sp_blockchain::Error::Backend(format!(
550                            "Header for consensus block {consensus_block_hash:?} not found"
551                        ))
552                    })?;
553                if *consensus_header.number() > self.domain_created_at + One::one() {
554                    let consensus_parent_hash = consensus_header.parent_hash();
555                    crate::aux_schema::best_domain_hash_for(&*self.client, consensus_parent_hash)?
556                        .ok_or_else(|| {
557                        sp_blockchain::Error::Backend(format!(
558                            "Domain hash for consensus block {consensus_parent_hash:?} not found",
559                        ))
560                    })?
561                } else {
562                    self.client.info().genesis_hash
563                }
564            }
565        };
566
567        crate::aux_schema::track_domain_hash_and_consensus_hash::<_, Block, CBlock>(
568            &self.client,
569            domain_hash,
570            consensus_block_hash,
571            cleanup,
572        )?;
573
574        Ok(())
575    }
576}
577
578#[derive(Debug, PartialEq)]
579pub(crate) struct InboxedBundleMismatchInfo {
580    bundle_index: u32,
581    mismatch_type: BundleMismatchType,
582}
583
584// Find the first mismatch of the `InboxedBundle` in the `ER::inboxed_bundles` list
585pub(crate) fn find_inboxed_bundles_mismatch<Block, CBlock>(
586    local_receipt: &ExecutionReceiptFor<Block, CBlock>,
587    external_receipt: &ExecutionReceiptFor<Block, CBlock>,
588) -> Result<Option<InboxedBundleMismatchInfo>, sp_blockchain::Error>
589where
590    Block: BlockT,
591    CBlock: BlockT,
592{
593    if local_receipt.inboxed_bundles() == external_receipt.inboxed_bundles() {
594        return Ok(None);
595    }
596
597    // The `bundles_extrinsics_roots` should be checked in the runtime when the consensus block is
598    // constructed/imported thus the `external_receipt` must have the same `bundles_extrinsics_roots`
599    //
600    // NOTE: this also check `local_receipt.inboxed_bundles` and `external_receipt.inboxed_bundles`
601    // have the same length
602    if local_receipt.bundles_extrinsics_roots() != external_receipt.bundles_extrinsics_roots() {
603        return Err(sp_blockchain::Error::Application(format!(
604            "Found mismatch of `ER::bundles_extrinsics_roots`, this should not happen, local: {:?}, external: {:?}",
605            local_receipt.bundles_extrinsics_roots(),
606            external_receipt.bundles_extrinsics_roots(),
607        ).into()));
608    }
609
610    // Get the first mismatch of `ER::inboxed_bundles`
611    let (bundle_index, (local_bundle, external_bundle)) = local_receipt
612        .inboxed_bundles()
613        .iter()
614        .zip(external_receipt.inboxed_bundles().iter())
615        .enumerate()
616        .find(|(_, (local_bundle, external_bundle))| local_bundle != external_bundle)
617        .expect(
618            "The `local_receipt.inboxed_bundles` and `external_receipt.inboxed_bundles` are checked to have the \
619            same length and being non-equal; qed",
620        );
621
622    // The `local_bundle` and `external_bundle` are checked to have the same `extrinsic_root`
623    // thus they must being mismatch due to bundle validity
624    let mismatch_type = match (local_bundle.bundle.clone(), external_bundle.bundle.clone()) {
625        (
626            BundleValidity::Invalid(local_invalid_type),
627            BundleValidity::Invalid(external_invalid_type),
628        ) => {
629            match local_invalid_type
630                .checking_order()
631                .cmp(&external_invalid_type.checking_order())
632            {
633                // The `external_invalid_type` claims a prior check passed, while the `local_invalid_type` thinks
634                // it failed, so generate a fraud proof to prove that prior check is truly invalid
635                Ordering::Less => BundleMismatchType::GoodInvalid(local_invalid_type),
636                // The `external_invalid_type` claims a prior check failed, while the `local_invalid_type` thinks
637                // it passed, so generate a fraud proof to prove that prior check was actually valid
638                Ordering::Greater => BundleMismatchType::BadInvalid(external_invalid_type),
639                Ordering::Equal => unreachable!(
640                    "bundle validity must be different as the local/external bundle are checked to be different \
641                    and they have the same `extrinsic_root`"
642                ),
643            }
644        }
645        (BundleValidity::Valid(_), BundleValidity::Valid(_)) => {
646            BundleMismatchType::ValidBundleContents
647        }
648        (BundleValidity::Valid(_), BundleValidity::Invalid(invalid_type)) => {
649            BundleMismatchType::BadInvalid(invalid_type)
650        }
651        (BundleValidity::Invalid(invalid_type), BundleValidity::Valid(_)) => {
652            BundleMismatchType::GoodInvalid(invalid_type)
653        }
654    };
655
656    Ok(Some(InboxedBundleMismatchInfo {
657        mismatch_type,
658        bundle_index: bundle_index as u32,
659    }))
660}
661
662pub(crate) struct ReceiptsChecker<Block, Client, CBlock, CClient, Backend, E>
663where
664    Block: BlockT,
665    CBlock: BlockT,
666{
667    pub(crate) domain_id: DomainId,
668    pub(crate) client: Arc<Client>,
669    pub(crate) consensus_client: Arc<CClient>,
670    pub(crate) domain_sync_oracle: Arc<dyn SyncOracle + Send + Sync>,
671    pub(crate) fraud_proof_generator:
672        FraudProofGenerator<Block, CBlock, Client, CClient, Backend, E>,
673    pub(crate) consensus_offchain_tx_pool_factory: OffchainTransactionPoolFactory<CBlock>,
674}
675
676impl<Block, CBlock, Client, CClient, Backend, E> Clone
677    for ReceiptsChecker<Block, Client, CBlock, CClient, Backend, E>
678where
679    Block: BlockT,
680    CBlock: BlockT,
681{
682    fn clone(&self) -> Self {
683        Self {
684            domain_id: self.domain_id,
685            client: self.client.clone(),
686            consensus_client: self.consensus_client.clone(),
687            domain_sync_oracle: self.domain_sync_oracle.clone(),
688            fraud_proof_generator: self.fraud_proof_generator.clone(),
689            consensus_offchain_tx_pool_factory: self.consensus_offchain_tx_pool_factory.clone(),
690        }
691    }
692}
693
694pub struct MismatchedReceipts<Block, CBlock>
695where
696    Block: BlockT,
697    CBlock: BlockT,
698{
699    local_receipt: ExecutionReceiptFor<Block, CBlock>,
700    bad_receipt: ExecutionReceiptFor<Block, CBlock>,
701}
702
703impl<Block, Client, CBlock, CClient, Backend, E>
704    ReceiptsChecker<Block, Client, CBlock, CClient, Backend, E>
705where
706    Block: BlockT,
707    Block::Hash: Into<H256>,
708    CBlock: BlockT,
709    NumberFor<CBlock>: Into<NumberFor<Block>>,
710    Client: HeaderBackend<Block>
711        + BlockBackend<Block>
712        + ProofProvider<Block>
713        + AuxStore
714        + ExecutorProvider<Block>
715        + ProvideRuntimeApi<Block>
716        + 'static,
717    Client::Api: DomainCoreApi<Block>
718        + sp_block_builder::BlockBuilder<Block>
719        + sp_api::ApiExt<Block>
720        + MessengerApi<Block, NumberFor<CBlock>, CBlock::Hash>,
721    CClient: HeaderBackend<CBlock>
722        + BlockBackend<CBlock>
723        + ProofProvider<CBlock>
724        + ProvideRuntimeApi<CBlock>
725        + 'static,
726    CClient::Api: DomainsApi<CBlock, Block::Header>
727        + FraudProofApi<CBlock, Block::Header>
728        + MmrApi<CBlock, H256, NumberFor<CBlock>>,
729    Backend: sc_client_api::Backend<Block> + 'static,
730    E: CodeExecutor + RuntimeVersionOf,
731{
732    pub(crate) fn maybe_submit_fraud_proof(
733        &self,
734        consensus_block_hash: CBlock::Hash,
735    ) -> sp_blockchain::Result<()> {
736        if self.domain_sync_oracle.is_major_syncing() {
737            tracing::debug!(
738                "Skip reporting unconfirmed bad receipt as the consensus node is still major syncing..."
739            );
740            return Ok(());
741        }
742
743        if let Some(mismatched_receipts) = self.find_mismatch_receipt(consensus_block_hash)? {
744            let fraud_proof = self.generate_fraud_proof(mismatched_receipts)?;
745            tracing::info!("Submit fraud proof: {fraud_proof:?}");
746
747            let consensus_best_hash = self.consensus_client.info().best_hash;
748            let mut consensus_runtime_api = self.consensus_client.runtime_api();
749            consensus_runtime_api.register_extension(
750                self.consensus_offchain_tx_pool_factory
751                    .offchain_transaction_pool(consensus_best_hash),
752            );
753            consensus_runtime_api.submit_fraud_proof_unsigned(consensus_best_hash, fraud_proof)?;
754        }
755
756        Ok(())
757    }
758
759    pub fn find_mismatch_receipt(
760        &self,
761        consensus_block_hash: CBlock::Hash,
762    ) -> sp_blockchain::Result<Option<MismatchedReceipts<Block, CBlock>>> {
763        let mut oldest_mismatch = None;
764        let mut to_check = self
765            .consensus_client
766            .runtime_api()
767            .head_receipt_number(consensus_block_hash, self.domain_id)?;
768        let oldest_unconfirmed_receipt_number = match self
769            .consensus_client
770            .runtime_api()
771            .oldest_unconfirmed_receipt_number(consensus_block_hash, self.domain_id)?
772        {
773            Some(er_number) => er_number,
774            // Early return if there is no non-confirmed ER exist
775            None => return Ok(None),
776        };
777
778        while !to_check.is_zero() && oldest_unconfirmed_receipt_number <= to_check {
779            let onchain_receipt_hash = self
780                .consensus_client
781                .runtime_api()
782                .receipt_hash(consensus_block_hash, self.domain_id, to_check)?
783                .ok_or_else(|| {
784                    sp_blockchain::Error::Application(
785                        format!("Receipt hash for #{to_check:?} not found").into(),
786                    )
787                })?;
788            let local_receipt = {
789                // Get the domain block hash corresponding to `to_check` in the
790                // domain canonical chain
791                let domain_hash = self.client.hash(to_check)?.ok_or_else(|| {
792                    sp_blockchain::Error::Backend(format!(
793                        "Domain block hash for #{to_check:?} not found"
794                    ))
795                })?;
796                crate::load_execution_receipt_by_domain_hash::<Block, CBlock, _>(
797                    &*self.client,
798                    domain_hash,
799                    to_check,
800                )?
801            };
802            if local_receipt.hash::<HeaderHashingFor<Block::Header>>() != onchain_receipt_hash {
803                oldest_mismatch.replace((local_receipt, onchain_receipt_hash));
804                to_check = to_check.saturating_sub(One::one());
805            } else {
806                break;
807            }
808        }
809
810        match oldest_mismatch {
811            None => Ok(None),
812            Some((local_receipt, bad_receipt_hash)) => {
813                let bad_receipt = self
814                    .consensus_client
815                    .runtime_api()
816                    .execution_receipt(consensus_block_hash, bad_receipt_hash)?
817                    .ok_or_else(|| {
818                        sp_blockchain::Error::Application(
819                            format!("Receipt for #{bad_receipt_hash:?} not found").into(),
820                        )
821                    })?;
822                debug_assert_eq!(
823                    local_receipt.consensus_block_hash(),
824                    bad_receipt.consensus_block_hash(),
825                );
826                Ok(Some(MismatchedReceipts {
827                    local_receipt,
828                    bad_receipt,
829                }))
830            }
831        }
832    }
833
834    #[allow(clippy::type_complexity)]
835    pub fn generate_fraud_proof(
836        &self,
837        mismatched_receipts: MismatchedReceipts<Block, CBlock>,
838    ) -> sp_blockchain::Result<FraudProofFor<CBlock, Block::Header>> {
839        let MismatchedReceipts {
840            local_receipt,
841            bad_receipt,
842        } = mismatched_receipts;
843
844        let bad_receipt_hash = bad_receipt.hash::<HeaderHashingFor<Block::Header>>();
845
846        // NOTE: the checking order MUST follow exactly as the dependency order of fraud proof
847        // see https://github.com/autonomys/subspace/issues/1892
848        if let Some(InboxedBundleMismatchInfo {
849            bundle_index,
850            mismatch_type,
851        }) = find_inboxed_bundles_mismatch::<Block, CBlock>(&local_receipt, &bad_receipt)?
852        {
853            return match mismatch_type {
854                BundleMismatchType::ValidBundleContents => self
855                    .fraud_proof_generator
856                    .generate_valid_bundle_proof(
857                        self.domain_id,
858                        &local_receipt,
859                        bundle_index as usize,
860                        bad_receipt_hash,
861                    )
862                    .map_err(|err| {
863                        sp_blockchain::Error::Application(Box::from(format!(
864                            "Failed to generate valid bundles fraud proof: {err}"
865                        )))
866                    }),
867                _ => self
868                    .fraud_proof_generator
869                    .generate_invalid_bundle_proof(
870                        self.domain_id,
871                        &local_receipt,
872                        mismatch_type,
873                        bundle_index,
874                        bad_receipt_hash,
875                    )
876                    .map_err(|err| {
877                        sp_blockchain::Error::Application(Box::from(format!(
878                            "Failed to generate invalid bundles fraud proof: {err}"
879                        )))
880                    }),
881            };
882        }
883
884        if bad_receipt.domain_block_extrinsics_root()
885            != local_receipt.domain_block_extrinsics_root()
886        {
887            return self
888                .fraud_proof_generator
889                .generate_invalid_domain_extrinsics_root_proof(
890                    self.domain_id,
891                    &local_receipt,
892                    bad_receipt_hash,
893                )
894                .map_err(|err| {
895                    sp_blockchain::Error::Application(Box::from(format!(
896                        "Failed to generate invalid domain extrinsics root fraud proof: {err}"
897                    )))
898                });
899        }
900
901        if let Some(execution_phase) = self
902            .fraud_proof_generator
903            .find_mismatched_execution_phase(
904                *local_receipt.domain_block_hash(),
905                local_receipt.execution_traces(),
906                bad_receipt.execution_traces(),
907            )
908            .map_err(|err| {
909                sp_blockchain::Error::Application(Box::from(format!(
910                    "Failed to find mismatched execution phase: {err}"
911                )))
912            })?
913        {
914            return self
915                .fraud_proof_generator
916                .generate_invalid_state_transition_proof(
917                    self.domain_id,
918                    execution_phase,
919                    &local_receipt,
920                    bad_receipt.execution_traces().len(),
921                    bad_receipt_hash,
922                )
923                .map_err(|err| {
924                    sp_blockchain::Error::Application(Box::from(format!(
925                        "Failed to generate invalid state transition fraud proof: {err}"
926                    )))
927                });
928        }
929
930        if bad_receipt.block_fees() != local_receipt.block_fees() {
931            return self
932                .fraud_proof_generator
933                .generate_invalid_block_fees_proof(self.domain_id, &local_receipt, bad_receipt_hash)
934                .map_err(|err| {
935                    sp_blockchain::Error::Application(Box::from(format!(
936                        "Failed to generate invalid block rewards fraud proof: {err}"
937                    )))
938                });
939        }
940
941        if bad_receipt.transfers() != local_receipt.transfers() {
942            return self
943                .fraud_proof_generator
944                .generate_invalid_transfers_proof(self.domain_id, &local_receipt, bad_receipt_hash)
945                .map_err(|err| {
946                    sp_blockchain::Error::Application(Box::from(format!(
947                        "Failed to generate invalid transfers fraud proof: {err}"
948                    )))
949                });
950        }
951
952        if bad_receipt.domain_block_hash() != local_receipt.domain_block_hash() {
953            return self
954                .fraud_proof_generator
955                .generate_invalid_domain_block_hash_proof(
956                    self.domain_id,
957                    &local_receipt,
958                    bad_receipt_hash,
959                )
960                .map_err(|err| {
961                    sp_blockchain::Error::Application(Box::from(format!(
962                        "Failed to generate invalid domain block hash fraud proof: {err}"
963                    )))
964                });
965        }
966
967        Err(sp_blockchain::Error::Application(Box::from(format!(
968            "No fraudulent field found for the mismatched ER, this should not happen, \
969            local_receipt {local_receipt:?}, bad_receipt {bad_receipt:?}"
970        ))))
971    }
972}
973
974#[cfg(test)]
975mod tests {
976    use super::*;
977    use domain_test_service::evm_domain_test_runtime::Block;
978    use sp_domains::bundle::{InboxedBundle, InvalidBundleType};
979    use subspace_runtime_primitives::BlockHashFor;
980    use subspace_test_runtime::Block as CBlock;
981
982    fn create_test_execution_receipt(
983        inboxed_bundles: Vec<InboxedBundle<BlockHashFor<Block>>>,
984    ) -> ExecutionReceiptFor<Block, CBlock>
985    where
986        Block: BlockT,
987        CBlock: BlockT,
988    {
989        ExecutionReceipt::V0(ExecutionReceiptV0 {
990            domain_block_number: Zero::zero(),
991            domain_block_hash: Default::default(),
992            domain_block_extrinsic_root: Default::default(),
993            parent_domain_block_receipt_hash: Default::default(),
994            consensus_block_hash: Default::default(),
995            consensus_block_number: Zero::zero(),
996            inboxed_bundles,
997            final_state_root: Default::default(),
998            execution_trace: vec![],
999            execution_trace_root: Default::default(),
1000            block_fees: Default::default(),
1001            transfers: Default::default(),
1002        })
1003    }
1004
1005    #[test]
1006    fn er_bundles_mismatch_detection() {
1007        // If empty invalid receipt field on both should result in no fraud proof
1008        assert_eq!(
1009            find_inboxed_bundles_mismatch::<Block, CBlock>(
1010                &create_test_execution_receipt(vec![]),
1011                &create_test_execution_receipt(vec![]),
1012            )
1013            .unwrap(),
1014            None
1015        );
1016
1017        assert_eq!(
1018            find_inboxed_bundles_mismatch::<Block, CBlock>(
1019                &create_test_execution_receipt(vec![InboxedBundle::invalid(
1020                    InvalidBundleType::UndecodableTx(0),
1021                    Default::default(),
1022                )]),
1023                &create_test_execution_receipt(vec![InboxedBundle::invalid(
1024                    InvalidBundleType::UndecodableTx(0),
1025                    Default::default(),
1026                )]),
1027            )
1028            .unwrap(),
1029            None
1030        );
1031
1032        // Mismatch in valid bundle
1033        assert_eq!(
1034            find_inboxed_bundles_mismatch::<Block, CBlock>(
1035                &create_test_execution_receipt(vec![
1036                    InboxedBundle::invalid(InvalidBundleType::UndecodableTx(0), Default::default()),
1037                    InboxedBundle::valid(H256::random(), Default::default()),
1038                ]),
1039                &create_test_execution_receipt(vec![
1040                    InboxedBundle::invalid(InvalidBundleType::UndecodableTx(0), Default::default()),
1041                    InboxedBundle::valid(H256::random(), Default::default()),
1042                ]),
1043            )
1044            .unwrap(),
1045            Some(InboxedBundleMismatchInfo {
1046                mismatch_type: BundleMismatchType::ValidBundleContents,
1047                bundle_index: 1,
1048            })
1049        );
1050
1051        // Mismatch in invalid extrinsic index
1052        assert_eq!(
1053            find_inboxed_bundles_mismatch::<Block, CBlock>(
1054                &create_test_execution_receipt(vec![
1055                    InboxedBundle::valid(Default::default(), Default::default()),
1056                    InboxedBundle::invalid(InvalidBundleType::UndecodableTx(1), Default::default()),
1057                ]),
1058                &create_test_execution_receipt(vec![
1059                    InboxedBundle::valid(Default::default(), Default::default()),
1060                    InboxedBundle::invalid(InvalidBundleType::UndecodableTx(2), Default::default()),
1061                ]),
1062            )
1063            .unwrap(),
1064            Some(InboxedBundleMismatchInfo {
1065                mismatch_type: BundleMismatchType::GoodInvalid(InvalidBundleType::UndecodableTx(1)),
1066                bundle_index: 1,
1067            })
1068        );
1069        assert_eq!(
1070            find_inboxed_bundles_mismatch::<Block, CBlock>(
1071                &create_test_execution_receipt(vec![
1072                    InboxedBundle::valid(Default::default(), Default::default()),
1073                    InboxedBundle::invalid(InvalidBundleType::UndecodableTx(4), Default::default()),
1074                ]),
1075                &create_test_execution_receipt(vec![
1076                    InboxedBundle::valid(Default::default(), Default::default()),
1077                    InboxedBundle::invalid(InvalidBundleType::UndecodableTx(3), Default::default()),
1078                ]),
1079            )
1080            .unwrap(),
1081            Some(InboxedBundleMismatchInfo {
1082                mismatch_type: BundleMismatchType::BadInvalid(InvalidBundleType::UndecodableTx(3)),
1083                bundle_index: 1,
1084            })
1085        );
1086        // Even the invalid type is mismatch, the extrinsic index mismatch should be considered first
1087        assert_eq!(
1088            find_inboxed_bundles_mismatch::<Block, CBlock>(
1089                &create_test_execution_receipt(vec![
1090                    InboxedBundle::valid(Default::default(), Default::default()),
1091                    InboxedBundle::invalid(InvalidBundleType::UndecodableTx(4), Default::default()),
1092                ]),
1093                &create_test_execution_receipt(vec![
1094                    InboxedBundle::valid(Default::default(), Default::default()),
1095                    InboxedBundle::invalid(InvalidBundleType::IllegalTx(3), Default::default()),
1096                ]),
1097            )
1098            .unwrap(),
1099            Some(InboxedBundleMismatchInfo {
1100                mismatch_type: BundleMismatchType::BadInvalid(InvalidBundleType::IllegalTx(3)),
1101                bundle_index: 1,
1102            })
1103        );
1104
1105        // Mismatch in invalid type
1106        assert_eq!(
1107            find_inboxed_bundles_mismatch::<Block, CBlock>(
1108                &create_test_execution_receipt(vec![
1109                    InboxedBundle::valid(Default::default(), Default::default()),
1110                    InboxedBundle::invalid(InvalidBundleType::IllegalTx(3), Default::default()),
1111                ]),
1112                &create_test_execution_receipt(vec![
1113                    InboxedBundle::valid(Default::default(), Default::default()),
1114                    InboxedBundle::invalid(
1115                        InvalidBundleType::InherentExtrinsic(3),
1116                        Default::default()
1117                    ),
1118                ]),
1119            )
1120            .unwrap(),
1121            Some(InboxedBundleMismatchInfo {
1122                mismatch_type: BundleMismatchType::BadInvalid(
1123                    InvalidBundleType::InherentExtrinsic(3)
1124                ),
1125                bundle_index: 1,
1126            })
1127        );
1128
1129        assert_eq!(
1130            find_inboxed_bundles_mismatch::<Block, CBlock>(
1131                &create_test_execution_receipt(vec![
1132                    InboxedBundle::valid(Default::default(), Default::default()),
1133                    InboxedBundle::invalid(
1134                        InvalidBundleType::InherentExtrinsic(3),
1135                        Default::default()
1136                    ),
1137                ]),
1138                &create_test_execution_receipt(vec![
1139                    InboxedBundle::valid(Default::default(), Default::default()),
1140                    InboxedBundle::invalid(InvalidBundleType::IllegalTx(3), Default::default()),
1141                ]),
1142            )
1143            .unwrap(),
1144            Some(InboxedBundleMismatchInfo {
1145                mismatch_type: BundleMismatchType::GoodInvalid(
1146                    InvalidBundleType::InherentExtrinsic(3)
1147                ),
1148                bundle_index: 1,
1149            })
1150        );
1151
1152        // Only first mismatch is detected
1153        assert_eq!(
1154            find_inboxed_bundles_mismatch::<Block, CBlock>(
1155                &create_test_execution_receipt(vec![
1156                    InboxedBundle::valid(H256::random(), Default::default()),
1157                    InboxedBundle::invalid(
1158                        InvalidBundleType::InherentExtrinsic(3),
1159                        Default::default()
1160                    ),
1161                ]),
1162                &create_test_execution_receipt(vec![
1163                    InboxedBundle::valid(H256::random(), Default::default()),
1164                    InboxedBundle::invalid(InvalidBundleType::IllegalTx(3), Default::default()),
1165                ]),
1166            )
1167            .unwrap(),
1168            Some(InboxedBundleMismatchInfo {
1169                mismatch_type: BundleMismatchType::ValidBundleContents,
1170                bundle_index: 0,
1171            })
1172        );
1173
1174        // Taking valid bundle as invalid
1175        assert_eq!(
1176            find_inboxed_bundles_mismatch::<Block, CBlock>(
1177                &create_test_execution_receipt(vec![InboxedBundle::valid(
1178                    H256::random(),
1179                    Default::default(),
1180                ),]),
1181                &create_test_execution_receipt(vec![InboxedBundle::invalid(
1182                    InvalidBundleType::IllegalTx(3),
1183                    Default::default(),
1184                ),]),
1185            )
1186            .unwrap(),
1187            Some(InboxedBundleMismatchInfo {
1188                mismatch_type: BundleMismatchType::BadInvalid(InvalidBundleType::IllegalTx(3)),
1189                bundle_index: 0,
1190            })
1191        );
1192
1193        // Taking invalid as valid
1194        assert_eq!(
1195            find_inboxed_bundles_mismatch::<Block, CBlock>(
1196                &create_test_execution_receipt(vec![InboxedBundle::invalid(
1197                    InvalidBundleType::IllegalTx(3),
1198                    Default::default(),
1199                ),]),
1200                &create_test_execution_receipt(vec![InboxedBundle::valid(
1201                    H256::random(),
1202                    Default::default(),
1203                ),]),
1204            )
1205            .unwrap(),
1206            Some(InboxedBundleMismatchInfo {
1207                mismatch_type: BundleMismatchType::GoodInvalid(InvalidBundleType::IllegalTx(3)),
1208                bundle_index: 0,
1209            })
1210        );
1211    }
1212}