domain_client_operator/
domain_block_processor.rs

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