diff --git a/Cargo.lock b/Cargo.lock index bebb663ce4..38719f0d02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10830,6 +10830,7 @@ dependencies = [ "frame-support", "hash-db 0.16.0", "parity-scale-codec", + "sc-client-api", "sc-executor", "scale-info", "sp-api", diff --git a/crates/pallet-domains/src/lib.rs b/crates/pallet-domains/src/lib.rs index d79ca70389..1d64c50089 100644 --- a/crates/pallet-domains/src/lib.rs +++ b/crates/pallet-domains/src/lib.rs @@ -57,7 +57,7 @@ use sp_domains_fraud_proof::fraud_proof::{ use sp_domains_fraud_proof::verification::{ verify_invalid_domain_block_hash_fraud_proof, verify_invalid_domain_extrinsics_root_fraud_proof, verify_invalid_state_transition_fraud_proof, - verify_invalid_total_rewards_fraud_proof, + verify_invalid_total_rewards_fraud_proof, verify_valid_bundle_fraud_proof, }; use sp_runtime::traits::{BlakeTwo256, CheckedSub, Hash, Header, One, Zero}; use sp_runtime::{RuntimeAppPublic, SaturatedConversion, Saturating}; @@ -614,6 +614,8 @@ mod pallet { InvalidStateTransitionFraudProof, /// Parent receipt not found. ParentReceiptNotFound, + /// Bad/Invalid valid bundle fraud proof + BadValidBundleFraudProof, } impl From for Error { @@ -1608,6 +1610,19 @@ impl Pallet { FraudProofError::InvalidStateTransitionFraudProof })?; } + FraudProof::ValidBundle(proof) => verify_valid_bundle_fraud_proof::< + T::Block, + DomainBlockNumberFor, + T::DomainHash, + BalanceOf, + >(bad_receipt, proof) + .map_err(|err| { + log::error!( + target: "runtime::domains", + "Valid bundle proof verification failed: {err:?}" + ); + FraudProofError::BadValidBundleFraudProof + })?, _ => {} } diff --git a/crates/pallet-domains/src/tests.rs b/crates/pallet-domains/src/tests.rs index 8a42ff89e2..6f9e8affef 100644 --- a/crates/pallet-domains/src/tests.rs +++ b/crates/pallet-domains/src/tests.rs @@ -277,6 +277,9 @@ impl FraudProofHostFunctions for MockDomainFraudProofExtension { .encode(), ) } + FraudProofVerificationInfoRequest::DomainBundleBody { .. } => { + FraudProofVerificationInfoResponse::DomainBundleBody(Default::default()) + } FraudProofVerificationInfoRequest::DomainRuntimeCode(_) => { FraudProofVerificationInfoResponse::DomainRuntimeCode(Default::default()) } @@ -298,6 +301,15 @@ impl FraudProofHostFunctions for MockDomainFraudProofExtension { Some(response) } + fn derive_bundle_digest( + &self, + _consensus_block_hash: H256, + _domain_id: DomainId, + _bundle_body: Vec, + ) -> Option { + Some(H256::random()) + } + fn execution_proof_check( &self, _pre_state_root: H256, diff --git a/crates/sp-domains-fraud-proof/Cargo.toml b/crates/sp-domains-fraud-proof/Cargo.toml index aba3a666ec..f3ed638f83 100644 --- a/crates/sp-domains-fraud-proof/Cargo.toml +++ b/crates/sp-domains-fraud-proof/Cargo.toml @@ -16,6 +16,7 @@ domain-runtime-primitives = { version = "0.1.0", default-features = false, path frame-support = { version = "4.0.0-dev", default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "892bf8e938c6bd2b893d3827d1093cd81baa59a1" } hash-db = { version = "0.16.0", default-features = false } scale-info = { version = "2.7.0", default-features = false, features = ["derive"] } +sc-client-api = { version = "4.0.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "892bf8e938c6bd2b893d3827d1093cd81baa59a1", optional = true } domain-block-preprocessor = { version = "0.1.0", default-features = false, path = "../../domains/client/block-preprocessor", optional = true } sc-executor = { version = "0.10.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "892bf8e938c6bd2b893d3827d1093cd81baa59a1", default-features = false, optional = true } sp-api = { version = "4.0.0-dev", default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "892bf8e938c6bd2b893d3827d1093cd81baa59a1" } @@ -43,6 +44,8 @@ std = [ "frame-support/std", "hash-db/std", "scale-info/std", + "sc-client-api", + "sc-executor/std", "domain-block-preprocessor", "sc-executor", "sp-api/std", diff --git a/crates/sp-domains-fraud-proof/src/fraud_proof.rs b/crates/sp-domains-fraud-proof/src/fraud_proof.rs index 34bb189e9b..30c062fcec 100644 --- a/crates/sp-domains-fraud-proof/src/fraud_proof.rs +++ b/crates/sp-domains-fraud-proof/src/fraud_proof.rs @@ -288,6 +288,18 @@ pub enum VerificationError { error("Failed to derive domain set code extrinsic") )] FailedToDeriveDomainSetCodeExtrinsic, + /// Failed to get the bundle body + #[cfg_attr(feature = "thiserror", error("Failed to get the bundle body"))] + FailedToGetDomainBundleBody, + /// Failed to derive bundle digest + #[cfg_attr(feature = "thiserror", error("Failed to derive bundle digest"))] + FailedToDeriveBundleDigest, + /// The target valid bundle not found from the target bad receipt + #[cfg_attr( + feature = "thiserror", + error("The target valid bundle not found from the target bad receipt") + )] + TargetValidBundleNotFound, } // TODO: Define rest of the fraud proof fields @@ -349,6 +361,7 @@ pub enum FraudProof { ImproperTransactionSortition(ImproperTransactionSortitionProof), InvalidTotalRewards(InvalidTotalRewardsProof), InvalidExtrinsicsRoot(InvalidExtrinsicsRootProof), + ValidBundle(ValidBundleProof), InvalidDomainBlockHash(InvalidDomainBlockHashProof), // Dummy fraud proof only used in test and benchmark #[cfg(any(feature = "std", feature = "runtime-benchmarks"))] @@ -370,10 +383,11 @@ impl FraudProof { Self::ImproperTransactionSortition(proof) => proof.domain_id, #[cfg(any(feature = "std", feature = "runtime-benchmarks"))] Self::Dummy { domain_id, .. } => *domain_id, - FraudProof::InvalidTotalRewards(proof) => proof.domain_id(), - FraudProof::InvalidBundles(proof) => proof.domain_id(), - FraudProof::InvalidExtrinsicsRoot(proof) => proof.domain_id, - FraudProof::InvalidDomainBlockHash(proof) => proof.domain_id, + Self::InvalidTotalRewards(proof) => proof.domain_id(), + Self::InvalidExtrinsicsRoot(proof) => proof.domain_id, + Self::InvalidBundles(proof) => proof.domain_id(), + Self::ValidBundle(proof) => proof.domain_id, + Self::InvalidDomainBlockHash(proof) => proof.domain_id, } } @@ -390,11 +404,12 @@ impl FraudProof { Self::Dummy { bad_receipt_hash, .. } => *bad_receipt_hash, - FraudProof::InvalidTotalRewards(proof) => proof.bad_receipt_hash(), + Self::InvalidExtrinsicsRoot(proof) => proof.bad_receipt_hash, + Self::InvalidTotalRewards(proof) => proof.bad_receipt_hash(), + Self::ValidBundle(proof) => proof.bad_receipt_hash, // TODO: Remove default value when invalid bundle proofs are fully expanded - FraudProof::InvalidBundles(_) => Default::default(), - FraudProof::InvalidExtrinsicsRoot(proof) => proof.bad_receipt_hash, - FraudProof::InvalidDomainBlockHash(proof) => proof.bad_receipt_hash, + Self::InvalidBundles(_) => Default::default(), + Self::InvalidDomainBlockHash(proof) => proof.bad_receipt_hash, } } @@ -589,6 +604,17 @@ pub fn operator_block_rewards_final_key() -> Vec { .to_vec() } +/// Fraud proof for the valid bundles in `ExecutionReceipt::inboxed_bundles` +#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] +pub struct ValidBundleProof { + /// The id of the domain this fraud proof targeted + pub domain_id: DomainId, + /// The targetted bad receipt + pub bad_receipt_hash: H256, + /// The index of the targetted bundle + pub bundle_index: u32, +} + /// Digest storage key in frame_system. /// Unfortunately, the digest storage is private and not possible to derive the key from it directly. pub fn system_digest_final_key() -> Vec { diff --git a/crates/sp-domains-fraud-proof/src/host_functions.rs b/crates/sp-domains-fraud-proof/src/host_functions.rs index f57aae19c9..9062506ce7 100644 --- a/crates/sp-domains-fraud-proof/src/host_functions.rs +++ b/crates/sp-domains-fraud-proof/src/host_functions.rs @@ -3,15 +3,20 @@ use crate::{ }; use codec::{Decode, Encode}; use domain_block_preprocessor::inherents::extract_domain_runtime_upgrade_code; -use domain_block_preprocessor::runtime_api::{SetCodeConstructor, TimestampExtrinsicConstructor}; +use domain_block_preprocessor::runtime_api::{ + SetCodeConstructor, SignerExtractor, TimestampExtrinsicConstructor, +}; use domain_block_preprocessor::runtime_api_light::RuntimeApiLight; +use sc_client_api::BlockBackend; use sc_executor::RuntimeVersionOf; -use sp_api::{BlockT, ProvideRuntimeApi}; +use sp_api::{BlockT, HashT, ProvideRuntimeApi}; use sp_blockchain::HeaderBackend; use sp_core::traits::{CodeExecutor, FetchRuntimeCode, RuntimeCode}; use sp_core::H256; use sp_domains::{DomainId, DomainsApi}; use sp_runtime::traits::{Header as HeaderT, NumberFor}; +use sp_runtime::OpaqueExtrinsic; +use sp_std::vec::Vec; use sp_trie::StorageProof; use std::borrow::Cow; use std::marker::PhantomData; @@ -35,6 +40,15 @@ pub trait FraudProofHostFunctions: Send + Sync { fraud_proof_verification_req: FraudProofVerificationInfoRequest, ) -> Option; + /// Derive the bundle digest for the given bundle body. + fn derive_bundle_digest( + &self, + consensus_block_hash: H256, + domain_id: DomainId, + bundle_body: Vec, + ) -> Option; + + /// Check the execution proof fn execution_proof_check( &self, pre_state_root: H256, @@ -83,7 +97,7 @@ where Block: BlockT, Block::Hash: From, DomainBlock: BlockT, - Client: HeaderBackend + ProvideRuntimeApi, + Client: BlockBackend + HeaderBackend + ProvideRuntimeApi, Client::Api: DomainsApi, DomainBlock::Hash>, Executor: CodeExecutor + RuntimeVersionOf, { @@ -118,6 +132,30 @@ where .map(|ext| ext.encode()) } + fn get_domain_bundle_body( + &self, + consensus_block_hash: H256, + domain_id: DomainId, + bundle_index: u32, + ) -> Option> { + let consensus_block_hash = consensus_block_hash.into(); + let consensus_extrinsics = self + .consensus_client + .block_body(consensus_block_hash) + .ok()??; + let mut bundles = self + .consensus_client + .runtime_api() + .extract_successful_bundles(consensus_block_hash, domain_id, consensus_extrinsics) + .ok()?; + + if bundle_index < bundles.len() as u32 { + Some(bundles.swap_remove(bundle_index as usize).extrinsics) + } else { + None + } + } + fn derive_domain_set_code_extrinsic( &self, consensus_block_hash: H256, @@ -176,8 +214,8 @@ where Block: BlockT, Block::Hash: From, DomainBlock: BlockT, - DomainBlock::Hash: From, - Client: HeaderBackend + ProvideRuntimeApi, + DomainBlock::Hash: Into + From, + Client: BlockBackend + HeaderBackend + ProvideRuntimeApi, Client::Api: DomainsApi, DomainBlock::Hash>, Executor: CodeExecutor + RuntimeVersionOf, { @@ -199,6 +237,14 @@ where domain_timestamp_extrinsic, ) }), + FraudProofVerificationInfoRequest::DomainBundleBody { + domain_id, + bundle_index, + } => self + .get_domain_bundle_body(consensus_block_hash, domain_id, bundle_index) + .map(|domain_bundle_body| { + FraudProofVerificationInfoResponse::DomainBundleBody(domain_bundle_body) + }), FraudProofVerificationInfoRequest::DomainRuntimeCode(domain_id) => self .get_domain_runtime_code(consensus_block_hash, domain_id) .map(|domain_runtime_code| { @@ -214,6 +260,45 @@ where } } + fn derive_bundle_digest( + &self, + consensus_block_hash: H256, + domain_id: DomainId, + bundle_body: Vec, + ) -> Option { + let mut extrinsics = Vec::with_capacity(bundle_body.len()); + for opaque_extrinsic in bundle_body { + let ext = <::Extrinsic>::decode( + &mut opaque_extrinsic.encode().as_slice(), + ) + .ok()?; + extrinsics.push(ext); + } + + let domain_runtime_code = self.get_domain_runtime_code(consensus_block_hash, domain_id)?; + let domain_runtime_api_light = + RuntimeApiLight::new(self.executor.clone(), domain_runtime_code.into()); + + let ext_signers: Vec<_> = SignerExtractor::::extract_signer( + &domain_runtime_api_light, + // `extract_signer` is a stateless runtime api thus it is okay to use + // default block hash + Default::default(), + extrinsics, + ) + .ok()? + .into_iter() + .map(|(signer, tx)| { + ( + signer, + ::Hashing::hash_of(&tx), + ) + }) + .collect(); + + Some(::Hashing::hash_of(&ext_signers).into()) + } + fn execution_proof_check( &self, pre_state_root: H256, diff --git a/crates/sp-domains-fraud-proof/src/lib.rs b/crates/sp-domains-fraud-proof/src/lib.rs index bdb3e4be8a..e4afda1092 100644 --- a/crates/sp-domains-fraud-proof/src/lib.rs +++ b/crates/sp-domains-fraud-proof/src/lib.rs @@ -34,6 +34,7 @@ pub use runtime_interface::fraud_proof_runtime_interface; pub use runtime_interface::fraud_proof_runtime_interface::HostFunctions; use sp_api::scale_info::TypeInfo; use sp_domains::DomainId; +use sp_runtime::OpaqueExtrinsic; use sp_runtime_interface::pass_by; use sp_runtime_interface::pass_by::PassBy; use sp_std::vec::Vec; @@ -46,6 +47,11 @@ pub enum FraudProofVerificationInfoRequest { BlockRandomness, /// Domain timestamp extrinsic using the timestamp at a given consensus block hash. DomainTimestampExtrinsic(DomainId), + /// The body of domain bundle included in a given consensus block at a given index + DomainBundleBody { + domain_id: DomainId, + bundle_index: u32, + }, /// The domain runtime code DomainRuntimeCode(DomainId), /// Domain set_code extrinsic if there is a runtime upgrade at a given consensus block hash. @@ -72,6 +78,8 @@ pub enum FraudProofVerificationInfoResponse { BlockRandomness(Randomness), /// Encoded domain timestamp extrinsic using the timestamp from consensus state at a specific block hash. DomainTimestampExtrinsic(Vec), + /// Domain block body fetch from a specific consensus block body + DomainBundleBody(Vec), /// The domain runtime code DomainRuntimeCode(Vec), /// Encoded domain set_code extrinsic if there is a runtime upgrade at given consensus block hash. @@ -108,4 +116,11 @@ impl FraudProofVerificationInfoResponse { _ => SetCodeExtrinsic::None, } } + + pub fn into_bundle_body(self) -> Option> { + match self { + Self::DomainBundleBody(bb) => Some(bb), + _ => None, + } + } } diff --git a/crates/sp-domains-fraud-proof/src/runtime_interface.rs b/crates/sp-domains-fraud-proof/src/runtime_interface.rs index 80a524759e..fba4ff1997 100644 --- a/crates/sp-domains-fraud-proof/src/runtime_interface.rs +++ b/crates/sp-domains-fraud-proof/src/runtime_interface.rs @@ -2,8 +2,10 @@ use crate::FraudProofExtension; use crate::{FraudProofVerificationInfoRequest, FraudProofVerificationInfoResponse}; use sp_core::H256; +use sp_domains::DomainId; #[cfg(feature = "std")] use sp_externalities::ExternalitiesExt; +use sp_runtime::OpaqueExtrinsic; use sp_runtime_interface::runtime_interface; use sp_std::vec::Vec; @@ -21,6 +23,18 @@ pub trait FraudProofRuntimeInterface { .get_fraud_proof_verification_info(consensus_block_hash, fraud_proof_verification_req) } + /// Derive the bundle digest for the given bundle body. + fn derive_bundle_digest( + &mut self, + consensus_block_hash: H256, + domain_id: DomainId, + bundle_body: Vec, + ) -> Option { + self.extension::() + .expect("No `FraudProofExtension` associated for the current context!") + .derive_bundle_digest(consensus_block_hash, domain_id, bundle_body) + } + /// Check the execution proof fn execution_proof_check( &mut self, diff --git a/crates/sp-domains-fraud-proof/src/verification.rs b/crates/sp-domains-fraud-proof/src/verification.rs index 5de58a13df..7200188fdb 100644 --- a/crates/sp-domains-fraud-proof/src/verification.rs +++ b/crates/sp-domains-fraud-proof/src/verification.rs @@ -1,5 +1,6 @@ use crate::fraud_proof::{ - ExtrinsicDigest, InvalidExtrinsicsRootProof, InvalidStateTransitionProof, VerificationError, + ExtrinsicDigest, InvalidExtrinsicsRootProof, InvalidStateTransitionProof, ValidBundleProof, + VerificationError, }; use crate::fraud_proof_runtime_interface::get_fraud_proof_verification_info; use crate::{ @@ -124,6 +125,55 @@ where Ok(()) } +/// Verifies valid bundle fraud proof. +pub fn verify_valid_bundle_fraud_proof( + bad_receipt: ExecutionReceipt< + NumberFor, + CBlock::Hash, + DomainNumber, + DomainHash, + Balance, + >, + fraud_proof: &ValidBundleProof, +) -> Result<(), VerificationError> +where + CBlock: BlockT, + CBlock::Hash: Into, +{ + let ValidBundleProof { + domain_id, + bundle_index, + .. + } = fraud_proof; + + let bundle_body = fraud_proof_runtime_interface::get_fraud_proof_verification_info( + bad_receipt.consensus_block_hash.into(), + FraudProofVerificationInfoRequest::DomainBundleBody { + domain_id: *domain_id, + bundle_index: *bundle_index, + }, + ) + .and_then(FraudProofVerificationInfoResponse::into_bundle_body) + .ok_or(VerificationError::FailedToGetDomainBundleBody)?; + + let valid_bundle_digest = fraud_proof_runtime_interface::derive_bundle_digest( + bad_receipt.consensus_block_hash.into(), + *domain_id, + bundle_body, + ) + .ok_or(VerificationError::FailedToDeriveBundleDigest)?; + + let bad_valid_bundle_digest = bad_receipt + .valid_bundle_digest_at(*bundle_index as usize) + .ok_or(VerificationError::TargetValidBundleNotFound)?; + + if bad_valid_bundle_digest == valid_bundle_digest { + Err(VerificationError::InvalidProof) + } else { + Ok(()) + } +} + /// Verifies invalid state transition fraud proof. pub fn verify_invalid_state_transition_fraud_proof( bad_receipt: ExecutionReceipt< diff --git a/crates/sp-domains/src/lib.rs b/crates/sp-domains/src/lib.rs index e70fcde10f..c4a8dd1694 100644 --- a/crates/sp-domains/src/lib.rs +++ b/crates/sp-domains/src/lib.rs @@ -395,6 +395,13 @@ impl .collect() } + pub fn valid_bundle_digest_at(&self, index: usize) -> Option { + match self.inboxed_bundles.get(index).map(|ib| &ib.bundle) { + Some(BundleValidity::Valid(bundle_digest_hash)) => Some(*bundle_digest_hash), + _ => None, + } + } + pub fn valid_bundle_digests(&self) -> Vec { self.inboxed_bundles .iter() diff --git a/crates/subspace-service/src/lib.rs b/crates/subspace-service/src/lib.rs index 6663a6dd43..43e91d9494 100644 --- a/crates/subspace-service/src/lib.rs +++ b/crates/subspace-service/src/lib.rs @@ -233,8 +233,13 @@ where Block: BlockT, Block::Hash: From, DomainBlock: BlockT, - DomainBlock::Hash: From, - Client: HeaderBackend + ProvideRuntimeApi + Send + Sync + 'static, + DomainBlock::Hash: Into + From, + Client: BlockBackend + + HeaderBackend + + ProvideRuntimeApi + + Send + + Sync + + 'static, Client::Api: SubspaceApi + DomainsApi, DomainBlock::Hash>, ExecutorDispatch: CodeExecutor + sc_executor::RuntimeVersionOf, diff --git a/domains/client/block-preprocessor/src/lib.rs b/domains/client/block-preprocessor/src/lib.rs index c98cd3aa7d..18e6deffc0 100644 --- a/domains/client/block-preprocessor/src/lib.rs +++ b/domains/client/block-preprocessor/src/lib.rs @@ -26,13 +26,14 @@ use runtime_api::TimestampExtrinsicConstructor; use sc_client_api::BlockBackend; use sp_api::{HashT, ProvideRuntimeApi}; use sp_blockchain::HeaderBackend; +use sp_core::H256; use sp_domains::extrinsics::deduplicate_and_shuffle_extrinsics; use sp_domains::{ DomainId, DomainsApi, ExecutionReceipt, InboxedBundle, InvalidBundleType, OpaqueBundle, OpaqueBundles, ReceiptValidity, }; use sp_messenger::MessengerApi; -use sp_runtime::traits::{BlakeTwo256, Block as BlockT, NumberFor}; +use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor}; use std::collections::VecDeque; use std::marker::PhantomData; use std::sync::Arc; @@ -118,6 +119,7 @@ impl DomainBlockPreprocessor where Block: BlockT, + Block::Hash: Into, CBlock: BlockT, CBlock::Hash: From, NumberFor: From>, @@ -226,10 +228,15 @@ where }; let bundle_digest: Vec<_> = extrinsics .iter() - .map(|(signer, tx)| (signer.clone(), BlakeTwo256::hash_of(tx))) + .map(|(signer, tx)| { + ( + signer.clone(), + ::Hashing::hash_of(tx), + ) + }) .collect(); inboxed_bundles.push(InboxedBundle::valid( - BlakeTwo256::hash_of(&bundle_digest), + ::Hashing::hash_of(&bundle_digest).into(), extrinsic_root, )); valid_extrinsics.extend(extrinsics); diff --git a/domains/client/domain-operator/src/domain_block_processor.rs b/domains/client/domain-operator/src/domain_block_processor.rs index 0c136d9945..f6abea4e39 100644 --- a/domains/client/domain-operator/src/domain_block_processor.rs +++ b/domains/client/domain-operator/src/domain_block_processor.rs @@ -20,7 +20,7 @@ use sp_core::traits::CodeExecutor; use sp_core::H256; use sp_domains::merkle_tree::MerkleTree; use sp_domains::{BundleValidity, DomainId, DomainsApi, ExecutionReceipt}; -use sp_domains_fraud_proof::fraud_proof::FraudProof; +use sp_domains_fraud_proof::fraud_proof::{FraudProof, ValidBundleProof}; use sp_runtime::traits::{Block as BlockT, Header as HeaderT, One, Zero}; use sp_runtime::Digest; use std::cmp::Ordering; @@ -566,7 +566,7 @@ where } } -// Find the first mismatch of the `InboxedBundle` in the `ER::bundles` list +// Find the first mismatch of the `InboxedBundle` in the `ER::inboxed_bundles` list pub(crate) fn find_inboxed_bundles_mismatch( local_receipt: &ExecutionReceiptFor, external_receipt: &ExecutionReceiptFor, @@ -592,7 +592,7 @@ where ).into())); } - // Get the first mismatch of `ER::bundles` + // Get the first mismatch of `ER::inboxed_bundles` let (bundle_index, (local_bundle, external_bundle)) = local_receipt .inboxed_bundles .iter() @@ -969,28 +969,27 @@ where mismatch_type, bundle_index, .. - } => { - match mismatch_type { - BundleMismatchType::Valid => { - // TODO: generate valid bundle fraud proof - return Ok(None); - } - _ => self - .fraud_proof_generator - .generate_invalid_bundle_field_proof::( - self.domain_id, - &local_receipt, - mismatch_type, - bundle_index, - bad_receipt_hash, - ) - .map_err(|err| { - sp_blockchain::Error::Application(Box::from(format!( - "Failed to generate invalid bundles field fraud proof: {err}" - ))) - })?, - } - } + } => match mismatch_type { + BundleMismatchType::Valid => FraudProof::ValidBundle(ValidBundleProof { + domain_id: self.domain_id, + bad_receipt_hash: local_receipt.hash(), + bundle_index, + }), + _ => self + .fraud_proof_generator + .generate_invalid_bundle_field_proof::( + self.domain_id, + &local_receipt, + mismatch_type, + bundle_index, + bad_receipt_hash, + ) + .map_err(|err| { + sp_blockchain::Error::Application(Box::from(format!( + "Failed to generate invalid bundles field fraud proof: {err}" + ))) + })?, + }, ReceiptMismatchInfo::DomainExtrinsicsRoot { .. } => self .fraud_proof_generator .generate_invalid_domain_extrinsics_root_proof::( diff --git a/domains/client/domain-operator/src/tests.rs b/domains/client/domain-operator/src/tests.rs index f13595cccd..5540c9609a 100644 --- a/domains/client/domain-operator/src/tests.rs +++ b/domains/client/domain-operator/src/tests.rs @@ -15,9 +15,9 @@ use sc_transaction_pool_api::TransactionPool; use sp_api::{AsTrieBackend, ProvideRuntimeApi}; use sp_consensus::SyncOracle; use sp_core::traits::FetchRuntimeCode; -use sp_core::Pair; +use sp_core::{Pair, H256}; use sp_domain_digests::AsPredigest; -use sp_domains::{Bundle, DomainId, DomainsApi}; +use sp_domains::{Bundle, BundleValidity, DomainId, DomainsApi}; use sp_domains_fraud_proof::fraud_proof::{ ExecutionPhase, FraudProof, InvalidDomainBlockHashProof, InvalidExtrinsicsRootProof, InvalidStateTransitionProof, InvalidTotalRewardsProof, @@ -1207,6 +1207,155 @@ async fn test_invalid_domain_extrinsics_root_proof_creation() { ferdie.produce_blocks(1).await.unwrap(); } +#[tokio::test(flavor = "multi_thread")] +#[ignore] +async fn test_valid_bundle_proof_generation_and_verification() { + let directory = TempDir::new().expect("Must be able to create temporary directory"); + + let mut builder = sc_cli::LoggerBuilder::new(""); + builder.with_colors(false); + let _ = builder.init(); + + let tokio_handle = tokio::runtime::Handle::current(); + + // Start Ferdie + let mut ferdie = MockConsensusNode::run( + tokio_handle.clone(), + Ferdie, + BasePath::new(directory.path().join("ferdie")), + ); + // Produce 1 consensus block to initialize genesis domain + ferdie.produce_block_with_slot(1.into()).await.unwrap(); + + // Run Alice (a evm domain authority node) + let mut alice = domain_test_service::DomainNodeBuilder::new( + tokio_handle.clone(), + Alice, + BasePath::new(directory.path().join("alice")), + ) + .build_evm_node(Role::Authority, GENESIS_DOMAIN_ID, &mut ferdie) + .await; + + for i in 0..3 { + let tx = alice.construct_extrinsic( + alice.account_nonce() + i, + pallet_balances::Call::transfer_allow_death { + dest: Bob.to_account_id(), + value: 1, + }, + ); + alice + .send_extrinsic(tx) + .await + .expect("Failed to send extrinsic"); + + // Produce a bundle and submit to the tx pool of the consensus node + let (slot, bundle) = ferdie.produce_slot_and_wait_for_bundle_submission().await; + assert!(bundle.is_some()); + + // In the last iteration, produce a consensus block which will included all the previous bundles + if i == 2 { + produce_block_with!(ferdie.produce_block_with_slot(slot), alice) + .await + .unwrap(); + } + } + let bundle_to_tx = |opaque_bundle| { + subspace_test_runtime::UncheckedExtrinsic::new_unsigned( + pallet_domains::Call::submit_bundle { opaque_bundle }.into(), + ) + .into() + }; + let proof_to_tx = |proof| { + subspace_test_runtime::UncheckedExtrinsic::new_unsigned( + pallet_domains::Call::submit_fraud_proof { + fraud_proof: Box::new(FraudProof::ValidBundle(proof)), + } + .into(), + ) + .into() + }; + + // Produce a bundle that will include the reciept of the last 3 bundles and modified the receipt's + // `inboxed_bundles` field to make it invalid + let (slot, bundle) = ferdie.produce_slot_and_wait_for_bundle_submission().await; + let original_submit_bundle_tx = bundle_to_tx(bundle.clone().unwrap()); + let bundle_index = 1; + let (bad_receipt, submit_bundle_tx_with_bad_receipt) = { + let mut bundle = bundle.unwrap(); + assert_eq!(bundle.receipt().inboxed_bundles.len(), 3); + + bundle.sealed_header.header.receipt.inboxed_bundles[bundle_index].bundle = + BundleValidity::Valid(H256::random()); + bundle.sealed_header.signature = Sr25519Keyring::Alice + .pair() + .sign(bundle.sealed_header.pre_hash().as_ref()) + .into(); + + (bundle.receipt().clone(), bundle_to_tx(bundle)) + }; + // Replace `original_submit_bundle_tx` with `submit_bundle_tx_with_bad_receipt` in the tx pool + ferdie + .prune_tx_from_pool(&original_submit_bundle_tx) + .await + .unwrap(); + assert!(ferdie.get_bundle_from_tx_pool(slot.into()).is_none()); + ferdie + .submit_transaction(submit_bundle_tx_with_bad_receipt) + .await + .unwrap(); + + // Produce one more block to inlcude the bad receipt in the consensus chain + let mut import_tx_stream = ferdie.transaction_pool.import_notification_stream(); + produce_block_with!(ferdie.produce_block_with_slot(slot), alice) + .await + .unwrap(); + + // When the domain node operator process the primary block that contains the `bad_submit_bundle_tx`, + // it will generate and submit a fraud proof + while let Some(ready_tx_hash) = import_tx_stream.next().await { + let ready_tx = ferdie + .transaction_pool + .ready_transaction(&ready_tx_hash) + .unwrap(); + let ext = subspace_test_runtime::UncheckedExtrinsic::decode( + &mut ready_tx.data.encode().as_slice(), + ) + .unwrap(); + if let subspace_test_runtime::RuntimeCall::Domains( + pallet_domains::Call::submit_fraud_proof { fraud_proof }, + ) = ext.function + { + if let FraudProof::ValidBundle(proof) = *fraud_proof { + // The fraud proof is targetting the `bad_receipt` + assert_eq!(proof.bad_receipt_hash, bad_receipt.hash()); + + // If the fraud proof target a non-exist receipt then it is invalid + let mut bad_proof = proof.clone(); + bad_proof.bad_receipt_hash = H256::random(); + assert!(ferdie + .submit_transaction(proof_to_tx(bad_proof)) + .await + .is_err()); + + // If the fraud proof point to non-exist bundle then it is invalid + let mut bad_proof = proof.clone(); + bad_proof.bundle_index = u32::MAX; + assert!(ferdie + .submit_transaction(proof_to_tx(bad_proof)) + .await + .is_err()); + + break; + } + } + } + + // Produce a consensus block that contains the fraud proof, the fraud proof wil be verified on + // on the runtime itself + ferdie.produce_blocks(1).await.unwrap(); +} + #[tokio::test(flavor = "multi_thread")] #[ignore] async fn fraud_proof_verification_in_tx_pool_should_work() { diff --git a/test/subspace-test-service/src/lib.rs b/test/subspace-test-service/src/lib.rs index ba1345ea38..94559730f8 100644 --- a/test/subspace-test-service/src/lib.rs +++ b/test/subspace-test-service/src/lib.rs @@ -28,7 +28,7 @@ use jsonrpsee::RpcModule; use parking_lot::Mutex; use sc_block_builder::BlockBuilderProvider; use sc_client_api::execution_extensions::ExtensionsFactory; -use sc_client_api::ExecutorProvider; +use sc_client_api::{BlockBackend, ExecutorProvider}; use sc_consensus::block_import::{ BlockCheckParams, BlockImportParams, ForkChoiceStrategy, ImportResult, }; @@ -196,8 +196,8 @@ where Block: BlockT, Block::Hash: From, DomainBlock: BlockT, - DomainBlock::Hash: From, - Client: HeaderBackend + ProvideRuntimeApi + 'static, + DomainBlock::Hash: Into + From, + Client: BlockBackend + HeaderBackend + ProvideRuntimeApi + 'static, Client::Api: DomainsApi, DomainBlock::Hash>, Executor: CodeExecutor + sc_executor::RuntimeVersionOf, {