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
55pub(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#[derive(Debug)]
102pub(crate) struct PendingConsensusBlocks<Block: BlockT, CBlock: BlockT> {
103 pub initial_parent: (Block::Hash, NumberFor<Block>),
105 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 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 Ordering::Less => return Ok(None),
148 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 Ordering::Greater => {}
162 }
163
164 let best_hash = self.client.info().best_hash;
165 let best_number = self.client.info().best_number;
166
167 let consensus_block_hash_for_best_domain_hash =
171 match crate::aux_schema::latest_consensus_block_hash_for(&*self.backend, &best_hash)? {
172 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 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 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 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 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 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 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 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 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 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
580pub(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 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 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 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 Ordering::Less => BundleMismatchType::GoodInvalid(local_invalid_type),
632 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 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 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 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 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 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 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 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 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 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 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 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}