diff --git a/Cargo.lock b/Cargo.lock index b2335386b5..dfc1069e10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2674,6 +2674,7 @@ dependencies = [ "sp-executive", "sp-externalities", "sp-inherents", + "sp-io", "sp-keyring", "sp-messenger", "sp-runtime", @@ -2745,6 +2746,7 @@ dependencies = [ "sc-cli", "sc-client-api", "sc-consensus", + "sc-domains", "sc-service", "sc-transaction-pool", "sc-transaction-pool-api", @@ -7573,9 +7575,8 @@ dependencies = [ "sp-externalities", "sp-io", "sp-runtime", - "sp-state-machine", "sp-std", - "sp-trie", + "sp-subspace-mmr", "sp-version", "subspace-core-primitives", "subspace-runtime-primitives", @@ -9916,6 +9917,7 @@ dependencies = [ "sp-blockchain", "sp-core", "sp-domains", + "sp-domains-fraud-proof", "sp-externalities", "sp-io", "sp-messenger-host-functions", @@ -11590,6 +11592,8 @@ dependencies = [ "sp-runtime-interface", "sp-state-machine", "sp-std", + "sp-storage", + "sp-subspace-mmr", "sp-trie", "subspace-core-primitives", "subspace-runtime-primitives", @@ -12701,6 +12705,8 @@ dependencies = [ "frame-support", "frame-system", "pallet-transaction-payment", + "parity-scale-codec", + "scale-info", "sp-core", "sp-io", "sp-runtime", @@ -12845,6 +12851,7 @@ dependencies = [ "pallet-mmr", "pallet-offences-subspace", "pallet-rewards", + "pallet-runtime-configs", "pallet-subspace", "pallet-subspace-mmr", "pallet-sudo", diff --git a/crates/pallet-domains/Cargo.toml b/crates/pallet-domains/Cargo.toml index 4151cdf7bf..81103a7777 100644 --- a/crates/pallet-domains/Cargo.toml +++ b/crates/pallet-domains/Cargo.toml @@ -28,6 +28,7 @@ sp-domains-fraud-proof = { version = "0.1.0", default-features = false, path = " sp-io = { default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } sp-runtime = { default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } sp-std = { default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } +sp-subspace-mmr = { version = "0.1.0", default-features = false, path = "../sp-subspace-mmr" } sp-version = { default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed", features = ["serde"] } subspace-core-primitives = { version = "0.1.0", default-features = false, path = "../subspace-core-primitives" } subspace-runtime-primitives = { version = "0.1.0", default-features = false, path = "../subspace-runtime-primitives" } @@ -38,8 +39,6 @@ hex-literal = "0.4.1" pallet-timestamp = { git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } pallet-block-fees = { version = "0.1.0", default-features = false, path = "../../domains/pallets/block-fees" } sp-externalities = { git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } -sp-state-machine = { git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } -sp-trie = { git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } [features] default = ["std"] @@ -60,6 +59,7 @@ std = [ "sp-io/std", "sp-runtime/std", "sp-std/std", + "sp-subspace-mmr/std", "sp-version/std", "subspace-core-primitives/std", "subspace-runtime-primitives/std", diff --git a/crates/pallet-domains/src/lib.rs b/crates/pallet-domains/src/lib.rs index c2728d57c1..50e4236d30 100644 --- a/crates/pallet-domains/src/lib.rs +++ b/crates/pallet-domains/src/lib.rs @@ -67,20 +67,17 @@ use sp_domains::{ DOMAIN_EXTRINSICS_SHUFFLING_SEED_SUBJECT, EMPTY_EXTRINSIC_ROOT, }; use sp_domains_fraud_proof::fraud_proof::{ - FraudProof, InvalidBlockFeesProof, InvalidDomainBlockHashProof, -}; -use sp_domains_fraud_proof::verification::{ - verify_bundle_equivocation_fraud_proof, verify_invalid_block_fees_fraud_proof, - verify_invalid_bundles_fraud_proof, verify_invalid_domain_block_hash_fraud_proof, - verify_invalid_domain_extrinsics_root_fraud_proof, verify_invalid_state_transition_fraud_proof, - verify_invalid_transfers_fraud_proof, verify_valid_bundle_fraud_proof, + DomainRuntimeCodeAt, FraudProofV2, FraudProofVariant, InvalidBlockFeesProofV2, + InvalidDomainBlockHashProofV2, InvalidTransfersProofV2, }; +use sp_domains_fraud_proof::storage_proof::{self, BasicStorageProof, DomainRuntimeCodeProof}; +use sp_domains_fraud_proof::verification_v2; use sp_runtime::traits::{BlockNumberProvider, CheckedSub, Hash, Header, One, Zero}; use sp_runtime::transaction_validity::TransactionPriority; use sp_runtime::{RuntimeAppPublic, SaturatedConversion, Saturating}; +use sp_subspace_mmr::{ConsensusChainMmrLeafProof, MmrProofVerifier}; pub use staking::OperatorConfig; use subspace_core_primitives::{BlockHash, PotOutput, SlotNumber, U256}; -use subspace_runtime_primitives::Balance; pub(crate) type BalanceOf = ::Balance; @@ -118,6 +115,13 @@ pub type OpaqueBundleOf = OpaqueBundle< BalanceOf, >; +pub type FraudProofV2For = FraudProofV2< + BlockNumberFor, + ::Hash, + ::DomainHeader, + ::MmrHash, +>; + /// Parameters used to verify proof of election. #[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)] pub(crate) struct ElectionVerificationParams { @@ -152,6 +156,8 @@ const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); /// 100 as the maximum number of bundle per block for benchmarking. const MAX_BUNLDE_PER_BLOCK: u32 = 100; +pub(crate) type StateRootOf = <::Hashing as Hash>::Output; + #[frame_support::pallet] mod pallet { #![allow(clippy::large_enum_variant)] @@ -185,8 +191,8 @@ mod pallet { use crate::DomainHashingFor; use crate::{ BalanceOf, BlockSlot, BlockTreeNodeFor, DomainBlockNumberFor, ElectionVerificationParams, - HoldIdentifier, NominatorId, OpaqueBundleOf, ReceiptHashFor, MAX_BUNLDE_PER_BLOCK, - STORAGE_VERSION, + FraudProofV2For, HoldIdentifier, NominatorId, OpaqueBundleOf, ReceiptHashFor, StateRootOf, + MAX_BUNLDE_PER_BLOCK, STORAGE_VERSION, }; #[cfg(not(feature = "std"))] use alloc::string::String; @@ -210,7 +216,7 @@ mod pallet { DomainsTransfersTracker, EpochIndex, GenesisDomain, OnDomainInstantiated, OperatorAllowList, OperatorId, OperatorPublicKey, RuntimeId, RuntimeObject, RuntimeType, }; - use sp_domains_fraud_proof::fraud_proof::FraudProof; + use sp_domains_fraud_proof::storage_proof::{self, FraudProofStorageKeyProvider}; use sp_domains_fraud_proof::InvalidTransactionCode; use sp_runtime::traits::{ AtLeast32BitUnsigned, BlockNumberProvider, CheckEqual, CheckedAdd, Header as HeaderT, @@ -220,6 +226,7 @@ mod pallet { use sp_std::boxed::Box; use sp_std::collections::btree_set::BTreeSet; use sp_std::fmt::Debug; + use sp_subspace_mmr::MmrProofVerifier; use subspace_core_primitives::U256; use subspace_runtime_primitives::StorageFee; @@ -392,6 +399,19 @@ mod pallet { /// A hook to call after a domain is instantiated type OnDomainInstantiated: OnDomainInstantiated; + + /// Hash type of MMR + type MmrHash: Parameter + Member + Default + Clone; + + /// MMR proof verifier + type MmrProofVerifier: MmrProofVerifier< + Self::MmrHash, + BlockNumberFor, + StateRootOf, + >; + + /// Fraud proof storage key provider + type FraudProofStorageKeyProvider: FraudProofStorageKeyProvider; } #[pallet::pallet] @@ -403,11 +423,6 @@ mod pallet { #[pallet::storage] pub type SuccessfulBundles = StorageMap<_, Identity, DomainId, Vec, ValueQuery>; - /// Fraud proofs submitted successfully in current block. - #[pallet::storage] - pub(super) type SuccessfulFraudProofs = - StorageMap<_, Identity, DomainId, Vec, ValueQuery>; - /// Stores the next runtime id. #[pallet::storage] pub(super) type NextRuntimeId = StorageValue<_, RuntimeId, ValueQuery>; @@ -727,6 +742,25 @@ mod pallet { BadBundleEquivocationFraudProof, /// The bad receipt already reported by a previous fraud proof BadReceiptAlreadyReported, + /// Bad MMR proof, it may due to the proof is expired or it is generated against a different fork. + BadMmrProof, + /// Unexpected MMR proof + UnexpectedMmrProof, + /// Missing MMR proof + MissingMmrProof, + /// Domain runtime not found + RuntimeNotFound, + /// The domain runtime code proof is not provided + DomainRuntimeCodeProofNotFound, + /// The domain runtime code proof is unexpected + UnexpectedDomainRuntimeCodeProof, + StorageProof(storage_proof::VerificationError), + } + + impl From for FraudProofError { + fn from(err: storage_proof::VerificationError) -> Self { + FraudProofError::StorageProof(err) + } } impl From for Error { @@ -1080,92 +1114,6 @@ mod pallet { Ok(Some(actual_weight.min(Self::max_submit_bundle_weight())).into()) } - #[pallet::call_index(1)] - #[pallet::weight(( - T::WeightInfo::submit_fraud_proof().saturating_add( - T::WeightInfo::handle_bad_receipt(MAX_BUNLDE_PER_BLOCK) - ), - DispatchClass::Operational, - Pays::No - ))] - pub fn submit_fraud_proof( - origin: OriginFor, - fraud_proof: Box, T::Hash, T::DomainHeader>>, - ) -> DispatchResultWithPostInfo { - ensure_none(origin)?; - - log::trace!(target: "runtime::domains", "Processing fraud proof: {fraud_proof:?}"); - let domain_id = fraud_proof.domain_id(); - let mut actual_weight = T::WeightInfo::submit_fraud_proof(); - - if let Some(bad_receipt_hash) = fraud_proof.targeted_bad_receipt_hash() { - let head_receipt_number = HeadReceiptNumber::::get(domain_id); - let bad_receipt_number = BlockTreeNodes::::get(bad_receipt_hash) - .ok_or::>(FraudProofError::BadReceiptNotFound.into())? - .execution_receipt - .domain_block_number; - // The `head_receipt_number` must greater than or equal to any existing receipt, including - // the bad receipt, otherwise the fraud proof should be rejected due to `BadReceiptNotFound`, - // double check here to make it more robust. - ensure!( - head_receipt_number >= bad_receipt_number, - Error::::from(FraudProofError::BadReceiptNotFound), - ); - - // Prune the bad ER and slash the submitter, the descendants of the bad ER (i.e. all ERs in - // `[bad_receipt_number + 1..head_receipt_number]` ) and the corresponding submitter will be - // pruned/slashed lazily as the domain progressed. - // - // NOTE: Skip the following staking related operations when benchmarking the - // `submit_fraud_proof` call, these operations will be benchmarked separately. - #[cfg(not(feature = "runtime-benchmarks"))] - { - let block_tree_node = prune_receipt::(domain_id, bad_receipt_number) - .map_err(Error::::from)? - .ok_or::>(FraudProofError::BadReceiptNotFound.into())?; - - actual_weight = - actual_weight.saturating_add(T::WeightInfo::handle_bad_receipt( - (block_tree_node.operator_ids.len() as u32).min(MAX_BUNLDE_PER_BLOCK), - )); - - do_slash_operators::( - block_tree_node.operator_ids.into_iter(), - SlashedReason::BadExecutionReceipt(bad_receipt_hash), - ) - .map_err(Error::::from)?; - } - - // Update the head receipt number to `bad_receipt_number - 1` - let new_head_receipt_number = bad_receipt_number.saturating_sub(One::one()); - HeadReceiptNumber::::insert(domain_id, new_head_receipt_number); - - Self::deposit_event(Event::FraudProofProcessed { - domain_id, - new_head_receipt_number: Some(new_head_receipt_number), - }); - } else if let Some((targeted_bad_operator, slot)) = - fraud_proof.targeted_bad_operator_and_slot_for_bundle_equivocation() - { - Self::deposit_event(Event::FraudProofProcessed { - domain_id, - new_head_receipt_number: None, - }); - - do_slash_operators::( - vec![targeted_bad_operator].into_iter(), - SlashedReason::BundleEquivocation(slot), - ) - .map_err(Error::::from)?; - - actual_weight = actual_weight.saturating_add(T::WeightInfo::handle_bad_receipt(1)); - } - - SuccessfulFraudProofs::::append(domain_id, fraud_proof.hash()); - - Ok(Some(actual_weight).into()) - } - #[pallet::call_index(2)] #[pallet::weight(T::WeightInfo::register_domain_runtime())] pub fn register_domain_runtime( @@ -1418,6 +1366,72 @@ mod pallet { PermissionedActionAllowedBy::::put(permissioned_action_allowed_by); Ok(()) } + + #[pallet::call_index(15)] + #[pallet::weight(( + T::WeightInfo::submit_fraud_proof().saturating_add( + T::WeightInfo::handle_bad_receipt(MAX_BUNLDE_PER_BLOCK) + ), + DispatchClass::Operational, + Pays::No + ))] + pub fn submit_fraud_proof( + origin: OriginFor, + fraud_proof: Box>, + ) -> DispatchResultWithPostInfo { + ensure_none(origin)?; + + log::trace!(target: "runtime::domains", "Processing fraud proof: {fraud_proof:?}"); + let mut actual_weight = T::WeightInfo::submit_fraud_proof(); + let domain_id = fraud_proof.domain_id(); + let bad_receipt_hash = fraud_proof.targeted_bad_receipt_hash(); + let head_receipt_number = HeadReceiptNumber::::get(domain_id); + let bad_receipt_number = BlockTreeNodes::::get(bad_receipt_hash) + .ok_or::>(FraudProofError::BadReceiptNotFound.into())? + .execution_receipt + .domain_block_number; + // The `head_receipt_number` must greater than or equal to any existing receipt, including + // the bad receipt, otherwise the fraud proof should be rejected due to `BadReceiptNotFound`, + // double check here to make it more robust. + ensure!( + head_receipt_number >= bad_receipt_number, + Error::::from(FraudProofError::BadReceiptNotFound), + ); + + // Prune the bad ER and slash the submitter, the descendants of the bad ER (i.e. all ERs in + // `[bad_receipt_number + 1..head_receipt_number]` ) and the corresponding submitter will be + // pruned/slashed lazily as the domain progressed. + // + // NOTE: Skip the following staking related operations when benchmarking the + // `submit_fraud_proof` call, these operations will be benchmarked separately. + #[cfg(not(feature = "runtime-benchmarks"))] + { + let block_tree_node = prune_receipt::(domain_id, bad_receipt_number) + .map_err(Error::::from)? + .ok_or::>(FraudProofError::BadReceiptNotFound.into())?; + + actual_weight = actual_weight.saturating_add(T::WeightInfo::handle_bad_receipt( + (block_tree_node.operator_ids.len() as u32).min(MAX_BUNLDE_PER_BLOCK), + )); + + do_slash_operators::( + block_tree_node.operator_ids.into_iter(), + SlashedReason::BadExecutionReceipt(bad_receipt_hash), + ) + .map_err(Error::::from)?; + } + + // Update the head receipt number to `bad_receipt_number - 1` + let new_head_receipt_number = bad_receipt_number.saturating_sub(One::one()); + HeadReceiptNumber::::insert(domain_id, new_head_receipt_number); + + Self::deposit_event(Event::FraudProofProcessed { + domain_id, + new_head_receipt_number: Some(new_head_receipt_number), + }); + + Ok(Some(actual_weight).into()) + } } #[pallet::genesis_config] @@ -1515,8 +1529,6 @@ mod pallet { T::DomainBundleSubmitted::domain_bundle_submitted(domain_id); } - let _ = SuccessfulFraudProofs::::clear(u32::MAX, None); - Weight::zero() } @@ -1540,9 +1552,11 @@ mod pallet { ) .map_err(|_| InvalidTransaction::Call.into()) }), - Call::submit_fraud_proof { fraud_proof } => Self::validate_fraud_proof(fraud_proof) - .map(|_| ()) - .map_err(|_| InvalidTransaction::Call.into()), + Call::submit_fraud_proof { fraud_proof } => { + Self::validate_fraud_proof_v2(fraud_proof) + .map(|_| ()) + .map_err(|_| InvalidTransaction::Call.into()) + } _ => Err(InvalidTransaction::Call.into()), } } @@ -1609,7 +1623,7 @@ mod pallet { .build() } Call::submit_fraud_proof { fraud_proof } => { - let (tag, priority) = match Self::validate_fraud_proof(fraud_proof) { + let (tag, priority) = match Self::validate_fraud_proof_v2(fraud_proof) { Err(e) => { log::warn!( target: "runtime::domains", @@ -1640,10 +1654,6 @@ impl Pallet { SuccessfulBundles::::get(domain_id) } - pub fn successful_fraud_proofs(domain_id: DomainId) -> Vec { - SuccessfulFraudProofs::::get(domain_id) - } - pub fn domain_runtime_code(domain_id: DomainId) -> Option> { RuntimeRegistry::::get(Self::runtime_id(domain_id)?) .and_then(|mut runtime_object| runtime_object.raw_genesis.take_runtime_code()) @@ -1807,6 +1817,8 @@ impl Pallet { Ok(()) } + // TODO: as bundle equivocation fraud proof is removed, add check to rejected bundle with the + // same `(operator_id, slot)` fn validate_bundle( opaque_bundle: &OpaqueBundleOf, pre_dispatch: bool, @@ -1878,211 +1890,248 @@ impl Pallet { Ok(()) } - fn validate_fraud_proof( - fraud_proof: &FraudProof, T::Hash, T::DomainHeader>, + fn validate_fraud_proof_v2( + fraud_proof: &FraudProofV2For, ) -> Result<(FraudProofTag, TransactionPriority), FraudProofError> { - let tag_and_priority = if let Some(bad_receipt_hash) = - fraud_proof.targeted_bad_receipt_hash() - { - let bad_receipt = BlockTreeNodes::::get(bad_receipt_hash) - .ok_or(FraudProofError::BadReceiptNotFound)? - .execution_receipt; - let domain_block_number = bad_receipt.domain_block_number; + let domain_id = fraud_proof.domain_id(); + let bad_receipt_hash = fraud_proof.targeted_bad_receipt_hash(); + let bad_receipt = BlockTreeNodes::::get(bad_receipt_hash) + .ok_or(FraudProofError::BadReceiptNotFound)? + .execution_receipt; + let bad_receipt_domain_block_number = bad_receipt.domain_block_number; - ensure!( - !bad_receipt.domain_block_number.is_zero(), - FraudProofError::ChallengingGenesisReceipt - ); + ensure!( + !bad_receipt_domain_block_number.is_zero(), + FraudProofError::ChallengingGenesisReceipt + ); - ensure!( - !Self::is_bad_er_pending_to_prune( - fraud_proof.domain_id(), - bad_receipt.domain_block_number - ), - FraudProofError::BadReceiptAlreadyReported, - ); + ensure!( + !Self::is_bad_er_pending_to_prune(domain_id, bad_receipt_domain_block_number), + FraudProofError::BadReceiptAlreadyReported, + ); - match fraud_proof { - FraudProof::InvalidBlockFees(InvalidBlockFeesProof { storage_proof, .. }) => { - verify_invalid_block_fees_fraud_proof::< - T::Block, - DomainBlockNumberFor, - T::DomainHash, - BalanceOf, - DomainHashingFor, - >(bad_receipt, storage_proof) - .map_err(|err| { - log::error!( - target: "runtime::domains", - "Block fees proof verification failed: {err:?}" - ); - FraudProofError::InvalidBlockFeesFraudProof - })?; - } - FraudProof::InvalidTransfers(req) => { - verify_invalid_transfers_fraud_proof::< - T::Block, - DomainBlockNumberFor, - T::DomainHash, - BalanceOf, - DomainHashingFor, - >(bad_receipt, req) - .map_err(|err| { - log::error!( - target: "runtime::domains", - "Domain transfers proof verification failed: {err:?}" - ); - FraudProofError::InvalidTransfersFraudProof - })?; - } - FraudProof::InvalidDomainBlockHash(InvalidDomainBlockHashProof { - digest_storage_proof, - .. - }) => { - let parent_receipt = - BlockTreeNodes::::get(bad_receipt.parent_domain_block_receipt_hash) - .ok_or(FraudProofError::ParentReceiptNotFound)? - .execution_receipt; - verify_invalid_domain_block_hash_fraud_proof::< - T::Block, - BalanceOf, - T::DomainHeader, - >( - bad_receipt, - digest_storage_proof.clone(), - parent_receipt.domain_block_hash, - ) - .map_err(|err| { - log::error!( - target: "runtime::domains", - "Invalid Domain block hash proof verification failed: {err:?}" - ); - FraudProofError::InvalidDomainBlockHashFraudProof - })?; - } - FraudProof::InvalidExtrinsicsRoot(proof) => { - verify_invalid_domain_extrinsics_root_fraud_proof::< - T::Block, - BalanceOf, - T::Hashing, - T::DomainHeader, - >(bad_receipt, proof) - .map_err(|err| { - log::error!( - target: "runtime::domains", - "Invalid Domain extrinsic root proof verification failed: {err:?}" - ); - FraudProofError::InvalidExtrinsicRootFraudProof - })?; - } - FraudProof::InvalidStateTransition(proof) => { - let bad_receipt_parent = - BlockTreeNodes::::get(bad_receipt.parent_domain_block_receipt_hash) - .ok_or(FraudProofError::ParentReceiptNotFound)? - .execution_receipt; - - verify_invalid_state_transition_fraud_proof::< - T::Block, - T::DomainHeader, - BalanceOf, - >(bad_receipt, bad_receipt_parent, proof) - .map_err(|err| { - log::error!( - target: "runtime::domains", - "Invalid State transition proof verification failed: {err:?}" - ); - FraudProofError::InvalidStateTransitionFraudProof - })?; - } - FraudProof::InvalidBundles(invalid_bundles_fraud_proof) => { - let bad_receipt_parent = - BlockTreeNodes::::get(bad_receipt.parent_domain_block_receipt_hash) - .ok_or(FraudProofError::ParentReceiptNotFound)? - .execution_receipt; - - verify_invalid_bundles_fraud_proof::>( - bad_receipt, - bad_receipt_parent, - invalid_bundles_fraud_proof, - ) - .map_err(|err| { - log::error!( - target: "runtime::domains", - "Invalid Bundle proof verification failed: {err:?}" - ); - FraudProofError::InvalidBundleFraudProof - })?; - } - FraudProof::ValidBundle(proof) => verify_valid_bundle_fraud_proof::< + ensure!( + !fraud_proof.is_unexpected_domain_runtime_code_proof(), + FraudProofError::UnexpectedDomainRuntimeCodeProof, + ); + + ensure!( + !fraud_proof.is_unexpected_mmr_proof(), + FraudProofError::UnexpectedMmrProof, + ); + + let maybe_state_root = match &fraud_proof.maybe_mmr_proof { + Some(mmr_proof) => Some(Self::verify_mmr_proof_and_extract_state_root( + mmr_proof.clone(), + bad_receipt.consensus_block_number, + )?), + None => None, + }; + + match &fraud_proof.proof { + FraudProofVariant::InvalidBlockFees(InvalidBlockFeesProofV2 { storage_proof }) => { + let domain_runtime_code = Self::get_domain_runtime_code_for_receipt( + domain_id, + &bad_receipt, + fraud_proof.maybe_domain_runtime_code_proof.clone(), + )?; + + verification_v2::verify_invalid_block_fees_fraud_proof::< T::Block, DomainBlockNumberFor, T::DomainHash, BalanceOf, - >(bad_receipt, proof) + DomainHashingFor, + >(bad_receipt, storage_proof, domain_runtime_code) .map_err(|err| { log::error!( target: "runtime::domains", - "Valid bundle proof verification failed: {err:?}" + "Block fees proof verification failed: {err:?}" ); - FraudProofError::BadValidBundleFraudProof - })?, - _ => return Err(FraudProofError::UnexpectedFraudProof), + FraudProofError::InvalidBlockFeesFraudProof + })?; } + FraudProofVariant::InvalidTransfers(InvalidTransfersProofV2 { storage_proof }) => { + let domain_runtime_code = Self::get_domain_runtime_code_for_receipt( + domain_id, + &bad_receipt, + fraud_proof.maybe_domain_runtime_code_proof.clone(), + )?; - // The priority of fraud proof is determined by how many blocks left before the bad ER - // is confirmed, the less the more emergency it is, thus give a higher priority. - let block_before_bad_er_confirm = domain_block_number.saturating_sub( - Self::latest_confirmed_domain_block_number(fraud_proof.domain_id()), - ); - let priority = - TransactionPriority::MAX - block_before_bad_er_confirm.saturated_into::(); - - // Use the domain id as tag thus the consensus node only accept one fraud proof for a - // specific domain at a time - let tag = FraudProofTag::BadER(fraud_proof.domain_id()); + verification_v2::verify_invalid_transfers_fraud_proof::< + T::Block, + DomainBlockNumberFor, + T::DomainHash, + BalanceOf, + DomainHashingFor, + >(bad_receipt, storage_proof, domain_runtime_code) + .map_err(|err| { + log::error!( + target: "runtime::domains", + "Domain transfers proof verification failed: {err:?}" + ); + FraudProofError::InvalidTransfersFraudProof + })?; + } + FraudProofVariant::InvalidDomainBlockHash(InvalidDomainBlockHashProofV2 { + digest_storage_proof, + }) => { + let parent_receipt = + BlockTreeNodes::::get(bad_receipt.parent_domain_block_receipt_hash) + .ok_or(FraudProofError::ParentReceiptNotFound)? + .execution_receipt; + verification_v2::verify_invalid_domain_block_hash_fraud_proof::< + T::Block, + BalanceOf, + T::DomainHeader, + >( + bad_receipt, + digest_storage_proof.clone(), + parent_receipt.domain_block_hash, + ) + .map_err(|err| { + log::error!( + target: "runtime::domains", + "Invalid Domain block hash proof verification failed: {err:?}" + ); + FraudProofError::InvalidDomainBlockHashFraudProof + })?; + } + FraudProofVariant::InvalidExtrinsicsRoot(proof) => { + let domain_runtime_code = Self::get_domain_runtime_code_for_receipt( + domain_id, + &bad_receipt, + fraud_proof.maybe_domain_runtime_code_proof.clone(), + )?; + let runtime_id = + Self::runtime_id(domain_id).ok_or(FraudProofError::RuntimeNotFound)?; + let state_root = maybe_state_root.ok_or(FraudProofError::MissingMmrProof)?; + + verification_v2::verify_invalid_domain_extrinsics_root_fraud_proof::< + T::Block, + BalanceOf, + T::DomainHeader, + T::Hashing, + T::FraudProofStorageKeyProvider, + >( + bad_receipt, + proof, + domain_id, + runtime_id, + state_root, + domain_runtime_code, + ) + .map_err(|err| { + log::error!( + target: "runtime::domains", + "Invalid Domain extrinsic root proof verification failed: {err:?}" + ); + FraudProofError::InvalidExtrinsicRootFraudProof + })?; + } + FraudProofVariant::InvalidStateTransition(proof) => { + let domain_runtime_code = Self::get_domain_runtime_code_for_receipt( + domain_id, + &bad_receipt, + fraud_proof.maybe_domain_runtime_code_proof.clone(), + )?; + let bad_receipt_parent = + BlockTreeNodes::::get(bad_receipt.parent_domain_block_receipt_hash) + .ok_or(FraudProofError::ParentReceiptNotFound)? + .execution_receipt; + + verification_v2::verify_invalid_state_transition_fraud_proof::< + T::Block, + T::DomainHeader, + BalanceOf, + >(bad_receipt, bad_receipt_parent, proof, domain_runtime_code) + .map_err(|err| { + log::error!( + target: "runtime::domains", + "Invalid State transition proof verification failed: {err:?}" + ); + FraudProofError::InvalidStateTransitionFraudProof + })?; + } + FraudProofVariant::InvalidBundles(proof) => { + let state_root = maybe_state_root.ok_or(FraudProofError::MissingMmrProof)?; + let domain_runtime_code = Self::get_domain_runtime_code_for_receipt( + domain_id, + &bad_receipt, + fraud_proof.maybe_domain_runtime_code_proof.clone(), + )?; - (tag, priority) - } else if let Some((bad_operator_id, _)) = - fraud_proof.targeted_bad_operator_and_slot_for_bundle_equivocation() - { - let operator = - Operators::::get(bad_operator_id).ok_or(FraudProofError::MissingOperator)?; - match fraud_proof { - FraudProof::BundleEquivocation(proof) => { - let operator_signing_key = operator.signing_key; - verify_bundle_equivocation_fraud_proof::( - &operator_signing_key, - &proof.first_header, - &proof.second_header, - ) - .map_err(|err| { - log::error!( - target: "runtime::domains", - "Bundle equivocation proof verification failed: {err:?}" - ); - FraudProofError::BadBundleEquivocationFraudProof - })?; - } + let bad_receipt_parent = + BlockTreeNodes::::get(bad_receipt.parent_domain_block_receipt_hash) + .ok_or(FraudProofError::ParentReceiptNotFound)? + .execution_receipt; - _ => return Err(FraudProofError::UnexpectedFraudProof), + verification_v2::verify_invalid_bundles_fraud_proof::< + T::Block, + T::DomainHeader, + BalanceOf, + T::FraudProofStorageKeyProvider, + >( + bad_receipt, + bad_receipt_parent, + proof, + domain_id, + state_root, + domain_runtime_code, + ) + .map_err(|err| { + log::error!( + target: "runtime::domains", + "Invalid Bundle proof verification failed: {err:?}" + ); + FraudProofError::InvalidBundleFraudProof + })?; } + FraudProofVariant::ValidBundle(proof) => { + let state_root = maybe_state_root.ok_or(FraudProofError::MissingMmrProof)?; + let domain_runtime_code = Self::get_domain_runtime_code_for_receipt( + domain_id, + &bad_receipt, + fraud_proof.maybe_domain_runtime_code_proof.clone(), + )?; - // Bundle equivocation fraud proof doesn't target bad ER thus we give it the lowest priority - // compared to other fraud proofs - let priority = TransactionPriority::MAX - - T::BlockTreePruningDepth::get().saturated_into::() - - 1; + verification_v2::verify_valid_bundle_fraud_proof::< + T::Block, + T::DomainHeader, + BalanceOf, + T::FraudProofStorageKeyProvider, + >( + bad_receipt, + proof, + domain_id, + state_root, + domain_runtime_code, + ) + .map_err(|err| { + log::error!( + target: "runtime::domains", + "Valid bundle proof verification failed: {err:?}" + ); + FraudProofError::BadValidBundleFraudProof + })? + } + #[cfg(any(feature = "std", feature = "runtime-benchmarks"))] + FraudProofVariant::Dummy => {} + } - // Use the operator id as tag thus the consensus node only accept one bundle equivacotion fraud proof - // for a specific operator at a time - let tag = FraudProofTag::BundleEquivocation(bad_operator_id); + // The priority of fraud proof is determined by how many blocks left before the bad ER + // is confirmed, the less the more emergency it is, thus give a higher priority. + let block_before_bad_er_confirm = bad_receipt_domain_block_number.saturating_sub( + Self::latest_confirmed_domain_block_number(fraud_proof.domain_id()), + ); + let priority = + TransactionPriority::MAX - block_before_bad_er_confirm.saturated_into::(); - (tag, priority) - } else { - return Err(FraudProofError::UnexpectedFraudProof); - }; + // Use the domain id as tag thus the consensus node only accept one fraud proof for a + // specific domain at a time + let tag = FraudProofTag::BadER(fraud_proof.domain_id()); - Ok(tag_and_priority) + Ok((tag, priority)) } /// Return operators specific election verification params for Proof of Election verification. @@ -2374,6 +2423,82 @@ impl Pallet { let storage_fund_acc = storage_fund_account::(operator_id); T::Currency::reducible_balance(&storage_fund_acc, Preservation::Preserve, Fortitude::Polite) } + + // Get the domain runtime code that used to derive `receipt`, if the runtime code still present in + // the state then get it from the state otherwise from the `maybe_domain_runtime_code_at` prood. + pub fn get_domain_runtime_code_for_receipt( + domain_id: DomainId, + receipt: &ExecutionReceiptOf, + maybe_domain_runtime_code_at: Option< + DomainRuntimeCodeAt, T::Hash, T::MmrHash>, + >, + ) -> Result, FraudProofError> { + let runtime_id = Self::runtime_id(domain_id).ok_or(FraudProofError::RuntimeNotFound)?; + let current_runtime_obj = + RuntimeRegistry::::get(runtime_id).ok_or(FraudProofError::RuntimeNotFound)?; + + // NOTE: domain runtime code is taking affect in the next block, so to get the domain runtime code + // that used to derive `receipt` we need to use runtime code at `parent_receipt.consensus_block_number` + let at = { + let parent_receipt = BlockTreeNodes::::get(receipt.parent_domain_block_receipt_hash) + .ok_or(FraudProofError::ParentReceiptNotFound)? + .execution_receipt; + parent_receipt.consensus_block_number + }; + + let is_domain_runtime_updraded = current_runtime_obj.updated_at >= at; + + let mut runtime_obj = match (is_domain_runtime_updraded, maybe_domain_runtime_code_at) { + // The domain runtime is upgraded since `at`, the domain runtime code in `at` is not available + // so `domain_runtime_code_proof` must be provided + (true, None) => return Err(FraudProofError::DomainRuntimeCodeProofNotFound), + (true, Some(domain_runtime_code_at)) => { + let DomainRuntimeCodeAt { + mmr_proof, + domain_runtime_code_proof, + } = domain_runtime_code_at; + + let state_root = Self::verify_mmr_proof_and_extract_state_root(mmr_proof, at)?; + + >::verify::< + T::FraudProofStorageKeyProvider, + >(domain_runtime_code_proof, runtime_id, &state_root)? + } + // Domain runtime code in `at` is available in the state so `domain_runtime_code_proof` + // is unexpected + (false, Some(_)) => return Err(FraudProofError::UnexpectedDomainRuntimeCodeProof), + (false, None) => current_runtime_obj, + }; + let code = runtime_obj + .raw_genesis + .take_runtime_code() + .ok_or(storage_proof::VerificationError::RuntimeCodeNotFound)?; + Ok(code) + } + + pub fn is_domain_runtime_updraded_since( + domain_id: DomainId, + at: BlockNumberFor, + ) -> Option { + Self::runtime_id(domain_id) + .and_then(RuntimeRegistry::::get) + .map(|runtime_obj| runtime_obj.updated_at >= at) + } + + pub fn verify_mmr_proof_and_extract_state_root( + mmr_leaf_proof: ConsensusChainMmrLeafProof, T::Hash, T::MmrHash>, + expected_block_number: BlockNumberFor, + ) -> Result { + let leaf_data = T::MmrProofVerifier::verify_proof_and_extract_leaf(mmr_leaf_proof) + .ok_or(FraudProofError::BadMmrProof)?; + + // Ensure it is a proof of the exact block that we expected + if expected_block_number != leaf_data.block_number() { + return Err(FraudProofError::UnexpectedMmrProof); + } + + Ok(leaf_data.state_root()) + } } impl sp_domains::DomainOwner for Pallet { @@ -2411,9 +2536,7 @@ where } /// Submits an unsigned extrinsic [`Call::submit_fraud_proof`]. - pub fn submit_fraud_proof_unsigned( - fraud_proof: FraudProof, T::Hash, T::DomainHeader>, - ) { + pub fn submit_fraud_proof_unsigned(fraud_proof: FraudProofV2For) { let call = Call::submit_fraud_proof { fraud_proof: Box::new(fraud_proof), }; diff --git a/crates/pallet-domains/src/tests.rs b/crates/pallet-domains/src/tests.rs index 4ef2c575f5..b5d221afe1 100644 --- a/crates/pallet-domains/src/tests.rs +++ b/crates/pallet-domains/src/tests.rs @@ -5,7 +5,7 @@ use crate::{ self as pallet_domains, BalanceOf, BlockSlot, BlockTree, BlockTreeNodes, BundleError, Config, ConsensusBlockHash, DomainBlockNumberFor, DomainHashingFor, DomainRegistry, ExecutionInbox, ExecutionReceiptOf, FraudProofError, FungibleHoldId, HeadReceiptNumber, NextDomainId, - Operators, ReceiptHashFor, + Operators, }; use codec::{Decode, Encode, MaxEncodedLen}; use domain_runtime_primitives::opaque::Header as DomainHeader; @@ -19,38 +19,22 @@ use frame_system::mocking::MockUncheckedExtrinsic; use frame_system::pallet_prelude::*; use scale_info::TypeInfo; use sp_core::crypto::Pair; -use sp_core::storage::{StateVersion, StorageKey}; use sp_core::{Get, H256, U256}; use sp_domains::merkle_tree::MerkleTree; -use sp_domains::proof_provider_and_verifier::StorageProofProvider; use sp_domains::storage::RawGenesis; use sp_domains::{ - BundleHeader, ChainId, DomainId, DomainsHoldIdentifier, ExecutionReceipt, ExtrinsicDigest, - InboxedBundle, InvalidBundleType, OpaqueBundle, OperatorAllowList, OperatorId, OperatorPair, - ProofOfElection, RuntimeType, SealedBundleHeader, StakingHoldIdentifier, -}; -use sp_domains_fraud_proof::fraud_proof::{ - FraudProof, InvalidBlockFeesProof, InvalidBundlesFraudProof, InvalidDomainBlockHashProof, - InvalidExtrinsicsRootProof, ValidBundleDigest, -}; -use sp_domains_fraud_proof::{ - DomainChainAllowlistUpdateExtrinsic, DomainInherentExtrinsic, DomainInherentExtrinsicData, - DomainStorageKeyRequest, FraudProofExtension, FraudProofHostFunctions, - FraudProofVerificationInfoRequest, FraudProofVerificationInfoResponse, SetCodeExtrinsic, - StatelessDomainRuntimeCall, + BundleHeader, ChainId, DomainId, DomainsHoldIdentifier, ExecutionReceipt, InboxedBundle, + OpaqueBundle, OperatorAllowList, OperatorId, OperatorPair, ProofOfElection, RuntimeType, + SealedBundleHeader, StakingHoldIdentifier, }; +use sp_domains_fraud_proof::fraud_proof::FraudProofV2; use sp_runtime::traits::{ AccountIdConversion, BlakeTwo256, BlockNumberProvider, Hash as HashT, IdentityLookup, One, }; use sp_runtime::transaction_validity::TransactionValidityError; -use sp_runtime::{BuildStorage, Digest, OpaqueExtrinsic, Saturating}; -use sp_state_machine::backend::AsTrieBackend; -use sp_state_machine::{prove_read, Backend, TrieBackendBuilder}; -use sp_std::sync::Arc; -use sp_trie::trie_types::TrieDBMutBuilderV1; -use sp_trie::{LayoutV1, PrefixedMemoryDB, StorageProof, TrieMut}; +use sp_runtime::{BuildStorage, OpaqueExtrinsic, Saturating}; use sp_version::RuntimeVersion; -use subspace_core_primitives::{Randomness, U256 as P256}; +use subspace_core_primitives::U256 as P256; use subspace_runtime_primitives::{Moment, StorageFee, SSC}; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; @@ -294,6 +278,9 @@ impl pallet_domains::Config for Test { type DomainBundleSubmitted = (); type OnDomainInstantiated = (); type Balance = Balance; + type MmrHash = H256; + type MmrProofVerifier = (); + type FraudProofStorageKeyProvider = (); } pub struct ExtrinsicStorageFees; @@ -332,175 +319,6 @@ pub(crate) fn new_test_ext() -> sp_io::TestExternalities { t.into() } -pub(crate) struct MockDomainFraudProofExtension { - block_randomness: Randomness, - timestamp: Moment, - runtime_code: Vec, - tx_range: bool, - is_inherent: bool, - is_decodable: bool, - domain_total_stake: Balance, - bundle_slot_probability: (u64, u64), - operator_stake: Balance, - maybe_illegal_extrinsic_index: Option, - is_valid_xdm: Option, -} - -impl FraudProofHostFunctions for MockDomainFraudProofExtension { - fn get_fraud_proof_verification_info( - &self, - _consensus_block_hash: H256, - fraud_proof_verification_info_req: FraudProofVerificationInfoRequest, - ) -> Option { - let response = match fraud_proof_verification_info_req { - FraudProofVerificationInfoRequest::BlockRandomness => { - FraudProofVerificationInfoResponse::BlockRandomness(self.block_randomness) - } - FraudProofVerificationInfoRequest::DomainTimestampExtrinsic(_) => { - FraudProofVerificationInfoResponse::DomainTimestampExtrinsic( - UncheckedExtrinsic::new_unsigned( - pallet_timestamp::Call::::set { - now: self.timestamp, - } - .into(), - ) - .encode(), - ) - } - FraudProofVerificationInfoRequest::ConsensusChainByteFeeExtrinsic(_) => { - FraudProofVerificationInfoResponse::ConsensusChainByteFeeExtrinsic( - UncheckedExtrinsic::new_unsigned( - pallet_block_fees::Call::::set_next_consensus_chain_byte_fee { - transaction_byte_fee: Default::default(), - } - .into(), - ) - .encode(), - ) - } - FraudProofVerificationInfoRequest::DomainBundleBody { .. } => { - FraudProofVerificationInfoResponse::DomainBundleBody(Default::default()) - } - FraudProofVerificationInfoRequest::DomainRuntimeCode(_) => { - FraudProofVerificationInfoResponse::DomainRuntimeCode(Default::default()) - } - FraudProofVerificationInfoRequest::DomainSetCodeExtrinsic(_) => { - FraudProofVerificationInfoResponse::DomainSetCodeExtrinsic( - SetCodeExtrinsic::EncodedExtrinsic( - UncheckedExtrinsic::new_unsigned( - domain_pallet_executive::Call::::set_code { - code: self.runtime_code.clone(), - } - .into(), - ) - .encode(), - ), - ) - } - FraudProofVerificationInfoRequest::TxRangeCheck { .. } => { - FraudProofVerificationInfoResponse::TxRangeCheck(self.tx_range) - } - FraudProofVerificationInfoRequest::InherentExtrinsicCheck { .. } => { - FraudProofVerificationInfoResponse::InherentExtrinsicCheck(self.is_inherent) - } - FraudProofVerificationInfoRequest::ExtrinsicDecodableCheck { .. } => { - FraudProofVerificationInfoResponse::ExtrinsicDecodableCheck(self.is_decodable) - } - FraudProofVerificationInfoRequest::DomainElectionParams { .. } => { - FraudProofVerificationInfoResponse::DomainElectionParams { - domain_total_stake: self.domain_total_stake, - bundle_slot_probability: self.bundle_slot_probability, - } - } - FraudProofVerificationInfoRequest::OperatorStake { .. } => { - FraudProofVerificationInfoResponse::OperatorStake(self.operator_stake) - } - FraudProofVerificationInfoRequest::CheckExtrinsicsInSingleContext { .. } => { - FraudProofVerificationInfoResponse::CheckExtrinsicsInSingleContext( - self.maybe_illegal_extrinsic_index, - ) - } - FraudProofVerificationInfoRequest::StorageKey { .. } => { - FraudProofVerificationInfoResponse::StorageKey(None) - } - FraudProofVerificationInfoRequest::XDMValidationCheck { .. } => { - FraudProofVerificationInfoResponse::XDMValidationCheck(self.is_valid_xdm) - } - FraudProofVerificationInfoRequest::DomainChainsAllowlistUpdateExtrinsic(_) => { - FraudProofVerificationInfoResponse::DomainChainAllowlistUpdateExtrinsic( - DomainChainAllowlistUpdateExtrinsic::None, - ) - } - }; - - Some(response) - } - - fn derive_bundle_digest( - &self, - _consensus_block_hash: H256, - _domain_id: DomainId, - _bundle_body: Vec, - ) -> Option { - Some(H256::random()) - } - - fn derive_bundle_digest_v2( - &self, - _domain_runtime_code: Vec, - _bundle_body: Vec, - ) -> Option { - Some(H256::random()) - } - - fn execution_proof_check( - &self, - _domain_id: (u32, H256), - _pre_state_root: H256, - _encoded_proof: Vec, - _execution_method: &str, - _call_data: &[u8], - _domain_runtime_code: Vec, - ) -> Option> { - None - } - - fn check_extrinsics_in_single_context( - &self, - _domain_runtime_code: Vec, - _domain_block_id: (u32, H256), - _domain_block_state_root: H256, - _bundle_extrinsics: Vec, - _encoded_proof: Vec, - ) -> Option> { - None - } - - fn construct_domain_inherent_extrinsic( - &self, - _domain_runtime_code: Vec, - _domain_inherent_extrinsic_data: DomainInherentExtrinsicData, - ) -> Option { - None - } - - fn domain_storage_key( - &self, - _domain_runtime_code: Vec, - _req: DomainStorageKeyRequest, - ) -> Option> { - None - } - - fn domain_runtime_call( - &self, - _domain_runtime_code: Vec, - _call: StatelessDomainRuntimeCall, - ) -> Option { - None - } -} - pub(crate) fn new_test_ext_with_extensions() -> sp_io::TestExternalities { let version = RuntimeVersion { spec_name: "test".into(), @@ -910,389 +728,22 @@ fn test_invalid_fraud_proof() { .unwrap() .execution_receipt .hash::>(); - let fraud_proof = FraudProof::dummy_fraud_proof(domain_id, bad_receipt_hash); + let fraud_proof = FraudProofV2::dummy_fraud_proof(domain_id, bad_receipt_hash); assert_eq!( - Domains::validate_fraud_proof(&fraud_proof), + Domains::validate_fraud_proof_v2(&fraud_proof), Err(FraudProofError::ChallengingGenesisReceipt) ); // Fraud proof target unknown ER is invalid let bad_receipt_hash = H256::random(); - let fraud_proof = FraudProof::dummy_fraud_proof(domain_id, bad_receipt_hash); + let fraud_proof = FraudProofV2::dummy_fraud_proof(domain_id, bad_receipt_hash); assert_eq!( - Domains::validate_fraud_proof(&fraud_proof), + Domains::validate_fraud_proof_v2(&fraud_proof), Err(FraudProofError::BadReceiptNotFound) ); }); } -#[test] -fn test_invalid_block_fees_fraud_proof() { - let creator = 0u128; - let operator_id = 1u64; - let head_domain_number = 10; - let mut ext = new_test_ext_with_extensions(); - ext.execute_with(|| { - let domain_id = register_genesis_domain(creator, vec![operator_id]); - extend_block_tree_from_zero(domain_id, operator_id, head_domain_number + 2); - assert_eq!( - HeadReceiptNumber::::get(domain_id), - head_domain_number - ); - - let bad_receipt_at = 8; - let mut domain_block = get_block_tree_node_at::(domain_id, bad_receipt_at).unwrap(); - - let bad_receipt_hash = domain_block - .execution_receipt - .hash::>(); - let (fraud_proof, root) = generate_invalid_block_fees_fraud_proof::( - domain_id, - bad_receipt_hash, - // set different reward in the storage and generate proof for that value - sp_domains::BlockFees::new( - domain_block - .execution_receipt - .block_fees - .domain_execution_fee - + 1, - domain_block - .execution_receipt - .block_fees - .consensus_storage_fee - + 1, - domain_block.execution_receipt.block_fees.burned_balance + 1, - ), - ); - domain_block.execution_receipt.final_state_root = root; - BlockTreeNodes::::insert(bad_receipt_hash, domain_block); - assert_ok!(Domains::validate_fraud_proof(&fraud_proof),); - }); -} - -type FraudProofFor = - FraudProof, ::Hash, ::DomainHeader>; - -fn generate_invalid_block_fees_fraud_proof( - domain_id: DomainId, - bad_receipt_hash: ReceiptHashFor, - block_fees: sp_domains::BlockFees>, -) -> (FraudProofFor, T::Hash) { - let storage_key = sp_domains::operator_block_fees_final_key(); - let mut root = T::Hash::default(); - let mut mdb = PrefixedMemoryDB::::default(); - { - let mut trie = TrieDBMutBuilderV1::new(&mut mdb, &mut root).build(); - trie.insert(&storage_key, &block_fees.encode()).unwrap(); - }; - - let backend = TrieBackendBuilder::new(mdb, root).build(); - let (root, storage_proof) = storage_proof_for_key::(backend, StorageKey(storage_key)); - ( - FraudProof::InvalidBlockFees(InvalidBlockFeesProof { - domain_id, - bad_receipt_hash, - storage_proof, - }), - root, - ) -} - -fn storage_proof_for_key + AsTrieBackend>( - backend: B, - key: StorageKey, -) -> (T::Hash, StorageProof) { - let state_version = sp_runtime::StateVersion::default(); - let root = backend.storage_root(std::iter::empty(), state_version).0; - let proof = StorageProof::new(prove_read(backend, &[key]).unwrap().iter_nodes().cloned()); - (root, proof) -} - -#[test] -fn test_invalid_domain_extrinsic_root_proof() { - let creator = 0u128; - let operator_id = 1u64; - let head_domain_number = 10; - let mut ext = new_test_ext_with_extensions(); - let fraud_proof = ext.execute_with(|| { - let domain_id = register_genesis_domain(creator, vec![operator_id]); - extend_block_tree_from_zero(domain_id, operator_id, head_domain_number + 2); - assert_eq!( - HeadReceiptNumber::::get(domain_id), - head_domain_number - ); - - let bad_receipt_at = 8; - let valid_bundle_digests = [ValidBundleDigest { - bundle_index: 0, - bundle_digest: vec![(Some(vec![1, 2, 3]), ExtrinsicDigest::Data(vec![4, 5, 6]))], - }]; - let mut domain_block = get_block_tree_node_at::(domain_id, bad_receipt_at).unwrap(); - let bad_receipt = &mut domain_block.execution_receipt; - bad_receipt.inboxed_bundles = { - valid_bundle_digests - .iter() - .map(|vbd| { - InboxedBundle::valid(BlakeTwo256::hash_of(&vbd.bundle_digest), H256::random()) - }) - .collect() - }; - bad_receipt.domain_block_extrinsic_root = H256::random(); - - let bad_receipt_hash = bad_receipt.hash::>(); - let fraud_proof = - generate_invalid_domain_extrinsic_root_fraud_proof::(domain_id, bad_receipt_hash); - let (consensus_block_number, consensus_block_hash) = ( - bad_receipt.consensus_block_number, - bad_receipt.consensus_block_hash, - ); - ConsensusBlockHash::::insert(domain_id, consensus_block_number, consensus_block_hash); - BlockTreeNodes::::insert(bad_receipt_hash, domain_block); - fraud_proof - }); - - let fraud_proof_ext = FraudProofExtension::new(Arc::new(MockDomainFraudProofExtension { - block_randomness: Randomness::from([1u8; 32]), - timestamp: 1000, - runtime_code: vec![1, 2, 3, 4], - tx_range: true, - is_inherent: true, - is_decodable: true, - domain_total_stake: 100 * SSC, - operator_stake: 10 * SSC, - bundle_slot_probability: (0, 0), - maybe_illegal_extrinsic_index: None, - is_valid_xdm: None, - })); - ext.register_extension(fraud_proof_ext); - - ext.execute_with(|| { - assert_ok!(Domains::validate_fraud_proof(&fraud_proof),); - }) -} - -fn generate_invalid_domain_extrinsic_root_fraud_proof( - domain_id: DomainId, - bad_receipt_hash: ReceiptHashFor, -) -> FraudProof, T::Hash, T::DomainHeader> { - let valid_bundle_digests = vec![ValidBundleDigest { - bundle_index: 0, - bundle_digest: vec![(Some(vec![1, 2, 3]), ExtrinsicDigest::Data(vec![4, 5, 6]))], - }]; - - FraudProof::InvalidExtrinsicsRoot(InvalidExtrinsicsRootProof { - domain_id, - bad_receipt_hash, - valid_bundle_digests, - }) -} - -#[test] -fn test_true_invalid_bundles_inherent_extrinsic_proof() { - let creator = 0u128; - let operator_id = 1u64; - let head_domain_number = 10; - let mut ext = new_test_ext_with_extensions(); - let fraud_proof = ext.execute_with(|| { - let domain_id = register_genesis_domain(creator, vec![operator_id]); - extend_block_tree_from_zero(domain_id, operator_id, head_domain_number + 2); - assert_eq!( - HeadReceiptNumber::::get(domain_id), - head_domain_number - ); - - let inherent_extrinsic = vec![1, 2, 3].encode(); - let extrinsics = vec![inherent_extrinsic]; - let bundle_extrinsic_root = - BlakeTwo256::ordered_trie_root(extrinsics.clone(), StateVersion::V1); - - let bad_receipt_at = 8; - let mut domain_block = get_block_tree_node_at::(domain_id, bad_receipt_at).unwrap(); - let bad_receipt = &mut domain_block.execution_receipt; - // bad receipt marks this particular bundle as valid even though bundle contains inherent extrinsic - bad_receipt.inboxed_bundles = - vec![InboxedBundle::valid(H256::random(), bundle_extrinsic_root)]; - bad_receipt.domain_block_extrinsic_root = H256::random(); - - let bad_receipt_hash = bad_receipt.hash::>(); - let fraud_proof = generate_invalid_bundle_inherent_extrinsic_fraud_proof::( - domain_id, - bad_receipt_hash, - 0, - 0, - extrinsics, - true, - ); - let (consensus_block_number, consensus_block_hash) = ( - bad_receipt.consensus_block_number, - bad_receipt.consensus_block_hash, - ); - ConsensusBlockHash::::insert(domain_id, consensus_block_number, consensus_block_hash); - BlockTreeNodes::::insert(bad_receipt_hash, domain_block); - fraud_proof - }); - - let fraud_proof_ext = FraudProofExtension::new(Arc::new(MockDomainFraudProofExtension { - block_randomness: Randomness::from([1u8; 32]), - timestamp: 1000, - runtime_code: vec![1, 2, 3, 4], - tx_range: true, - // return `true` indicating this is an inherent extrinsic - is_inherent: true, - is_decodable: true, - domain_total_stake: 100 * SSC, - operator_stake: 10 * SSC, - bundle_slot_probability: (0, 0), - maybe_illegal_extrinsic_index: None, - is_valid_xdm: None, - })); - ext.register_extension(fraud_proof_ext); - - ext.execute_with(|| { - assert_ok!(Domains::validate_fraud_proof(&fraud_proof),); - }) -} - -#[test] -fn test_false_invalid_bundles_inherent_extrinsic_proof() { - let creator = 0u128; - let operator_id = 1u64; - let head_domain_number = 10; - let mut ext = new_test_ext_with_extensions(); - let fraud_proof = ext.execute_with(|| { - let domain_id = register_genesis_domain(creator, vec![operator_id]); - extend_block_tree_from_zero(domain_id, operator_id, head_domain_number + 2); - assert_eq!( - HeadReceiptNumber::::get(domain_id), - head_domain_number - ); - - let non_inherent_extrinsic = vec![1, 2, 3].encode(); - let extrinsics = vec![non_inherent_extrinsic]; - let bundle_extrinsic_root = - BlakeTwo256::ordered_trie_root(extrinsics.clone(), StateVersion::V1); - - let bad_receipt_at = 8; - let mut domain_block = get_block_tree_node_at::(domain_id, bad_receipt_at).unwrap(); - let bad_receipt = &mut domain_block.execution_receipt; - // bad receipt marks this bundle as invalid even though bundle do not contain inherent extrinsic. - bad_receipt.inboxed_bundles = vec![InboxedBundle::invalid( - InvalidBundleType::InherentExtrinsic(0), - bundle_extrinsic_root, - )]; - bad_receipt.domain_block_extrinsic_root = H256::random(); - - let bad_receipt_hash = bad_receipt.hash::>(); - let fraud_proof = generate_invalid_bundle_inherent_extrinsic_fraud_proof::( - domain_id, - bad_receipt_hash, - 0, - 0, - extrinsics, - false, - ); - let (consensus_block_number, consensus_block_hash) = ( - bad_receipt.consensus_block_number, - bad_receipt.consensus_block_hash, - ); - ConsensusBlockHash::::insert(domain_id, consensus_block_number, consensus_block_hash); - BlockTreeNodes::::insert(bad_receipt_hash, domain_block); - fraud_proof - }); - - let fraud_proof_ext = FraudProofExtension::new(Arc::new(MockDomainFraudProofExtension { - block_randomness: Randomness::from([1u8; 32]), - timestamp: 1000, - runtime_code: vec![1, 2, 3, 4], - tx_range: true, - // return `false` indicating this is not an inherent extrinsic - is_inherent: false, - is_decodable: true, - domain_total_stake: 100 * SSC, - operator_stake: 10 * SSC, - bundle_slot_probability: (0, 0), - maybe_illegal_extrinsic_index: None, - is_valid_xdm: None, - })); - ext.register_extension(fraud_proof_ext); - - ext.execute_with(|| { - assert_ok!(Domains::validate_fraud_proof(&fraud_proof),); - }) -} - -fn generate_invalid_bundle_inherent_extrinsic_fraud_proof( - domain_id: DomainId, - bad_receipt_hash: ReceiptHashFor, - bundle_index: u32, - bundle_extrinsic_index: u32, - bundle_extrinsics: Vec>, - is_true_invalid_fraud_proof: bool, -) -> FraudProof, T::Hash, T::DomainHeader> { - let extrinsic_inclusion_proof = - StorageProofProvider::>::generate_enumerated_proof_of_inclusion( - bundle_extrinsics.as_slice(), - bundle_extrinsic_index, - ) - .unwrap(); - FraudProof::InvalidBundles(InvalidBundlesFraudProof { - domain_id, - bad_receipt_hash, - bundle_index, - invalid_bundle_type: InvalidBundleType::InherentExtrinsic(bundle_extrinsic_index), - proof_data: extrinsic_inclusion_proof, - is_true_invalid_fraud_proof, - }) -} - -#[test] -fn test_invalid_domain_block_hash_fraud_proof() { - let creator = 0u128; - let operator_id = 1u64; - let head_domain_number = 10; - let mut ext = new_test_ext_with_extensions(); - ext.execute_with(|| { - let domain_id = register_genesis_domain(creator, vec![operator_id]); - extend_block_tree_from_zero(domain_id, operator_id, head_domain_number + 2); - assert_eq!( - HeadReceiptNumber::::get(domain_id), - head_domain_number - ); - - let bad_receipt_at = 8; - let mut domain_block = get_block_tree_node_at::(domain_id, bad_receipt_at).unwrap(); - let (root, digest_storage_proof) = - generate_invalid_domain_block_hash_fraud_proof::(Digest::default()); - domain_block.execution_receipt.final_state_root = root; - domain_block.execution_receipt.domain_block_hash = H256::random(); - let bad_receipt_hash = domain_block - .execution_receipt - .hash::>(); - BlockTreeNodes::::insert(bad_receipt_hash, domain_block); - let fraud_proof = FraudProof::InvalidDomainBlockHash(InvalidDomainBlockHashProof { - domain_id, - bad_receipt_hash, - digest_storage_proof, - }); - assert_ok!(Domains::validate_fraud_proof(&fraud_proof),); - }); -} - -fn generate_invalid_domain_block_hash_fraud_proof( - digest: Digest, -) -> (T::Hash, StorageProof) { - let digest_storage_key = sp_domains::system_digest_final_key(); - let mut root = T::Hash::default(); - let mut mdb = PrefixedMemoryDB::::default(); - { - let mut trie = TrieDBMutBuilderV1::new(&mut mdb, &mut root).build(); - trie.insert(&digest_storage_key, &digest.encode()).unwrap(); - }; - - let backend = TrieBackendBuilder::new(mdb, root).build(); - storage_proof_for_key::(backend, StorageKey(digest_storage_key)) -} - #[test] fn test_basic_fraud_proof_processing() { let creator = 0u128; @@ -1322,7 +773,7 @@ fn test_basic_fraud_proof_processing() { .unwrap() .execution_receipt; let bad_receipt_hash = bad_receipt.hash::>(); - let fraud_proof = FraudProof::dummy_fraud_proof(domain_id, bad_receipt_hash); + let fraud_proof = FraudProofV2::dummy_fraud_proof(domain_id, bad_receipt_hash); assert_ok!(Domains::submit_fraud_proof( RawOrigin::None.into(), Box::new(fraud_proof) diff --git a/crates/sp-domains-fraud-proof/Cargo.toml b/crates/sp-domains-fraud-proof/Cargo.toml index 068e8d803c..130165ff61 100644 --- a/crates/sp-domains-fraud-proof/Cargo.toml +++ b/crates/sp-domains-fraud-proof/Cargo.toml @@ -31,6 +31,7 @@ sp-messenger = { version = "0.1.0", default-features = false, path = "../../doma sp-runtime = { default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } sp-runtime-interface = { default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } sp-state-machine = { optional = true, git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } +sp-subspace-mmr = { version = "0.1.0", default-features = false, path = "../sp-subspace-mmr" } sp-std = { default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } sp-trie = { default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } subspace-core-primitives = { version = "0.1.0", default-features = false, path = "../subspace-core-primitives" } @@ -58,6 +59,8 @@ rlp = "0.5.2" sp-core = { default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } sc-cli = { git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed", default-features = false } sc-service = { git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed", default-features = false } +sp-state-machine = { default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } +sp-storage = { default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed"} subspace-test-client = { version = "0.1.0", path = "../../test/subspace-test-client" } subspace-test-service = { version = "0.1.0", path = "../../test/subspace-test-service" } subspace-runtime-primitives = { version = "0.1.0", path = "../../crates/subspace-runtime-primitives" } @@ -90,6 +93,7 @@ std = [ "sp-runtime-interface/std", "sp-std/std", "sp-state-machine/std", + "sp-subspace-mmr/std", "sp-trie/std", "subspace-core-primitives/std", "subspace-runtime-primitives/std", diff --git a/crates/sp-domains-fraud-proof/src/bundle_equivocation.rs b/crates/sp-domains-fraud-proof/src/bundle_equivocation.rs deleted file mode 100644 index 8bba0768e1..0000000000 --- a/crates/sp-domains-fraud-proof/src/bundle_equivocation.rs +++ /dev/null @@ -1,294 +0,0 @@ -//! Module to check bundle equivocation and produce the Equivocation fraud proof. -//! This is mostly derived from the `sc_consensus_slots::aux_schema` with changes adapted -//! for Bundle headers instead of block headers - -use crate::fraud_proof::{BundleEquivocationProof, FraudProof}; -use codec::{Decode, Encode}; -use sc_client_api::backend::AuxStore; -use sp_blockchain::{Error as ClientError, Result as ClientResult}; -use sp_consensus_slots::Slot; -use sp_domains::SealedBundleHeader; -use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor}; -use std::sync::Arc; -use subspace_runtime_primitives::Balance; - -const SLOT_BUNDLE_HEADER_MAP_KEY: &[u8] = b"slot_bundle_header_map"; -const SLOT_BUNDLE_HEADER_START: &[u8] = b"slot_bundle_header_start"; - -// TODO: revisit these values when there more than 1000 domains. -/// We keep at least this number of slots in database. -const MAX_SLOT_CAPACITY: u64 = 1000; -/// We prune slots when they reach this number. -const PRUNING_BOUND: u64 = 2 * MAX_SLOT_CAPACITY; - -fn load_decode(client: &Arc, key: &[u8]) -> ClientResult> -where - CClient: AuxStore, - T: Decode, -{ - match client.get_aux(key)? { - None => Ok(None), - Some(t) => T::decode(&mut &t[..]) - .map_err(|e| { - ClientError::Backend(format!("Slots DB is corrupted. Decode error: {}", e)) - }) - .map(Some), - } -} - -pub type CheckEquivocationResult = - ClientResult>>; - -/// Checks if the header is an equivocation and returns the proof in that case. -/// -/// Note: it detects equivocations only when slot_now - slot <= MAX_SLOT_CAPACITY. -pub fn check_equivocation( - backend: &Arc, - slot_now: Slot, - bundle_header: SealedBundleHeader, CBlock::Hash, DomainHeader, Balance>, -) -> CheckEquivocationResult, CBlock::Hash, DomainHeader> -where - CClient: AuxStore, - CBlock: BlockT, - DomainHeader: HeaderT, -{ - let slot: Slot = bundle_header.header.proof_of_election.slot_number.into(); - - // We don't check equivocations for old headers out of our capacity. - if slot_now.saturating_sub(*slot) > MAX_SLOT_CAPACITY { - return Ok(None); - } - - // Key for this slot. - let mut curr_slot_key = SLOT_BUNDLE_HEADER_MAP_KEY.to_vec(); - slot.using_encoded(|s| curr_slot_key.extend(s)); - - // Get headers of this slot. - let mut headers_with_sig = load_decode::< - CClient, - Vec, CBlock::Hash, DomainHeader, Balance>>, - >(backend, &curr_slot_key[..])? - .unwrap_or_else(Vec::new); - - // Get first slot saved. - let slot_header_start = SLOT_BUNDLE_HEADER_START.to_vec(); - let first_saved_slot = load_decode::<_, Slot>(backend, &slot_header_start[..])?.unwrap_or(slot); - - if slot_now < first_saved_slot { - // The code below assumes that slots will be visited sequentially. - return Ok(None); - } - - for previous_bundle_header in headers_with_sig.iter() { - let operator_set_1 = ( - previous_bundle_header.header.proof_of_election.operator_id, - previous_bundle_header.header.proof_of_election.domain_id, - ); - let operator_set_2 = ( - bundle_header.header.proof_of_election.operator_id, - bundle_header.header.proof_of_election.domain_id, - ); - - // A proof of equivocation consists of two headers: - // 1) signed by the same operator for same domain - if operator_set_1 == operator_set_2 { - // 2) with different hash - return if bundle_header.hash() != previous_bundle_header.hash() { - log::warn!( - "Bundle equivocation occurred: Operator{}; Slot{}; DomainId{}; First Bundle{}; Second Bundle{}", - operator_set_1.0, - slot, - operator_set_1.1, - previous_bundle_header.hash(), - bundle_header.hash(), - ); - Ok(Some(FraudProof::BundleEquivocation( - BundleEquivocationProof { - domain_id: bundle_header.header.proof_of_election.domain_id, - slot, - first_header: previous_bundle_header.clone(), - second_header: bundle_header, - }, - ))) - } else { - // We don't need to continue in case of duplicated header, - // since it's already saved and a possible equivocation - // would have been detected before. - Ok(None) - }; - } - } - - let mut keys_to_delete = vec![]; - let mut new_first_saved_slot = first_saved_slot; - - if *slot_now - *first_saved_slot >= PRUNING_BOUND { - let prefix = SLOT_BUNDLE_HEADER_MAP_KEY.to_vec(); - new_first_saved_slot = slot_now.saturating_sub(MAX_SLOT_CAPACITY); - - for s in u64::from(first_saved_slot)..new_first_saved_slot.into() { - let mut p = prefix.clone(); - s.using_encoded(|s| p.extend(s)); - keys_to_delete.push(p); - } - } - - headers_with_sig.push(bundle_header); - - backend.insert_aux( - &[ - (&curr_slot_key[..], headers_with_sig.encode().as_slice()), - ( - &slot_header_start[..], - new_first_saved_slot.encode().as_slice(), - ), - ], - &keys_to_delete - .iter() - .map(|k| &k[..]) - .collect::>()[..], - )?; - - Ok(None) -} - -#[cfg(test)] -mod test { - use super::{check_equivocation, MAX_SLOT_CAPACITY, PRUNING_BOUND}; - use domain_runtime_primitives::opaque::Header as DomainHeader; - use parking_lot::Mutex; - use sc_client_api::backend::AuxStore; - use sp_core::crypto::UncheckedFrom; - use sp_domains::{ - BundleHeader, DomainId, ExecutionReceipt, OperatorId, OperatorSignature, ProofOfElection, - SealedBundleHeader, - }; - use std::collections::HashMap; - use std::sync::Arc; - use subspace_runtime_primitives::opaque::Block; - use subspace_runtime_primitives::{Balance, BlockNumber, Hash}; - - fn create_header( - number: BlockNumber, - slot_number: u64, - domain_id: DomainId, - operator_id: OperatorId, - ) -> SealedBundleHeader { - let mut poe = ProofOfElection::dummy(domain_id, operator_id); - poe.slot_number = slot_number; - SealedBundleHeader { - header: BundleHeader { - proof_of_election: poe, - receipt: ExecutionReceipt { - domain_block_number: number, - domain_block_hash: Default::default(), - domain_block_extrinsic_root: Default::default(), - parent_domain_block_receipt_hash: Default::default(), - consensus_block_number: number, - consensus_block_hash: Default::default(), - inboxed_bundles: vec![], - final_state_root: Default::default(), - execution_trace: vec![], - execution_trace_root: Default::default(), - block_fees: Default::default(), - transfers: Default::default(), - }, - estimated_bundle_weight: Default::default(), - bundle_extrinsics_root: Default::default(), - }, - signature: OperatorSignature::unchecked_from([0u8; 64]), - } - } - - #[derive(Default)] - struct TestClient(Mutex, Vec>>); - - impl AuxStore for TestClient { - fn insert_aux< - 'a, - 'b: 'a, - 'c: 'a, - I: IntoIterator, - D: IntoIterator, - >( - &self, - insert: I, - delete: D, - ) -> sp_blockchain::Result<()> { - let mut map = self.0.lock(); - for d in delete { - map.remove(&d.to_vec()); - } - for (k, v) in insert { - map.insert(k.to_vec(), v.to_vec()); - } - Ok(()) - } - - fn get_aux(&self, key: &[u8]) -> sp_blockchain::Result>> { - Ok(self.0.lock().get(key).cloned()) - } - } - - #[test] - fn test_check_equivocation() { - let client = Arc::new(TestClient::default()); - let domain_id = DomainId::new(0); - let operator_id = 1; - - let header1 = create_header(1, 2, domain_id, operator_id); // @ slot 2 - let header2 = create_header(2, 2, domain_id, operator_id); // @ slot 2 - let header3 = create_header(2, 4, domain_id, operator_id); // @ slot 4 - let header4 = create_header(3, MAX_SLOT_CAPACITY + 4, domain_id, operator_id); // @ slot MAX_SLOT_CAPACITY + 4 - let header5 = create_header(4, MAX_SLOT_CAPACITY + 4, domain_id, operator_id); // @ slot MAX_SLOT_CAPACITY + 4 - let header6 = create_header(3, 4, domain_id, operator_id); // @ slot 4 - - // It's ok to sign same headers. - assert!( - check_equivocation::<_, Block, _>(&client, 2.into(), header1.clone()) - .unwrap() - .is_none(), - ); - - assert!( - check_equivocation::<_, Block, _>(&client, 3.into(), header1.clone()) - .unwrap() - .is_none(), - ); - - // But not two different headers at the same slot. - assert!( - check_equivocation::<_, Block, _>(&client, 4.into(), header2) - .unwrap() - .is_some(), - ); - - // Different slot is ok. - assert!( - check_equivocation::<_, Block, _>(&client, 5.into(), header3) - .unwrap() - .is_none(), - ); - - // Here we trigger pruning and save header 4. - assert!( - check_equivocation::<_, Block, _>(&client, (PRUNING_BOUND + 2).into(), header4,) - .unwrap() - .is_none(), - ); - - // This fails because header 5 is an equivocation of header 4. - assert!( - check_equivocation::<_, Block, _>(&client, (PRUNING_BOUND + 3).into(), header5,) - .unwrap() - .is_some(), - ); - - // This is ok because we pruned the corresponding header. Shows that we are pruning. - assert!( - check_equivocation::<_, Block, _>(&client, (PRUNING_BOUND + 4).into(), header6,) - .unwrap() - .is_none(), - ); - } -} diff --git a/crates/sp-domains-fraud-proof/src/fraud_proof.rs b/crates/sp-domains-fraud-proof/src/fraud_proof.rs index 19e28a0040..c00c9bc8f8 100644 --- a/crates/sp-domains-fraud-proof/src/fraud_proof.rs +++ b/crates/sp-domains-fraud-proof/src/fraud_proof.rs @@ -1,22 +1,22 @@ #[cfg(not(feature = "std"))] extern crate alloc; -use crate::verification::InvalidBundleEquivocationError; +use crate::storage_proof::{self, *}; #[cfg(not(feature = "std"))] use alloc::vec::Vec; use codec::{Decode, Encode}; use core::fmt; use scale_info::TypeInfo; -use sp_consensus_slots::Slot; use sp_core::H256; use sp_domain_digests::AsPredigest; use sp_domains::proof_provider_and_verifier::StorageProofVerifier; use sp_domains::{ BundleValidity, DomainId, ExecutionReceiptFor, ExtrinsicDigest, HeaderHashFor, - HeaderHashingFor, InvalidBundleType, OperatorId, SealedBundleHeader, + HeaderHashingFor, InvalidBundleType, }; use sp_runtime::traits::{Block as BlockT, Hash as HashT, Header as HeaderT}; use sp_runtime::{Digest, DigestItem}; +use sp_subspace_mmr::ConsensusChainMmrLeafProof; use sp_trie::StorageProof; use subspace_runtime_primitives::Balance; @@ -402,345 +402,260 @@ pub enum VerificationError { error("Failed to check if a given extrinsic is decodable or not") )] FailedToCheckExtrinsicDecodable, - /// Invalid bundle equivocation fraud proof. - #[cfg_attr( - feature = "thiserror", - error("Invalid bundle equivocation fraud proof: {0}") - )] - InvalidBundleEquivocationFraudProof(InvalidBundleEquivocationError), /// Failed to check extrinsics in single context #[cfg_attr( feature = "thiserror", error("Failed to check extrinsics in single context") )] FailedToCheckExtrinsicsInSingleContext, + #[cfg_attr( + feature = "thiserror", + error( + "Bad MMR proof, the proof is probably expired or is generated against a different fork" + ) + )] + BadMmrProof, + #[cfg_attr(feature = "thiserror", error("Unexpected MMR proof"))] + UnexpectedMmrProof, + #[cfg_attr(feature = "thiserror", error("Failed to verify storage proof"))] + StorageProof(storage_proof::VerificationError), + /// Failed to derive domain inherent extrinsic + #[cfg_attr( + feature = "thiserror", + error("Failed to derive domain inherent extrinsic") + )] + FailedToDeriveDomainInherentExtrinsic, + /// Failed to derive domain storage key + #[cfg_attr(feature = "thiserror", error("Failed to derive domain storage key"))] + FailedToGetDomainStorageKey, + /// Unexpected invalid bundle proof data + #[cfg_attr(feature = "thiserror", error("Unexpected invalid bundle proof data"))] + UnexpectedInvalidBundleProofData, + /// Extrinsic with requested index not found in bundle + #[cfg_attr( + feature = "thiserror", + error("Extrinsic with requested index not found in bundle") + )] + ExtrinsicNotFound, + /// Failed to get domain runtime call response + #[cfg_attr( + feature = "thiserror", + error("Failed to get domain runtime call response") + )] + FailedToGetDomainRuntimeCallResponse, } -impl From for VerificationError { - fn from(err: InvalidBundleEquivocationError) -> Self { - Self::InvalidBundleEquivocationFraudProof(err) +impl From for VerificationError { + fn from(err: storage_proof::VerificationError) -> Self { + Self::StorageProof(err) } } +// Domain runtime code at a specific block #[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] -pub struct InvalidBundlesFraudProof { - pub bad_receipt_hash: ReceiptHash, - pub domain_id: DomainId, +pub struct DomainRuntimeCodeAt { + pub mmr_proof: ConsensusChainMmrLeafProof, + pub domain_runtime_code_proof: DomainRuntimeCodeProof, +} + +/// Proves an invalid state transition by challenging the trace at specific index in a bad receipt. +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] +pub struct InvalidStateTransitionProofV2 { + /// Proof recorded during the computation. + pub execution_proof: StorageProof, + /// Execution phase. + pub execution_phase: ExecutionPhase, +} + +/// Fraud proof for the valid bundles in `ExecutionReceipt::inboxed_bundles` +#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] +pub struct ValidBundleProofV2 { + /// The targetted bundle with proof + pub bundle_with_proof: OpaqueBundleWithProof, +} + +#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] +pub struct InvalidExtrinsicsRootProofV2 { + /// Valid Bundle digests + pub valid_bundle_digests: Vec, + /// Block randomness storage proof + pub block_randomness_proof: BlockRandomnessProof, + /// The storage proof used during verification + pub domain_inherent_extrinsic_data_proof: DomainInherentExtrinsicDataProof, +} + +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] +pub enum InvalidBundlesProofData { + Extrinsic(StorageProof), + Bundle(OpaqueBundleWithProof), + BundleAndExecution { + bundle_with_proof: OpaqueBundleWithProof, + execution_proof: StorageProof, + }, +} + +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] +pub struct InvalidBundlesProofV2 { pub bundle_index: u32, pub invalid_bundle_type: InvalidBundleType, - pub proof_data: StorageProof, pub is_true_invalid_fraud_proof: bool, + /// Proof data of the invalid bundle + pub proof_data: InvalidBundlesProofData, } -impl InvalidBundlesFraudProof { - pub fn new( - bad_receipt_hash: ReceiptHash, - domain_id: DomainId, - bundle_index: u32, - invalid_bundle_type: InvalidBundleType, - proof_data: StorageProof, - is_true_invalid_fraud_proof: bool, - ) -> Self { - Self { - bad_receipt_hash, - domain_id, - bundle_index, - invalid_bundle_type, - proof_data, - is_true_invalid_fraud_proof, - } - } +/// Represents an invalid block fees proof. +#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] +pub struct InvalidBlockFeesProofV2 { + /// Storage witness needed for verifying this proof. + pub storage_proof: StorageProof, +} + +/// Represents an invalid transfers proof. +#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] +pub struct InvalidTransfersProofV2 { + /// Storage witness needed for verifying this proof. + pub storage_proof: StorageProof, +} + +/// Represents an invalid domain block hash fraud proof. +#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] +pub struct InvalidDomainBlockHashProofV2 { + /// Digests storage proof that is used to derive Domain block hash. + pub digest_storage_proof: StorageProof, } -/// Fraud proof. -// TODO: Revisit when fraud proof v2 is implemented. -#[allow(clippy::large_enum_variant)] #[derive(Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] -pub enum FraudProof { - InvalidStateTransition(InvalidStateTransitionProof>), - InvalidTransaction(InvalidTransactionProof>), - BundleEquivocation(BundleEquivocationProof), - ImproperTransactionSortition(ImproperTransactionSortitionProof>), - InvalidBlockFees(InvalidBlockFeesProof>), - InvalidExtrinsicsRoot(InvalidExtrinsicsRootProof>), - ValidBundle(ValidBundleProof>), - InvalidDomainBlockHash(InvalidDomainBlockHashProof>), - InvalidBundles(InvalidBundlesFraudProof>), - InvalidTransfers(InvalidTransfersProof>), +pub struct FraudProofV2 { + pub domain_id: DomainId, + /// Hash of the bad receipt this fraud proof targeted + pub bad_receipt_hash: HeaderHashFor, + /// The MMR proof for the consensus state root that used to verify the storage proof + /// + /// It is set `None` if the specific fraud proof variant doesn't contains storage proof + pub maybe_mmr_proof: Option>, + /// The domain runtime code storage proof + /// + /// It is set `None` if the specific fraud proof variant doesn't required domain runtime code + /// or the required domain runtime code is available from the current runtime state. + pub maybe_domain_runtime_code_proof: Option>, + /// The specific fraud proof variant + pub proof: FraudProofVariant, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] +pub enum FraudProofVariant { + InvalidStateTransition(InvalidStateTransitionProofV2), + ValidBundle(ValidBundleProofV2), + InvalidExtrinsicsRoot(InvalidExtrinsicsRootProofV2), + InvalidBundles(InvalidBundlesProofV2), + InvalidDomainBlockHash(InvalidDomainBlockHashProofV2), + InvalidBlockFees(InvalidBlockFeesProofV2), + InvalidTransfers(InvalidTransfersProofV2), // Dummy fraud proof only used in test and benchmark // // NOTE: the `Dummy` must be the last variant, because the `#[cfg(..)]` will apply to // all the variants after it. #[cfg(any(feature = "std", feature = "runtime-benchmarks"))] - Dummy { - /// Id of the domain this fraud proof targeted - domain_id: DomainId, - /// Hash of the bad receipt this fraud proof targeted - bad_receipt_hash: HeaderHashFor, - }, + Dummy, } -impl FraudProof { +impl + FraudProofV2 +{ pub fn domain_id(&self) -> DomainId { - match self { - Self::InvalidStateTransition(proof) => proof.domain_id, - Self::InvalidTransaction(proof) => proof.domain_id, - Self::BundleEquivocation(proof) => proof.domain_id, - Self::ImproperTransactionSortition(proof) => proof.domain_id, - #[cfg(any(feature = "std", feature = "runtime-benchmarks"))] - Self::Dummy { domain_id, .. } => *domain_id, - Self::InvalidBlockFees(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, - Self::InvalidTransfers(proof) => proof.domain_id, - } + self.domain_id } - pub fn targeted_bad_receipt_hash(&self) -> Option> { - match self { - Self::InvalidStateTransition(proof) => Some(proof.bad_receipt_hash), - Self::InvalidTransaction(proof) => Some(proof.bad_receipt_hash), - Self::ImproperTransactionSortition(proof) => Some(proof.bad_receipt_hash), - Self::BundleEquivocation(_) => None, - #[cfg(any(feature = "std", feature = "runtime-benchmarks"))] - Self::Dummy { - bad_receipt_hash, .. - } => Some(*bad_receipt_hash), - Self::InvalidExtrinsicsRoot(proof) => Some(proof.bad_receipt_hash), - Self::InvalidBlockFees(proof) => Some(proof.bad_receipt_hash()), - Self::ValidBundle(proof) => Some(proof.bad_receipt_hash), - Self::InvalidBundles(proof) => Some(proof.bad_receipt_hash), - Self::InvalidDomainBlockHash(proof) => Some(proof.bad_receipt_hash), - Self::InvalidTransfers(proof) => Some(proof.bad_receipt_hash), - } + pub fn targeted_bad_receipt_hash(&self) -> HeaderHashFor { + self.bad_receipt_hash } - pub fn targeted_bad_operator_and_slot_for_bundle_equivocation( - &self, - ) -> Option<(OperatorId, Slot)> { - match self { - Self::BundleEquivocation(proof) => Some(( - proof.first_header.header.proof_of_election.operator_id, - proof.slot, - )), - _ => None, + pub fn is_unexpected_domain_runtime_code_proof(&self) -> bool { + // The invalid domain block hash fraud proof doesn't use the domain runtime code + // during its verification so it is unexpected to see `maybe_domain_runtime_code_proof` + // set to `Some` + self.maybe_domain_runtime_code_proof.is_some() + && matches!(self.proof, FraudProofVariant::InvalidDomainBlockHash(_)) + } + + pub fn is_unexpected_mmr_proof(&self) -> bool { + if self.maybe_mmr_proof.is_none() { + return false; } + // Only the `InvalidExtrinsicsRoot`, `InvalidBundles` and `ValidBundle` fraud proof + // are using the MMR proof during verifiction, for other fraud proofs it is unexpected + // to see `maybe_mmr_proof` set to `Some` + !matches!( + self.proof, + FraudProofVariant::InvalidExtrinsicsRoot(_) + | FraudProofVariant::InvalidBundles(_) + | FraudProofVariant::ValidBundle(_) + ) } #[cfg(any(feature = "std", feature = "runtime-benchmarks"))] pub fn dummy_fraud_proof( domain_id: DomainId, bad_receipt_hash: HeaderHashFor, - ) -> FraudProof { - FraudProof::Dummy { + ) -> FraudProofV2 { + Self { domain_id, bad_receipt_hash, + maybe_mmr_proof: None, + maybe_domain_runtime_code_proof: None, + proof: FraudProofVariant::Dummy, } } } -impl FraudProof +impl FraudProofV2 where Number: Encode, Hash: Encode, + MmrHash: Encode, { pub fn hash(&self) -> HeaderHashFor { HeaderHashingFor::::hash(&self.encode()) } } -impl fmt::Debug for FraudProof { +impl fmt::Display + for FraudProofV2 +{ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let domain_id = self.domain_id(); - let bad_receipt_hash = self.targeted_bad_receipt_hash(); - let bad_operator = self.targeted_bad_operator_and_slot_for_bundle_equivocation(); - match self { - Self::InvalidStateTransition(_) => { - write!( - f, - "InvalidStateTransitionFraudProof({domain_id:?}#{bad_receipt_hash:?})" - ) + let fp_target = + scale_info::prelude::format!("{:?}#{:?}", self.domain_id, self.bad_receipt_hash); + match self.proof { + FraudProofVariant::InvalidStateTransition(_) => { + write!(f, "InvalidStateTransitionFraudProof({fp_target})") } - Self::InvalidTransaction(_) => { - write!( - f, - "InvalidTransactionFraudProof({domain_id:?}#{bad_receipt_hash:?})" - ) - } - Self::ImproperTransactionSortition(_) => { - write!( - f, - "ImproperTransactionSortitionFraudProof({domain_id:?}#{bad_receipt_hash:?})" - ) - } - Self::BundleEquivocation(_) => { - write!( - f, - "BundleEquivocationFraudProof({domain_id:?}#{bad_operator:?})" - ) - } - Self::InvalidExtrinsicsRoot(_) => { - write!( - f, - "InvalidExtrinsicsRootFraudProof({domain_id:?}#{bad_receipt_hash:?})" - ) + FraudProofVariant::InvalidExtrinsicsRoot(_) => { + write!(f, "InvalidExtrinsicsRootFraudProof({fp_target})") } - Self::InvalidBlockFees(_) => { - write!( - f, - "InvalidBlockFeesFraudProof({domain_id:?}#{bad_receipt_hash:?})" - ) + FraudProofVariant::InvalidBlockFees(_) => { + write!(f, "InvalidBlockFeesFraudProof({fp_target})") } - Self::ValidBundle(_) => { - write!( - f, - "ValidBundleFraudProof({domain_id:?}#{bad_receipt_hash:?})" - ) + FraudProofVariant::ValidBundle(_) => { + write!(f, "ValidBundleFraudProof({fp_target})") } - Self::InvalidBundles(_) => { - write!( - f, - "InvalidBundlesFraudProof({domain_id:?}#{bad_receipt_hash:?})" - ) + FraudProofVariant::InvalidBundles(_) => { + write!(f, "InvalidBundlesFraudProof({fp_target})") } - Self::InvalidDomainBlockHash(_) => { - write!( - f, - "InvalidDomainBlockHashFraudProof({domain_id:?}#{bad_receipt_hash:?})" - ) + FraudProofVariant::InvalidDomainBlockHash(_) => { + write!(f, "InvalidDomainBlockHashFraudProof({fp_target})") } - Self::InvalidTransfers(_) => { - write!( - f, - "InvalidTransfersFraudProof({domain_id:?}#{bad_receipt_hash:?})" - ) + FraudProofVariant::InvalidTransfers(_) => { + write!(f, "InvalidTransfersFraudProof({fp_target})") } #[cfg(any(feature = "std", feature = "runtime-benchmarks"))] - Self::Dummy { .. } => { - write!(f, "DummyFraudProof({domain_id:?}#{bad_receipt_hash:?})") + FraudProofVariant::Dummy => { + write!(f, "DummyFraudProof({fp_target})") } } } } -/// Proves an invalid state transition by challenging the trace at specific index in a bad receipt. -#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] -pub struct InvalidStateTransitionProof { - /// The id of the domain this fraud proof targeted - pub domain_id: DomainId, - /// Hash of the bad receipt in which an invalid trace occurred. - pub bad_receipt_hash: ReceiptHash, - /// Proof recorded during the computation. - pub proof: StorageProof, - /// Execution phase. - pub execution_phase: ExecutionPhase, -} - -pub fn dummy_invalid_state_transition_proof( - domain_id: DomainId, -) -> InvalidStateTransitionProof { - InvalidStateTransitionProof { - domain_id, - bad_receipt_hash: ReceiptHash::default(), - proof: StorageProof::empty(), - execution_phase: ExecutionPhase::FinalizeBlock { - mismatch: FinalizeBlockMismatch::StateRoot, - }, - } -} - -/// Represents a bundle equivocation proof. An equivocation happens when an executor -/// produces more than one bundle on the same slot. The proof of equivocation -/// are the given distinct bundle headers that were signed by the validator and which -/// include the slot number. -#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] -pub struct BundleEquivocationProof { - /// The id of the domain this fraud proof targeted - pub domain_id: DomainId, - /// The slot at which the equivocation happened. - pub slot: Slot, - // TODO: The generic type should be `` - // TODO: `SealedBundleHeader` contains `ExecutionReceipt` which make the size of the proof - // large, revisit when proceeding to fraud proof v2. - /// The first header involved in the equivocation. - pub first_header: SealedBundleHeader, - /// The second header involved in the equivocation. - pub second_header: SealedBundleHeader, -} - -impl BundleEquivocationProof -where - Number: Clone + From + Encode, - Hash: Clone + Default + Encode, - DomainHeader: HeaderT, -{ - /// Returns the hash of this bundle equivocation proof. - pub fn hash(&self) -> HeaderHashFor { - HeaderHashingFor::::hash_of(self) - } -} - -/// Represents an invalid transaction proof. -#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] -pub struct InvalidTransactionProof { - /// The id of the domain this fraud proof targeted - pub domain_id: DomainId, - /// Hash of the bad receipt this fraud proof targeted - pub bad_receipt_hash: DomainHash, - /// Number of the block at which the invalid transaction occurred. - pub domain_block_number: u32, - /// Hash of the domain block corresponding to `block_number`. - pub domain_block_hash: DomainHash, - // TODO: Verifiable invalid extrinsic. - pub invalid_extrinsic: Vec, - /// Storage witness needed for verifying this proof. - pub storage_proof: StorageProof, -} - -/// Represents an invalid transaction proof. -#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] -pub struct ImproperTransactionSortitionProof { - /// The id of the domain this fraud proof targeted - pub domain_id: DomainId, - /// Hash of the bad receipt this fraud proof targeted - pub bad_receipt_hash: ReceiptHash, -} - -/// Represents an invalid block fees proof. -#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] -pub struct InvalidBlockFeesProof { - /// The id of the domain this fraud proof targeted - pub domain_id: DomainId, - /// Hash of the bad receipt this fraud proof targeted - pub bad_receipt_hash: ReceiptHash, - /// Storage witness needed for verifying this proof. - pub storage_proof: StorageProof, -} - -/// Represents an invalid transfers proof. -#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] -pub struct InvalidTransfersProof { - /// The id of the domain this fraud proof targeted - pub domain_id: DomainId, - /// Hash of the bad receipt this fraud proof targeted - pub bad_receipt_hash: ReceiptHash, - /// Storage witness needed for verifying this proof. - pub storage_proof: StorageProof, -} - -/// Represents an invalid domain block hash fraud proof. -#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] -pub struct InvalidDomainBlockHashProof { - /// The id of the domain this fraud proof targeted - pub domain_id: DomainId, - /// Hash of the bad receipt this fraud proof targeted - pub bad_receipt_hash: ReceiptHash, - /// Digests storage proof that is used to derive Domain block hash. - pub digest_storage_proof: StorageProof, -} - /// Represents a valid bundle index and all the extrinsics within that bundle. #[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] pub struct ValidBundleDigest { @@ -752,41 +667,3 @@ pub struct ValidBundleDigest { ExtrinsicDigest, )>, } - -/// Represents an Invalid domain extrinsics root proof with necessary info for verification. -#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] -pub struct InvalidExtrinsicsRootProof { - /// The id of the domain this fraud proof targeted - pub domain_id: DomainId, - /// Hash of the bad receipt this fraud proof targeted - pub bad_receipt_hash: ReceiptHash, - /// Valid Bundle digests - pub valid_bundle_digests: Vec, -} - -impl InvalidBlockFeesProof { - pub(crate) fn domain_id(&self) -> DomainId { - self.domain_id - } - - pub(crate) fn bad_receipt_hash(&self) -> ReceiptHash { - self.bad_receipt_hash - } -} - -/// 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: ReceiptHash, - /// 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 { - frame_support::storage::storage_prefix("System".as_ref(), "Digest".as_ref()).to_vec() -} diff --git a/crates/sp-domains-fraud-proof/src/lib.rs b/crates/sp-domains-fraud-proof/src/lib.rs index a582c076f5..e4ab8ee9ee 100644 --- a/crates/sp-domains-fraud-proof/src/lib.rs +++ b/crates/sp-domains-fraud-proof/src/lib.rs @@ -18,8 +18,6 @@ #![cfg_attr(not(feature = "std"), no_std)] #![feature(associated_type_defaults)] -#[cfg(feature = "std")] -pub mod bundle_equivocation; #[cfg(feature = "std")] pub mod execution_prover; pub mod fraud_proof; @@ -31,12 +29,12 @@ pub mod storage_proof; pub mod test_ethereum_tx; #[cfg(test)] mod tests; -pub mod verification; +pub mod verification_v2; #[cfg(not(feature = "std"))] extern crate alloc; -use crate::fraud_proof::FraudProof; +use crate::fraud_proof::FraudProofV2; use crate::storage_proof::FraudProofStorageKeyRequest; #[cfg(not(feature = "std"))] use alloc::vec::Vec; @@ -398,15 +396,10 @@ impl PassBy for StatelessDomainRuntimeCall { sp_api::decl_runtime_apis! { /// API necessary for fraud proof. + #[api_version(2)] pub trait FraudProofApi { /// Submit the fraud proof via an unsigned extrinsic. - fn submit_fraud_proof_unsigned(fraud_proof: FraudProof, Block::Hash, DomainHeader>); - - /// Extract the fraud proof handled successfully from the given extrinsics. - fn extract_fraud_proofs( - domain_id: DomainId, - extrinsics: Vec, - ) -> Vec, Block::Hash, DomainHeader>>; + fn submit_fraud_proof_unsigned(fraud_proof: FraudProofV2, Block::Hash, DomainHeader, H256>); /// Reture the storage key used in fraud proof fn fraud_proof_storage_key(req: FraudProofStorageKeyRequest) -> Vec; diff --git a/crates/sp-domains-fraud-proof/src/runtime_interface.rs b/crates/sp-domains-fraud-proof/src/runtime_interface.rs index e925d69e40..2143ac2efe 100644 --- a/crates/sp-domains-fraud-proof/src/runtime_interface.rs +++ b/crates/sp-domains-fraud-proof/src/runtime_interface.rs @@ -46,7 +46,7 @@ pub trait FraudProofRuntimeInterface { } /// Derive the bundle digest for the given bundle body. - #[version(2, register_only)] + #[version(2)] fn derive_bundle_digest( &mut self, domain_runtime_code: Vec, @@ -103,7 +103,7 @@ pub trait FraudProofRuntimeInterface { ) } - #[version(1, register_only)] + #[version(1)] fn check_extrinsics_in_single_context( &mut self, domain_runtime_code: Vec, @@ -123,7 +123,7 @@ pub trait FraudProofRuntimeInterface { ) } - #[version(1, register_only)] + #[version(1)] fn construct_domain_inherent_extrinsic( &mut self, domain_runtime_code: Vec, @@ -137,7 +137,7 @@ pub trait FraudProofRuntimeInterface { ) } - #[version(1, register_only)] + #[version(1)] fn domain_storage_key( &mut self, domain_runtime_code: Vec, @@ -148,7 +148,7 @@ pub trait FraudProofRuntimeInterface { .domain_storage_key(domain_runtime_code, req) } - #[version(1, register_only)] + #[version(1)] fn domain_runtime_call( &mut self, domain_runtime_code: Vec, diff --git a/crates/sp-domains-fraud-proof/src/storage_proof.rs b/crates/sp-domains-fraud-proof/src/storage_proof.rs index f520728804..b5d1c2c14c 100644 --- a/crates/sp-domains-fraud-proof/src/storage_proof.rs +++ b/crates/sp-domains-fraud-proof/src/storage_proof.rs @@ -1,3 +1,4 @@ +use crate::DomainInherentExtrinsicData; use codec::{Decode, Encode}; use frame_support::PalletError; use scale_info::TypeInfo; @@ -12,6 +13,7 @@ use sp_domains::{ use sp_runtime::generic::Digest; use sp_runtime::traits::{Block as BlockT, HashingFor, Header as HeaderT, NumberFor}; use sp_std::vec::Vec; +use core::fmt; use sp_trie::StorageProof; use subspace_core_primitives::Randomness; use subspace_runtime_primitives::{Balance, BlockTransactionByteFee, Moment}; @@ -42,14 +44,6 @@ impl From for VerificationError { } } -#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] -pub struct DomainInherentExtrinsicData { - pub timestamp: Moment, - pub maybe_domain_runtime_upgrade: Option>, - pub consensus_transaction_byte_fee: Balance, - pub domain_chain_allowlist: DomainAllowlistUpdates, -} - #[derive(Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo)] pub enum FraudProofStorageKeyRequest { BlockRandomness, diff --git a/crates/sp-domains-fraud-proof/src/verification.rs b/crates/sp-domains-fraud-proof/src/verification_v2.rs similarity index 50% rename from crates/sp-domains-fraud-proof/src/verification.rs rename to crates/sp-domains-fraud-proof/src/verification_v2.rs index 87204f3f40..2afbf6e483 100644 --- a/crates/sp-domains-fraud-proof/src/verification.rs +++ b/crates/sp-domains-fraud-proof/src/verification_v2.rs @@ -2,14 +2,13 @@ extern crate alloc; use crate::fraud_proof::{ - InvalidBundlesFraudProof, InvalidExtrinsicsRootProof, InvalidStateTransitionProof, - InvalidTransfersProof, ValidBundleProof, VerificationError, + InvalidBundlesProofData, InvalidBundlesProofV2, InvalidExtrinsicsRootProofV2, + InvalidStateTransitionProofV2, ValidBundleProofV2, VerificationError, }; -use crate::fraud_proof_runtime_interface::get_fraud_proof_verification_info; +use crate::storage_proof::*; use crate::{ - fraud_proof_runtime_interface, DomainChainAllowlistUpdateExtrinsic, - FraudProofVerificationInfoRequest, FraudProofVerificationInfoResponse, SetCodeExtrinsic, - StorageKeyRequest, + fraud_proof_runtime_interface, DomainInherentExtrinsic, DomainStorageKeyRequest, + StatelessDomainRuntimeCall, }; #[cfg(not(feature = "std"))] use alloc::vec::Vec; @@ -18,26 +17,31 @@ use domain_runtime_primitives::BlockNumber; use hash_db::Hasher; use sp_core::storage::StorageKey; use sp_core::H256; -use sp_domains::bundle_producer_election::{check_proof_of_election, ProofOfElectionError}; use sp_domains::extrinsics::{deduplicate_and_shuffle_extrinsics, extrinsics_shuffling_seed}; use sp_domains::proof_provider_and_verifier::StorageProofVerifier; use sp_domains::valued_trie::valued_ordered_trie_root; use sp_domains::{ - BlockFees, BundleValidity, ExecutionReceipt, ExtrinsicDigest, HeaderHashFor, HeaderHashingFor, - HeaderNumberFor, InboxedBundle, InvalidBundleType, OperatorPublicKey, SealedBundleHeader, - Transfers, + BlockFees, BundleValidity, DomainId, ExecutionReceipt, ExtrinsicDigest, HeaderHashFor, + HeaderHashingFor, HeaderNumberFor, InboxedBundle, InvalidBundleType, RuntimeId, Transfers, + INITIAL_DOMAIN_TX_RANGE, }; use sp_runtime::generic::Digest; use sp_runtime::traits::{ Block as BlockT, Hash, Header as HeaderT, NumberFor, UniqueSaturatedInto, }; -use sp_runtime::{OpaqueExtrinsic, RuntimeAppPublic, SaturatedConversion}; +use sp_runtime::{OpaqueExtrinsic, SaturatedConversion}; use sp_trie::{LayoutV1, StorageProof}; -use subspace_core_primitives::Randomness; +use subspace_core_primitives::{Randomness, U256}; use trie_db::node::Value; /// Verifies invalid domain extrinsic root fraud proof. -pub fn verify_invalid_domain_extrinsics_root_fraud_proof( +pub fn verify_invalid_domain_extrinsics_root_fraud_proof< + CBlock, + Balance, + DomainHeader, + Hashing, + SKP, +>( bad_receipt: ExecutionReceipt< NumberFor, CBlock::Hash, @@ -45,55 +49,45 @@ pub fn verify_invalid_domain_extrinsics_root_fraud_proof, Balance, >, - fraud_proof: &InvalidExtrinsicsRootProof>, + fraud_proof: &InvalidExtrinsicsRootProofV2, + domain_id: DomainId, + runtime_id: RuntimeId, + state_root: CBlock::Hash, + domain_runtime_code: Vec, ) -> Result<(), VerificationError> where CBlock: BlockT, - Hashing: Hasher, DomainHeader: HeaderT, DomainHeader::Hash: Into + PartialEq + Copy, + Hashing: Hasher, + SKP: FraudProofStorageKeyProvider, { - let InvalidExtrinsicsRootProof { + let InvalidExtrinsicsRootProofV2 { valid_bundle_digests, - domain_id, + block_randomness_proof, + domain_inherent_extrinsic_data_proof, .. } = fraud_proof; - let consensus_block_hash = bad_receipt.consensus_block_hash; - let block_randomness = get_fraud_proof_verification_info( - H256::from_slice(consensus_block_hash.as_ref()), - FraudProofVerificationInfoRequest::BlockRandomness, - ) - .and_then(|resp| resp.into_block_randomness()) - .ok_or(VerificationError::FailedToGetBlockRandomness)?; - - let domain_timestamp_extrinsic = get_fraud_proof_verification_info( - H256::from_slice(consensus_block_hash.as_ref()), - FraudProofVerificationInfoRequest::DomainTimestampExtrinsic(*domain_id), - ) - .and_then(|resp| resp.into_domain_timestamp_extrinsic()) - .ok_or(VerificationError::FailedToDeriveDomainTimestampExtrinsic)?; - - let maybe_domain_set_code_extrinsic = get_fraud_proof_verification_info( - H256::from_slice(consensus_block_hash.as_ref()), - FraudProofVerificationInfoRequest::DomainSetCodeExtrinsic(*domain_id), - ) - .map(|resp| resp.into_domain_set_code_extrinsic()) - .ok_or(VerificationError::FailedToDeriveDomainSetCodeExtrinsic)?; + let domain_inherent_extrinsic_data = domain_inherent_extrinsic_data_proof + .verify::(domain_id, runtime_id, &state_root)?; - let consensus_chain_byte_fee_extrinsic = get_fraud_proof_verification_info( - H256::from_slice(consensus_block_hash.as_ref()), - FraudProofVerificationInfoRequest::ConsensusChainByteFeeExtrinsic(*domain_id), - ) - .and_then(|resp| resp.into_consensus_chain_byte_fee_extrinsic()) - .ok_or(VerificationError::FailedToDeriveConsensusChainByteFeeExtrinsic)?; + let block_randomness = >::verify::( + block_randomness_proof.clone(), + (), + &state_root, + )?; - let domain_chain_allowlist_extrinsic = get_fraud_proof_verification_info( - H256::from_slice(consensus_block_hash.as_ref()), - FraudProofVerificationInfoRequest::DomainChainsAllowlistUpdateExtrinsic(*domain_id), + let DomainInherentExtrinsic { + domain_timestamp_extrinsic, + maybe_domain_chain_allowlist_extrinsic, + consensus_chain_byte_fee_extrinsic, + maybe_domain_set_code_extrinsic, + } = fraud_proof_runtime_interface::construct_domain_inherent_extrinsic( + domain_runtime_code, + domain_inherent_extrinsic_data, ) - .map(|resp| resp.into_domain_chain_allowlist_update_extrinsic()) - .ok_or(VerificationError::FailedToDeriveDomainChainAllowlistExtrinsic)?; + .ok_or(VerificationError::FailedToDeriveDomainInherentExtrinsic)?; let bad_receipt_valid_bundle_digests = bad_receipt.valid_bundle_digests(); if valid_bundle_digests.len() != bad_receipt_valid_bundle_digests.len() { @@ -139,18 +133,14 @@ where >(consensus_chain_byte_fee_extrinsic); ordered_extrinsics.push_front(transaction_byte_fee_extrinsic); - if let DomainChainAllowlistUpdateExtrinsic::EncodedExtrinsic(domain_chain_allowlist_extrinsic) = - domain_chain_allowlist_extrinsic - { + if let Some(domain_chain_allowlist_extrinsic) = maybe_domain_chain_allowlist_extrinsic { let domain_set_code_extrinsic = ExtrinsicDigest::new::< LayoutV1>, >(domain_chain_allowlist_extrinsic); ordered_extrinsics.push_front(domain_set_code_extrinsic); } - if let SetCodeExtrinsic::EncodedExtrinsic(domain_set_code_extrinsic) = - maybe_domain_set_code_extrinsic - { + if let Some(domain_set_code_extrinsic) = maybe_domain_set_code_extrinsic { let domain_set_code_extrinsic = ExtrinsicDigest::new::< LayoutV1>, >(domain_set_code_extrinsic); @@ -181,41 +171,40 @@ where } /// Verifies valid bundle fraud proof. -pub fn verify_valid_bundle_fraud_proof( +pub fn verify_valid_bundle_fraud_proof( bad_receipt: ExecutionReceipt< NumberFor, CBlock::Hash, - DomainNumber, - DomainHash, + HeaderNumberFor, + HeaderHashFor, Balance, >, - fraud_proof: &ValidBundleProof, -) -> Result<(), VerificationError> + fraud_proof: &ValidBundleProofV2, CBlock::Hash, DomainHeader>, + domain_id: DomainId, + state_root: CBlock::Hash, + domain_runtime_code: Vec, +) -> Result<(), VerificationError> where CBlock: BlockT, CBlock::Hash: Into, - DomainHash: Copy + Into, + DomainHeader: HeaderT, + DomainHeader::Hash: Into + PartialEq + Copy, + SKP: FraudProofStorageKeyProvider, { - let ValidBundleProof { - domain_id, - bundle_index, - .. + let ValidBundleProofV2 { + bundle_with_proof, .. } = fraud_proof; - let bundle_body = 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)?; + bundle_with_proof.verify::(domain_id, &state_root)?; + let OpaqueBundleWithProof { + bundle, + bundle_index, + .. + } = bundle_with_proof; let valid_bundle_digest = fraud_proof_runtime_interface::derive_bundle_digest( - bad_receipt.consensus_block_hash.into(), - *domain_id, - bundle_body, + domain_runtime_code, + bundle.extrinsics.clone(), ) .ok_or(VerificationError::FailedToDeriveBundleDigest)?; @@ -246,7 +235,8 @@ pub fn verify_invalid_state_transition_fraud_proof, - fraud_proof: &InvalidStateTransitionProof>, + fraud_proof: &InvalidStateTransitionProofV2, + domain_runtime_code: Vec, ) -> Result<(), VerificationError> where CBlock: BlockT, @@ -255,20 +245,12 @@ where DomainHeader::Hash: Into + From, DomainHeader::Number: UniqueSaturatedInto + From, { - let InvalidStateTransitionProof { - domain_id, - proof, + let InvalidStateTransitionProofV2 { + execution_proof, execution_phase, .. } = fraud_proof; - let domain_runtime_code = fraud_proof_runtime_interface::get_fraud_proof_verification_info( - bad_receipt.consensus_block_hash.into(), - FraudProofVerificationInfoRequest::DomainRuntimeCode(*domain_id), - ) - .and_then(FraudProofVerificationInfoResponse::into_domain_runtime_code) - .ok_or(VerificationError::FailedToGetDomainRuntimeCode)?; - let (pre_state_root, post_state_root) = execution_phase .pre_post_state_root::(&bad_receipt, &bad_receipt_parent)?; @@ -281,7 +263,7 @@ where bad_receipt_parent.domain_block_hash.into(), ), pre_state_root, - proof.encode(), + execution_proof.encode(), execution_phase.execution_method(), call_data.as_ref(), domain_runtime_code, @@ -364,20 +346,24 @@ pub fn verify_invalid_block_fees_fraud_proof< Balance, >, storage_proof: &StorageProof, + domain_runtime_code: Vec, ) -> Result<(), VerificationError> where CBlock: BlockT, Balance: PartialEq + Decode, DomainHashing: Hasher, { - let storage_key = StorageKey(sp_domains::operator_block_fees_final_key()); - let storage_proof = storage_proof.clone(); + let storage_key = fraud_proof_runtime_interface::domain_storage_key( + domain_runtime_code, + DomainStorageKeyRequest::BlockFees, + ) + .ok_or(VerificationError::FailedToGetDomainStorageKey)?; let block_fees = StorageProofVerifier::::get_decoded_value::>( &bad_receipt.final_state_root, - storage_proof, - storage_key, + storage_proof.clone(), + StorageKey(storage_key), ) .map_err(|_| VerificationError::InvalidStorageProof)?; @@ -404,7 +390,8 @@ pub fn verify_invalid_transfers_fraud_proof< DomainHash, Balance, >, - proof: &InvalidTransfersProof, + storage_proof: &StorageProof, + domain_runtime_code: Vec, ) -> Result<(), VerificationError> where CBlock: BlockT, @@ -412,26 +399,15 @@ where Balance: PartialEq + Decode, DomainHashing: Hasher, { - let InvalidTransfersProof { - domain_id, - storage_proof, - .. - } = proof; - - let storage_key = get_fraud_proof_verification_info( - bad_receipt.consensus_block_hash.into(), - FraudProofVerificationInfoRequest::StorageKey { - domain_id: *domain_id, - req: StorageKeyRequest::Transfers, - }, + let storage_key = fraud_proof_runtime_interface::domain_storage_key( + domain_runtime_code, + DomainStorageKeyRequest::Transfers, ) - .and_then(FraudProofVerificationInfoResponse::into_storage_key) - .ok_or(VerificationError::FailedToGetDomainTransfersStorageKey)?; - let storage_proof = storage_proof.clone(); + .ok_or(VerificationError::FailedToGetDomainStorageKey)?; let transfers = StorageProofVerifier::::get_decoded_value::>( &bad_receipt.final_state_root, - storage_proof, + storage_proof.clone(), StorageKey(storage_key), ) .map_err(|_| VerificationError::InvalidStorageProof)?; @@ -455,7 +431,9 @@ fn check_expected_bundle_entry( HeaderHashFor, Balance, >, - invalid_bundle_fraud_proof: &InvalidBundlesFraudProof, + bundle_index: u32, + invalid_bundle_type: InvalidBundleType, + is_true_invalid_fraud_proof: bool, ) -> Result>, VerificationError> where CBlock: BlockT, @@ -463,11 +441,10 @@ where { let targeted_invalid_bundle_entry = bad_receipt .inboxed_bundles - .get(invalid_bundle_fraud_proof.bundle_index as usize) + .get(bundle_index as usize) .ok_or(VerificationError::BundleNotFound)?; - let invalid_bundle_type = invalid_bundle_fraud_proof.invalid_bundle_type.clone(); - let is_expected = if !invalid_bundle_fraud_proof.is_true_invalid_fraud_proof { + let is_expected = if !is_true_invalid_fraud_proof { // `FalseInvalid` // The proof trying to prove `bad_receipt_bundle`'s `invalid_bundle_type` is wrong, // so the proof should contains the same `invalid_bundle_type` @@ -492,7 +469,7 @@ where if !is_expected { return Err(VerificationError::UnexpectedTargetedBundleEntry { - bundle_index: invalid_bundle_fraud_proof.bundle_index, + bundle_index, fraud_proof_invalid_type_of_proof: invalid_bundle_type, targeted_entry_bundle: targeted_invalid_bundle_entry.bundle.clone(), }); @@ -518,7 +495,7 @@ fn get_extrinsic_from_proof( .map_err(|_e| VerificationError::InvalidProof) } -pub fn verify_invalid_bundles_fraud_proof( +pub fn verify_invalid_bundles_fraud_proof( bad_receipt: ExecutionReceipt< NumberFor, CBlock::Hash, @@ -533,280 +510,170 @@ pub fn verify_invalid_bundles_fraud_proof( HeaderHashFor, Balance, >, - invalid_bundles_fraud_proof: &InvalidBundlesFraudProof>, + invalid_bundles_fraud_proof: &InvalidBundlesProofV2< + NumberFor, + ::Hash, + DomainHeader, + >, + domain_id: DomainId, + state_root: CBlock::Hash, + domain_runtime_code: Vec, ) -> Result<(), VerificationError> where CBlock: BlockT, DomainHeader: HeaderT, CBlock::Hash: Into, DomainHeader::Hash: Into, + SKP: FraudProofStorageKeyProvider, { + let InvalidBundlesProofV2 { + bundle_index, + invalid_bundle_type, + is_true_invalid_fraud_proof, + proof_data, + .. + } = invalid_bundles_fraud_proof; + let (bundle_index, is_true_invalid_fraud_proof) = (*bundle_index, *is_true_invalid_fraud_proof); + let invalid_bundle_entry = check_expected_bundle_entry::( &bad_receipt, - invalid_bundles_fraud_proof, + bundle_index, + invalid_bundle_type.clone(), + is_true_invalid_fraud_proof, )?; - match &invalid_bundles_fraud_proof.invalid_bundle_type { + match &invalid_bundle_type { InvalidBundleType::OutOfRangeTx(extrinsic_index) => { - let extrinsic = get_extrinsic_from_proof::( - *extrinsic_index, - invalid_bundle_entry.extrinsics_root, - invalid_bundles_fraud_proof.proof_data.clone(), - )?; - let is_tx_in_range = get_fraud_proof_verification_info( - H256::from_slice(bad_receipt.consensus_block_hash.as_ref()), - FraudProofVerificationInfoRequest::TxRangeCheck { - domain_id: invalid_bundles_fraud_proof.domain_id, - bundle_index: invalid_bundles_fraud_proof.bundle_index, - opaque_extrinsic: extrinsic, + let bundle = match proof_data { + InvalidBundlesProofData::Bundle(bundle_with_proof) + if bundle_with_proof.bundle_index == bundle_index => + { + bundle_with_proof.verify::(domain_id, &state_root)?; + bundle_with_proof.bundle.clone() + } + _ => return Err(VerificationError::UnexpectedInvalidBundleProofData), + }; + + let opaque_extrinsic = bundle + .extrinsics + .get(*extrinsic_index as usize) + .cloned() + .ok_or(VerificationError::ExtrinsicNotFound)?; + + let domain_tx_range = U256::MAX / INITIAL_DOMAIN_TX_RANGE; + let bundle_vrf_hash = + U256::from_be_bytes(bundle.sealed_header.header.proof_of_election.vrf_hash()); + + let is_tx_in_range = fraud_proof_runtime_interface::domain_runtime_call( + domain_runtime_code, + StatelessDomainRuntimeCall::IsTxInRange { + opaque_extrinsic, + domain_tx_range, + bundle_vrf_hash, }, ) - .and_then(FraudProofVerificationInfoResponse::into_tx_range_check) - .ok_or(VerificationError::FailedToGetResponseFromTxRangeHostFn)?; + .ok_or(VerificationError::FailedToGetDomainRuntimeCallResponse)?; // If it is true invalid fraud proof then tx must not be in range and // if it is false invalid fraud proof then tx must be in range for fraud // proof to be considered valid. - if is_tx_in_range == invalid_bundles_fraud_proof.is_true_invalid_fraud_proof { + if is_tx_in_range == is_true_invalid_fraud_proof { return Err(VerificationError::InvalidProof); } Ok(()) } InvalidBundleType::InherentExtrinsic(extrinsic_index) => { - let extrinsic = get_extrinsic_from_proof::( - *extrinsic_index, - invalid_bundle_entry.extrinsics_root, - invalid_bundles_fraud_proof.proof_data.clone(), - )?; - let is_inherent = get_fraud_proof_verification_info( - H256::from_slice(bad_receipt.consensus_block_hash.as_ref()), - FraudProofVerificationInfoRequest::InherentExtrinsicCheck { - domain_id: invalid_bundles_fraud_proof.domain_id, - opaque_extrinsic: extrinsic, - }, + let opaque_extrinsic = { + let extrinsic_storage_proof = match proof_data { + InvalidBundlesProofData::Extrinsic(p) => p.clone(), + _ => return Err(VerificationError::UnexpectedInvalidBundleProofData), + }; + get_extrinsic_from_proof::( + *extrinsic_index, + invalid_bundle_entry.extrinsics_root, + extrinsic_storage_proof, + )? + }; + let is_inherent = fraud_proof_runtime_interface::domain_runtime_call( + domain_runtime_code, + StatelessDomainRuntimeCall::IsInherentExtrinsic(opaque_extrinsic), ) - .and_then(FraudProofVerificationInfoResponse::into_inherent_extrinsic_check) - .ok_or(VerificationError::FailedToCheckInherentExtrinsic)?; + .ok_or(VerificationError::FailedToGetDomainRuntimeCallResponse)?; // Proof to be considered valid only, // If it is true invalid fraud proof then extrinsic must be an inherent and // If it is false invalid fraud proof then extrinsic must not be an inherent - if is_inherent == invalid_bundles_fraud_proof.is_true_invalid_fraud_proof { + if is_inherent == is_true_invalid_fraud_proof { Ok(()) } else { Err(VerificationError::InvalidProof) } } InvalidBundleType::IllegalTx(extrinsic_index) => { - let mut bundle_body = get_fraud_proof_verification_info( - bad_receipt.consensus_block_hash.into(), - FraudProofVerificationInfoRequest::DomainBundleBody { - domain_id: invalid_bundles_fraud_proof.domain_id, - bundle_index: invalid_bundles_fraud_proof.bundle_index, - }, - ) - .and_then(FraudProofVerificationInfoResponse::into_bundle_body) - .ok_or(VerificationError::FailedToGetDomainBundleBody)?; + let (mut bundle, execution_proof) = match proof_data { + InvalidBundlesProofData::BundleAndExecution { + bundle_with_proof, + execution_proof, + } if bundle_with_proof.bundle_index == bundle_index => { + bundle_with_proof.verify::(domain_id, &state_root)?; + (bundle_with_proof.bundle.clone(), execution_proof.clone()) + } + _ => return Err(VerificationError::UnexpectedInvalidBundleProofData), + }; - let extrinsics = bundle_body + let extrinsics = bundle + .extrinsics .drain(..) .take((*extrinsic_index + 1) as usize) .collect(); // Make host call for check extrinsic in single context - let check_extrinsic_result = get_fraud_proof_verification_info( - bad_receipt.consensus_block_hash.into(), - FraudProofVerificationInfoRequest::CheckExtrinsicsInSingleContext { - domain_id: invalid_bundles_fraud_proof.domain_id, - domain_block_number: bad_receipt_parent.domain_block_number.saturated_into(), - domain_block_hash: bad_receipt_parent.domain_block_hash.into(), - domain_block_state_root: bad_receipt_parent.final_state_root.into(), + let check_extrinsic_result = + fraud_proof_runtime_interface::check_extrinsics_in_single_context( + domain_runtime_code, + ( + bad_receipt_parent.domain_block_number.saturated_into(), + bad_receipt_parent.domain_block_hash.into(), + ), + bad_receipt_parent.final_state_root.into(), extrinsics, - storage_proof: invalid_bundles_fraud_proof.proof_data.clone(), - }, - ) - .and_then(FraudProofVerificationInfoResponse::into_single_context_extrinsic_check) - .ok_or(VerificationError::FailedToCheckExtrinsicsInSingleContext)?; + execution_proof.encode(), + ) + .ok_or(VerificationError::FailedToCheckExtrinsicsInSingleContext)?; let is_extrinsic_invalid = check_extrinsic_result == Some(*extrinsic_index); // Proof to be considered valid only, // If it is true invalid fraud proof then extrinsic must be an invalid extrinsic and // If it is false invalid fraud proof then extrinsic must not be an invalid extrinsic - if is_extrinsic_invalid == invalid_bundles_fraud_proof.is_true_invalid_fraud_proof { + if is_extrinsic_invalid == is_true_invalid_fraud_proof { Ok(()) } else { Err(VerificationError::InvalidProof) } } InvalidBundleType::UndecodableTx(extrinsic_index) => { - let extrinsic = get_extrinsic_from_proof::( - *extrinsic_index, - invalid_bundle_entry.extrinsics_root, - invalid_bundles_fraud_proof.proof_data.clone(), - )?; - let is_decodable = get_fraud_proof_verification_info( - H256::from_slice(bad_receipt.consensus_block_hash.as_ref()), - FraudProofVerificationInfoRequest::ExtrinsicDecodableCheck { - domain_id: invalid_bundles_fraud_proof.domain_id, - opaque_extrinsic: extrinsic, - }, + let opaque_extrinsic = { + let extrinsic_storage_proof = match proof_data { + InvalidBundlesProofData::Extrinsic(p) => p.clone(), + _ => return Err(VerificationError::UnexpectedInvalidBundleProofData), + }; + get_extrinsic_from_proof::( + *extrinsic_index, + invalid_bundle_entry.extrinsics_root, + extrinsic_storage_proof, + )? + }; + let is_decodable = fraud_proof_runtime_interface::domain_runtime_call( + domain_runtime_code, + StatelessDomainRuntimeCall::IsDecodableExtrinsic(opaque_extrinsic), ) - .and_then(FraudProofVerificationInfoResponse::into_extrinsic_decodable_check) - .ok_or(VerificationError::FailedToCheckExtrinsicDecodable)?; + .ok_or(VerificationError::FailedToGetDomainRuntimeCallResponse)?; - if is_decodable == invalid_bundles_fraud_proof.is_true_invalid_fraud_proof { + if is_decodable == is_true_invalid_fraud_proof { return Err(VerificationError::InvalidProof); } Ok(()) } - InvalidBundleType::InvalidXDM(extrinsic_index) => { - let extrinsic = get_extrinsic_from_proof::( - *extrinsic_index, - invalid_bundle_entry.extrinsics_root, - invalid_bundles_fraud_proof.proof_data.clone(), - )?; - - let maybe_is_valid_xdm = get_fraud_proof_verification_info( - H256::from_slice(bad_receipt.consensus_block_hash.as_ref()), - FraudProofVerificationInfoRequest::XDMValidationCheck { - domain_id: invalid_bundles_fraud_proof.domain_id, - opaque_extrinsic: extrinsic, - }, - ) - .and_then(FraudProofVerificationInfoResponse::into_xdm_validation_check); - - if let Some(is_valid_xdm) = maybe_is_valid_xdm { - // Proof to be considered valid only, - // If it is true invalid fraud proof then extrinsic must be an invalid xdm and - // If it is false invalid fraud proof then extrinsic must be a valid xdm - if is_valid_xdm != invalid_bundles_fraud_proof.is_true_invalid_fraud_proof { - Ok(()) - } else { - Err(VerificationError::InvalidProof) - } - } else { - // If this extrinsic is not an XDM, - // If it is false invalid, then bad receipt marked this extrinsic as InvalidXDM - // even though it is not an XDM, if so accept the fraud proof - if !invalid_bundles_fraud_proof.is_true_invalid_fraud_proof { - Ok(()) - } else { - // If this is a true invalid but the extrinsic is not an XDM, then reject fraud proof. - // this can happen if there is a bug in the challenger node implementation. - Err(VerificationError::InvalidProof) - } - } - } } } - -/// Represents error for invalid bundle equivocation proof. -#[derive(Debug)] -#[cfg_attr(feature = "thiserror", derive(thiserror::Error))] -pub enum InvalidBundleEquivocationError { - /// Bundle signature is invalid. - #[cfg_attr(feature = "thiserror", error("Invalid bundle signature."))] - BadBundleSignature, - /// Bundle slot mismatch. - #[cfg_attr(feature = "thiserror", error("Bundle slot mismatch."))] - BundleSlotMismatch, - /// Same bundle hash. - #[cfg_attr(feature = "thiserror", error("Same bundle hash."))] - SameBundleHash, - /// Invalid Proof of election. - #[cfg_attr(feature = "thiserror", error("Invalid Proof of Election: {0:?}"))] - InvalidProofOfElection(ProofOfElectionError), - /// Failed to get domain total stake. - #[cfg_attr(feature = "thiserror", error("Failed to get domain total stake."))] - FailedToGetDomainTotalStake, - /// Failed to get operator stake. - #[cfg_attr(feature = "thiserror", error("Failed to get operator stake"))] - FailedToGetOperatorStake, - /// Mismatched operatorId and Domain. - #[cfg_attr(feature = "thiserror", error("Mismatched operatorId and Domain."))] - MismatchedOperatorAndDomain, -} - -/// Verifies Bundle equivocation fraud proof. -pub fn verify_bundle_equivocation_fraud_proof( - operator_signing_key: &OperatorPublicKey, - header_1: &SealedBundleHeader, CBlock::Hash, DomainHeader, Balance>, - header_2: &SealedBundleHeader, CBlock::Hash, DomainHeader, Balance>, -) -> Result<(), InvalidBundleEquivocationError> -where - CBlock: BlockT, - DomainHeader: HeaderT, - Balance: Encode, -{ - if !operator_signing_key.verify(&header_1.pre_hash(), &header_1.signature) { - return Err(InvalidBundleEquivocationError::BadBundleSignature); - } - - if !operator_signing_key.verify(&header_2.pre_hash(), &header_2.signature) { - return Err(InvalidBundleEquivocationError::BadBundleSignature); - } - - let operator_set_1 = ( - header_1.header.proof_of_election.operator_id, - header_1.header.proof_of_election.domain_id, - ); - let operator_set_2 = ( - header_2.header.proof_of_election.operator_id, - header_2.header.proof_of_election.domain_id, - ); - - // Operator and the domain the proof of election targeted should be same - if operator_set_1 != operator_set_2 { - return Err(InvalidBundleEquivocationError::MismatchedOperatorAndDomain); - } - - let consensus_block_hash = header_1.header.proof_of_election.consensus_block_hash; - let domain_id = header_1.header.proof_of_election.domain_id; - let operator_id = header_1.header.proof_of_election.operator_id; - - let (domain_total_stake, bundle_slot_probability) = get_fraud_proof_verification_info( - H256::from_slice(consensus_block_hash.as_ref()), - FraudProofVerificationInfoRequest::DomainElectionParams { domain_id }, - ) - .and_then(|resp| resp.into_domain_election_params()) - .ok_or(InvalidBundleEquivocationError::FailedToGetDomainTotalStake)?; - - let operator_stake = get_fraud_proof_verification_info( - H256::from_slice(consensus_block_hash.as_ref()), - FraudProofVerificationInfoRequest::OperatorStake { operator_id }, - ) - .and_then(|resp| resp.into_operator_stake()) - .ok_or(InvalidBundleEquivocationError::FailedToGetOperatorStake)? - .saturated_into(); - - check_proof_of_election( - operator_signing_key, - bundle_slot_probability, - &header_1.header.proof_of_election, - operator_stake, - domain_total_stake, - ) - .map_err(InvalidBundleEquivocationError::InvalidProofOfElection)?; - - check_proof_of_election( - operator_signing_key, - bundle_slot_probability, - &header_2.header.proof_of_election, - operator_stake, - domain_total_stake, - ) - .map_err(InvalidBundleEquivocationError::InvalidProofOfElection)?; - - if header_1.header.proof_of_election.slot_number - != header_2.header.proof_of_election.slot_number - { - return Err(InvalidBundleEquivocationError::BundleSlotMismatch); - } - - if header_1.hash() == header_2.hash() { - return Err(InvalidBundleEquivocationError::SameBundleHash); - } - - Ok(()) -} diff --git a/crates/sp-domains/src/lib.rs b/crates/sp-domains/src/lib.rs index f92b7d3464..11d293c055 100644 --- a/crates/sp-domains/src/lib.rs +++ b/crates/sp-domains/src/lib.rs @@ -713,6 +713,9 @@ pub struct ProofOfElection { pub vrf_signature: VrfSignature, /// Operator index in the OperatorRegistry. pub operator_id: OperatorId, + /// TODO: remove this field before the next network since it is only used + /// in the bundle equivocation fraud proof and which is deprecated. + /// /// Consensus block hash at which proof of election was derived. pub consensus_block_hash: CHash, } @@ -1054,8 +1057,6 @@ pub enum InvalidBundleType { OutOfRangeTx(u32), /// Transaction is illegal (unable to pay the fee, etc). IllegalTx(u32), - /// Transaction is an invalid XDM - InvalidXDM(u32), /// Transaction is an inherent extrinsic. InherentExtrinsic(u32), } @@ -1069,8 +1070,7 @@ impl InvalidBundleType { Self::UndecodableTx(_) => 1, Self::OutOfRangeTx(_) => 2, Self::InherentExtrinsic(_) => 3, - Self::InvalidXDM(_) => 4, - Self::IllegalTx(_) => 5, + Self::IllegalTx(_) => 4, } } @@ -1079,7 +1079,6 @@ impl InvalidBundleType { Self::UndecodableTx(i) => *i, Self::OutOfRangeTx(i) => *i, Self::IllegalTx(i) => *i, - Self::InvalidXDM(i) => *i, Self::InherentExtrinsic(i) => *i, } } @@ -1316,7 +1315,7 @@ pub fn operator_block_fees_final_key() -> Vec { sp_api::decl_runtime_apis! { /// API necessary for domains pallet. - #[api_version(3)] + #[api_version(4)] pub trait DomainsApi { /// Submits the transaction bundle via an unsigned extrinsic. fn submit_bundle_unsigned(opaque_bundle: OpaqueBundle, Block::Hash, DomainHeader, Balance>); @@ -1399,6 +1398,9 @@ sp_api::decl_runtime_apis! { /// Return the balance of the storage fund account fn storage_fund_account_balance(operator_id: OperatorId) -> Balance; + + /// Return if the domain runtime code is upgraded since `at` + fn is_domain_runtime_updraded_since(domain_id: DomainId, at: NumberFor) -> Option; } pub trait BundleProducerElectionApi { diff --git a/crates/sp-subspace-mmr/src/lib.rs b/crates/sp-subspace-mmr/src/lib.rs index 1e91e4f8c6..a0d555d70b 100644 --- a/crates/sp-subspace-mmr/src/lib.rs +++ b/crates/sp-subspace-mmr/src/lib.rs @@ -39,12 +39,18 @@ pub enum MmrLeaf { V0(LeafDataV0), } -impl MmrLeaf { +impl MmrLeaf { pub fn state_root(&self) -> Hash { match self { MmrLeaf::V0(leaf) => leaf.state_root.clone(), } } + + pub fn block_number(&self) -> BlockNumber { + match self { + MmrLeaf::V0(leaf) => leaf.block_number.clone(), + } + } } /// MMR v0 leaf data @@ -116,15 +122,15 @@ impl Clone /// Trait to verify MMR proofs pub trait MmrProofVerifier { /// Returns consensus state root if the given MMR proof is valid - fn verify_proof_and_extract_consensus_state_root( + fn verify_proof_and_extract_leaf( mmr_leaf_proof: ConsensusChainMmrLeafProof, - ) -> Option; + ) -> Option>; } impl MmrProofVerifier for () { - fn verify_proof_and_extract_consensus_state_root( + fn verify_proof_and_extract_leaf( _mmr_leaf_proof: ConsensusChainMmrLeafProof, - ) -> Option { + ) -> Option> { None } } diff --git a/crates/subspace-fake-runtime-api/src/lib.rs b/crates/subspace-fake-runtime-api/src/lib.rs index a05aae7b59..341323697f 100644 --- a/crates/subspace-fake-runtime-api/src/lib.rs +++ b/crates/subspace-fake-runtime-api/src/lib.rs @@ -29,7 +29,7 @@ use sp_domains::{ DomainAllowlistUpdates, DomainId, DomainInstanceData, ExecutionReceiptFor, OpaqueBundle, OperatorId, OperatorPublicKey, }; -use sp_domains_fraud_proof::fraud_proof::FraudProof; +use sp_domains_fraud_proof::fraud_proof::FraudProofV2; use sp_domains_fraud_proof::storage_proof::FraudProofStorageKeyRequest; use sp_messenger::messages::{ BlockMessagesWithStorageKey, ChainId, CrossDomainMessage, MessageId, MessageKey, @@ -300,6 +300,10 @@ sp_api::impl_runtime_apis! { fn storage_fund_account_balance(_operator_id: OperatorId) -> Balance { unreachable!() } + + fn is_domain_runtime_updraded_since(_domain_id: DomainId, _at: NumberFor) -> Option { + unreachable!() + } } impl sp_domains::BundleProducerElectionApi for Runtime { @@ -398,14 +402,7 @@ sp_api::impl_runtime_apis! { } impl sp_domains_fraud_proof::FraudProofApi for Runtime { - fn submit_fraud_proof_unsigned(_fraud_proof: FraudProof, ::Hash, DomainHeader>) { - unreachable!() - } - - fn extract_fraud_proofs( - _domain_id: DomainId, - _extrinsics: Vec<::Extrinsic>, - ) -> Vec, ::Hash, DomainHeader>> { + fn submit_fraud_proof_unsigned(_fraud_proof: FraudProofV2, ::Hash, DomainHeader, H256>) { unreachable!() } diff --git a/crates/subspace-runtime/src/domains.rs b/crates/subspace-runtime/src/domains.rs index 7f5e8443a5..6dbbb0dc43 100644 --- a/crates/subspace-runtime/src/domains.rs +++ b/crates/subspace-runtime/src/domains.rs @@ -6,7 +6,6 @@ use crate::{Balance, Block, Domains, RuntimeCall, UncheckedExtrinsic}; use alloc::vec::Vec; use domain_runtime_primitives::opaque::Header as DomainHeader; use sp_domains::DomainId; -use sp_domains_fraud_proof::fraud_proof::FraudProof; use sp_runtime::traits::{Block as BlockT, NumberFor}; pub(crate) fn extract_successful_bundles( @@ -40,22 +39,3 @@ pub(crate) fn extract_bundle( _ => None, } } - -pub(crate) fn extract_fraud_proofs( - domain_id: DomainId, - extrinsics: Vec, -) -> Vec, ::Hash, DomainHeader>> { - let successful_fraud_proofs = Domains::successful_fraud_proofs(domain_id); - extrinsics - .into_iter() - .filter_map(|uxt| match uxt.function { - RuntimeCall::Domains(pallet_domains::Call::submit_fraud_proof { fraud_proof }) - if fraud_proof.domain_id() == domain_id - && successful_fraud_proofs.contains(&fraud_proof.hash()) => - { - Some(*fraud_proof) - } - _ => None, - }) - .collect() -} diff --git a/crates/subspace-runtime/src/lib.rs b/crates/subspace-runtime/src/lib.rs index 46f861254c..f397010532 100644 --- a/crates/subspace-runtime/src/lib.rs +++ b/crates/subspace-runtime/src/lib.rs @@ -67,7 +67,7 @@ use sp_domains::{ OperatorPublicKey, StakingHoldIdentifier, DOMAIN_STORAGE_FEE_MULTIPLIER, INITIAL_DOMAIN_TX_RANGE, }; -use sp_domains_fraud_proof::fraud_proof::FraudProof; +use sp_domains_fraud_proof::fraud_proof::FraudProofV2; use sp_domains_fraud_proof::storage_proof::{ FraudProofStorageKeyProvider, FraudProofStorageKeyRequest, }; @@ -501,9 +501,9 @@ impl sp_messenger::OnXDMRewards for OnXDMRewards { pub struct MmrProofVerifier; impl sp_subspace_mmr::MmrProofVerifier, Hash> for MmrProofVerifier { - fn verify_proof_and_extract_consensus_state_root( + fn verify_proof_and_extract_leaf( mmr_leaf_proof: ConsensusChainMmrLeafProof, Hash, mmr::Hash>, - ) -> Option { + ) -> Option { let ConsensusChainMmrLeafProof { consensus_block_number, opaque_mmr_leaf, @@ -524,7 +524,7 @@ impl sp_subspace_mmr::MmrProofVerifier, Hash> for Mm let leaf: mmr::Leaf = opaque_mmr_leaf.into_opaque_leaf().try_decode()?; - Some(leaf.state_root()) + Some(leaf) } } @@ -711,6 +711,9 @@ impl pallet_domains::Config for Runtime { type DomainBundleSubmitted = Messenger; type OnDomainInstantiated = Messenger; type Balance = Balance; + type MmrHash = mmr::Hash; + type MmrProofVerifier = MmrProofVerifier; + type FraudProofStorageKeyProvider = StorageKeyProvider; } parameter_types! { @@ -1257,6 +1260,10 @@ impl_runtime_apis! { fn storage_fund_account_balance(operator_id: OperatorId) -> Balance { Domains::storage_fund_account_balance(operator_id) } + + fn is_domain_runtime_updraded_since(domain_id: DomainId, at: NumberFor) -> Option { + Domains::is_domain_runtime_updraded_since(domain_id, at) + } } impl sp_domains::BundleProducerElectionApi for Runtime { @@ -1355,17 +1362,10 @@ impl_runtime_apis! { } impl sp_domains_fraud_proof::FraudProofApi for Runtime { - fn submit_fraud_proof_unsigned(fraud_proof: FraudProof, ::Hash, DomainHeader>) { + fn submit_fraud_proof_unsigned(fraud_proof: FraudProofV2, ::Hash, DomainHeader, H256>) { Domains::submit_fraud_proof_unsigned(fraud_proof) } - fn extract_fraud_proofs( - domain_id: DomainId, - extrinsics: Vec<::Extrinsic>, - ) -> Vec, ::Hash, DomainHeader>> { - crate::domains::extract_fraud_proofs(domain_id, extrinsics) - } - fn fraud_proof_storage_key(req: FraudProofStorageKeyRequest) -> Vec { ::storage_key(req) } diff --git a/crates/subspace-service/src/lib.rs b/crates/subspace-service/src/lib.rs index 0238fd922d..db9c2e9742 100644 --- a/crates/subspace-service/src/lib.rs +++ b/crates/subspace-service/src/lib.rs @@ -51,6 +51,7 @@ use sc_basic_authorship::ProposerFactory; use sc_client_api::execution_extensions::ExtensionsFactory; use sc_client_api::{ AuxStore, Backend, BlockBackend, BlockchainEvents, ExecutorProvider, HeaderBackend, + ProofProvider, }; use sc_consensus::{ BasicQueue, BlockCheckParams, BlockImport, BlockImportParams, DefaultImportQueue, ImportQueue, @@ -249,6 +250,7 @@ where Client: BlockBackend + HeaderBackend + ProvideRuntimeApi + + ProofProvider + Send + Sync + 'static, @@ -566,7 +568,6 @@ where config.prometheus_registry(), &task_manager, client.clone(), - sync_target_block_number.clone(), )?; let verifier = SubspaceVerifier::::new(SubspaceVerifierOptions { @@ -621,6 +622,7 @@ where + BlockIdTo + HeaderBackend + HeaderMetadata + + ProofProvider + 'static, Client::Api: TaggedTransactionQueue + DomainsApi diff --git a/crates/subspace-service/src/transaction_pool.rs b/crates/subspace-service/src/transaction_pool.rs index 14872fe563..a8e5b175b9 100644 --- a/crates/subspace-service/src/transaction_pool.rs +++ b/crates/subspace-service/src/transaction_pool.rs @@ -1,26 +1,22 @@ use async_trait::async_trait; -use futures::future::{Future, FutureExt, Ready}; +use futures::future::{Future, Ready}; use sc_client_api::blockchain::HeaderBackend; use sc_client_api::{AuxStore, BlockBackend, ExecutorProvider, UsageProvider}; use sc_service::{TaskManager, TransactionPoolOptions}; -use sc_transaction_pool::error::{Error as TxPoolError, Result as TxPoolResult}; +use sc_transaction_pool::error::Result as TxPoolResult; use sc_transaction_pool::{ BasicPool, ChainApi, FullChainApi, Pool, RevalidationType, Transaction, ValidatedTransaction, }; use sc_transaction_pool_api::{ ChainEvent, ImportNotificationStream, LocalTransactionPool, MaintainedTransactionPool, - OffchainTransactionPoolFactory, PoolFuture, PoolStatus, ReadyTransactions, TransactionFor, - TransactionPool, TransactionSource, TransactionStatusStreamFor, TxHash, + PoolFuture, PoolStatus, ReadyTransactions, TransactionFor, TransactionPool, TransactionSource, + TransactionStatusStreamFor, TxHash, }; -use sp_api::{ApiExt, ProvideRuntimeApi}; +use sp_api::ProvideRuntimeApi; use sp_blockchain::{HeaderMetadata, TreeRoute}; -use sp_consensus_slots::Slot; -use sp_consensus_subspace::{ChainConstants, FarmerPublicKey, SubspaceApi}; +use sp_consensus_subspace::{FarmerPublicKey, SubspaceApi}; use sp_core::traits::SpawnEssentialNamed; use sp_domains::DomainsApi; -use sp_domains_fraud_proof::bundle_equivocation::check_equivocation; -use sp_domains_fraud_proof::fraud_proof::FraudProof; -use sp_domains_fraud_proof::{FraudProofApi, InvalidTransactionCode}; use sp_runtime::generic::BlockId; use sp_runtime::traits::{Block as BlockT, BlockIdTo, Header as HeaderT, NumberFor}; use sp_runtime::transaction_validity::{TransactionValidity, TransactionValidityError}; @@ -29,12 +25,8 @@ use sp_transaction_pool::runtime_api::TaggedTransactionQueue; use std::collections::HashMap; use std::marker::PhantomData; use std::pin::Pin; -use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; use substrate_prometheus_endpoint::Registry as PrometheusRegistry; -use tokio::sync::mpsc; -use tokio::sync::mpsc::UnboundedSender; -use tracing::log::error; /// Block hash type for a pool. type BlockHash = <::Block as BlockT>::Hash; @@ -62,10 +54,6 @@ pub type BlockExtrinsicOf = ::Extrinsic; pub struct FullChainApiWrapper { inner: Arc>, client: Arc, - sync_target_block_number: Arc, - chain_constants: ChainConstants, - fraud_proof_submit_sink: - UnboundedSender, Block::Hash, DomainHeader>>, marker: PhantomData, } @@ -87,14 +75,7 @@ where client: Arc, prometheus: Option<&PrometheusRegistry>, task_manager: &TaskManager, - sync_target_block_number: Arc, - fraud_proof_submit_sink: UnboundedSender< - FraudProof, Block::Hash, DomainHeader>, - >, ) -> sp_blockchain::Result { - let chain_constants = client - .runtime_api() - .chain_constants(client.info().best_hash)?; Ok(Self { inner: Arc::new(FullChainApi::new( client.clone(), @@ -102,9 +83,6 @@ where &task_manager.spawn_essential_handle(), )), client, - sync_target_block_number, - chain_constants, - fraud_proof_submit_sink, marker: Default::default(), }) } @@ -150,76 +128,10 @@ where source: TransactionSource, uxt: ExtrinsicFor, ) -> Self::ValidationFuture { - let chain_api = self.inner.clone(); - let client = self.client.clone(); - let best_block_number = TryInto::::try_into(client.info().best_number) - .expect("Block number will always fit into u32; qed"); - let diff_in_blocks = self - .sync_target_block_number - .load(Ordering::Relaxed) - .saturating_sub(best_block_number); - let slot_probability = self.chain_constants.slot_probability(); - let fraud_proof_submit_sink = self.fraud_proof_submit_sink.clone(); - async move { - // TODO: after https://github.com/paritytech/polkadot-sdk/issues/3705 is resolved, check if - // there is already a fraud proof with the same tag and higher priority in the tx pool, if so - // drop the incoming fraud proof before validating it. - - let uxt_validity = chain_api - .validate_transaction(at, source, uxt.clone()) - .await?; - - if uxt_validity.is_ok() { - // Transaction is successfully validated. - // If the transaction is `submit_bundle`, then extract the bundle - // and check for equivocation. - let runtime_api = client.runtime_api(); - let maybe_opaque_bundle = runtime_api - .extract_bundle(at, uxt) - .map_err(|err| TxPoolError::RuntimeApi(err.to_string()))?; - if let Some(opaque_bundle) = maybe_opaque_bundle { - let slot = opaque_bundle - .sealed_header - .header - .proof_of_election - .slot_number - .into(); - - let slot_now = if diff_in_blocks > 0 { - slot + Slot::from( - u64::from(diff_in_blocks) * slot_probability.1 / slot_probability.0, - ) - } else { - slot - }; - - let maybe_equivocation_fraud_proof = check_equivocation::<_, Block, _>( - &client, - slot_now, - opaque_bundle.sealed_header, - )?; - - if let Some(equivocation_fraud_proof) = maybe_equivocation_fraud_proof { - let sent_result = fraud_proof_submit_sink.send(equivocation_fraud_proof); - if let Err(err) = sent_result { - error!( - target: "consensus-fraud-proof-sender", - "failed to send fraud proof to be submitted: {err:?}" - ); - } - - return Err(TxPoolError::Pool( - sc_transaction_pool_api::error::Error::InvalidTransaction( - InvalidTransactionCode::BundleEquivocation.into(), - ), - )); - } - } - } - - Ok(uxt_validity) - } - .boxed() + // TODO: after https://github.com/paritytech/polkadot-sdk/issues/3705 is resolved, check if + // there is already a fraud proof with the same tag and higher priority in the tx pool, if so + // drop the incoming fraud proof before validating it. + self.inner.validate_transaction(at, source, uxt.clone()) } fn block_id_to_number( @@ -324,7 +236,6 @@ where + 'static, Client::Api: TaggedTransactionQueue + SubspaceApi - + FraudProofApi + DomainsApi, { type Block = Block; @@ -460,7 +371,6 @@ pub fn new_full( prometheus_registry: Option<&PrometheusRegistry>, task_manager: &TaskManager, client: Arc, - sync_target_block_number: Arc, ) -> sp_blockchain::Result>> where Block: BlockT, @@ -479,16 +389,12 @@ where DomainHeader: HeaderT, Client::Api: TaggedTransactionQueue + SubspaceApi - + FraudProofApi + DomainsApi, { - let (fraud_proof_submit_sink, mut fraud_proof_submit_stream) = mpsc::unbounded_channel(); let pool_api = Arc::new(FullChainApiWrapper::new( client.clone(), prometheus_registry, task_manager, - sync_target_block_number, - fraud_proof_submit_sink, )?); let basic_pool = Arc::new(BasicPoolWrapper::with_revalidation_type( @@ -500,37 +406,5 @@ where client.clone(), )); - let offchain_tx_pool_factory = OffchainTransactionPoolFactory::new(basic_pool.clone()); - - // run a separate task to submit fraud proof since chain api cannot depend on Basic pool since - // Basic pool would require chain api to be instantiated first. - // Ofcourse, there are other approaches that inject this into chain Api but it feels more like - // a hack and prefer to run a separate task that submits the fraud proof when equivocation is detected - task_manager - .spawn_essential_handle() - .spawn_essential_blocking( - "consensus-fraud-proof-submitter", - None, - Box::pin(async move { - loop { - if let Some(fraud_proof) = fraud_proof_submit_stream.recv().await { - let mut runtime_api = client.runtime_api(); - let best_hash = client.info().best_hash; - runtime_api.register_extension( - offchain_tx_pool_factory.offchain_transaction_pool(best_hash), - ); - let result = - runtime_api.submit_fraud_proof_unsigned(best_hash, fraud_proof); - if let Err(err) = result { - error!( - target: "consensus-fraud-proof-submitter", - "failed to submit fraud proof: {err:?}" - ); - } - } - } - }), - ); - Ok(basic_pool) } diff --git a/domains/client/block-preprocessor/src/lib.rs b/domains/client/block-preprocessor/src/lib.rs index 4128e12c44..0c6a857b82 100644 --- a/domains/client/block-preprocessor/src/lib.rs +++ b/domains/client/block-preprocessor/src/lib.rs @@ -18,7 +18,7 @@ use crate::inherents::is_runtime_upgraded; use codec::Encode; use domain_runtime_primitives::opaque::AccountId; use sc_client_api::BlockBackend; -use sp_api::{ApiExt, ProvideRuntimeApi}; +use sp_api::{ProvideRuntimeApi}; use sp_blockchain::HeaderBackend; use sp_core::H256; use sp_domains::core_api::DomainCoreApi; @@ -297,21 +297,6 @@ where )); } - // TODO: remove version check before next network - let messenger_api_version = runtime_api - .api_version::>(at)? - // safe to return default version as 1 since there will always be version 1. - .unwrap_or(1); - - if messenger_api_version >= 2 { - // check if the extrinsic is an XDM and is valid - if let Some(false) = runtime_api.is_xdm_valid(at, extrinsic.encode())? { - return Ok(BundleValidity::Invalid(InvalidBundleType::InvalidXDM( - index as u32, - ))); - } - } - // Using one instance of runtime_api throughout the loop in order to maintain context // between them. // Using `check_extrinsics_and_do_pre_dispatch` instead of `check_transaction_validity` diff --git a/domains/client/block-preprocessor/src/stateless_runtime.rs b/domains/client/block-preprocessor/src/stateless_runtime.rs index 75d826eb53..7e0efb8d98 100644 --- a/domains/client/block-preprocessor/src/stateless_runtime.rs +++ b/domains/client/block-preprocessor/src/stateless_runtime.rs @@ -5,7 +5,7 @@ use sc_client_api::execution_extensions::ExtensionsFactory; use sc_executor::RuntimeVersionOf; use sp_api::{ApiError, Core, RuntimeApiInfo}; use sp_core::traits::{CallContext, CodeExecutor, FetchRuntimeCode, RuntimeCode}; -use sp_core::{Decode, Hasher}; +use sp_core::Hasher; use sp_domains::core_api::DomainCoreApi; use sp_domains::DomainAllowlistUpdates; use sp_messenger::messages::MessageKey; @@ -284,9 +284,24 @@ where } pub fn block_fees_storage_key(&self) -> Result, ApiError> { - let has_runtime_api = sp_io::misc::runtime_version(&self.runtime_code) - .and_then(|v| RuntimeVersion::decode(&mut &v[..]).ok()) - .and_then(|runtime_version| runtime_version.api_version(&>::ID)) + let runtime_version = { + let mut ext = BasicExternalities::new(self.storage.clone()); + let ext_extensions = ext.extensions(); + ext_extensions.merge( + self.extension_factory + .extensions_for(Default::default(), Default::default()), + ); + let runtime_code = self.runtime_code(); + self.executor + .runtime_version(&mut ext, &runtime_code) + .map_err(|err| { + ApiError::Application(Box::from(format!( + "failed to read domain runtime version: {err}" + ))) + })? + }; + let has_runtime_api = runtime_version + .api_version(&>::ID) .map_or(false, |runtime_api_version| runtime_api_version >= 2); if has_runtime_api { diff --git a/domains/client/domain-operator/Cargo.toml b/domains/client/domain-operator/Cargo.toml index 42621fa8c3..a8f596778f 100644 --- a/domains/client/domain-operator/Cargo.toml +++ b/domains/client/domain-operator/Cargo.toml @@ -14,6 +14,7 @@ futures-timer = "3.0.3" parking_lot = "0.12.2" sc-client-api = { git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } sc-consensus = { git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } +sc-domains = { version = "0.1.0", path = "../../../crates/sc-domains" } sc-transaction-pool = { git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } sc-transaction-pool-api = { git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } sc-utils = { git = "https://github.com/subspace/polkadot-sdk", rev = "44d742b90e7852aed1f08ab5299d5d88cfa1c6ed" } diff --git a/domains/client/domain-operator/src/bundle_processor.rs b/domains/client/domain-operator/src/bundle_processor.rs index d65185aea0..3f3b7c18bf 100644 --- a/domains/client/domain-operator/src/bundle_processor.rs +++ b/domains/client/domain-operator/src/bundle_processor.rs @@ -15,6 +15,7 @@ use sp_domains::core_api::DomainCoreApi; use sp_domains::{DomainId, DomainsApi, ReceiptValidity}; use sp_domains_fraud_proof::FraudProofApi; use sp_messenger::MessengerApi; +use sp_mmr_primitives::MmrApi; use sp_runtime::traits::{Block as BlockT, NumberFor, Zero}; use sp_runtime::{Digest, DigestItem}; use sp_weights::constants::WEIGHT_REF_TIME_PER_MILLIS; @@ -148,6 +149,7 @@ where CClient::Api: DomainsApi + MessengerApi + FraudProofApi + + MmrApi> + 'static, Backend: sc_client_api::Backend + 'static, E: CodeExecutor, diff --git a/domains/client/domain-operator/src/domain_block_processor.rs b/domains/client/domain-operator/src/domain_block_processor.rs index 6b558e1d3e..7851919831 100644 --- a/domains/client/domain-operator/src/domain_block_processor.rs +++ b/domains/client/domain-operator/src/domain_block_processor.rs @@ -12,7 +12,7 @@ use sc_consensus::{ StorageChanges, }; use sc_transaction_pool_api::OffchainTransactionPoolFactory; -use sp_api::{ApiExt, ProvideRuntimeApi}; +use sp_api::{ApiError, ApiExt, ProvideRuntimeApi}; use sp_blockchain::{HashAndNumber, HeaderBackend, HeaderMetadata}; use sp_consensus::{BlockOrigin, SyncOracle}; use sp_core::traits::CodeExecutor; @@ -20,13 +20,12 @@ use sp_core::H256; use sp_domains::core_api::DomainCoreApi; use sp_domains::merkle_tree::MerkleTree; use sp_domains::{BundleValidity, DomainId, DomainsApi, ExecutionReceipt, HeaderHashingFor}; -use sp_domains_fraud_proof::fraud_proof::{FraudProof, ValidBundleProof}; +use sp_domains_fraud_proof::fraud_proof::FraudProofV2; use sp_domains_fraud_proof::FraudProofApi; use sp_messenger::MessengerApi; use sp_mmr_primitives::MmrApi; use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor, One, Zero}; use sp_runtime::{Digest, Saturating}; -use sp_subspace_mmr::ConsensusChainMmrLeafProof; use std::cmp::Ordering; use std::collections::VecDeque; use std::str::FromStr; @@ -686,6 +685,7 @@ where pub(crate) struct ReceiptsChecker where + Block: BlockT, CBlock: BlockT, { pub(crate) domain_id: DomainId, @@ -715,6 +715,7 @@ where } } +#[derive(Clone)] pub struct MismatchedReceipts where Block: BlockT, @@ -744,7 +745,9 @@ where + ProofProvider + ProvideRuntimeApi + 'static, - CClient::Api: DomainsApi + FraudProofApi, + CClient::Api: DomainsApi + + FraudProofApi + + MmrApi>, Backend: sc_client_api::Backend + 'static, E: CodeExecutor, { @@ -760,16 +763,39 @@ where } if let Some(mismatched_receipts) = self.find_mismatch_receipt(consensus_block_hash)? { - let fraud_proof = self.generate_fraud_proof(mismatched_receipts)?; - - tracing::info!("Submit fraud proof: {fraud_proof:?}"); let consensus_best_hash = self.consensus_client.info().best_hash; - let mut runtime_api = self.consensus_client.runtime_api(); - runtime_api.register_extension( - self.consensus_offchain_tx_pool_factory - .offchain_transaction_pool(consensus_best_hash), - ); - runtime_api.submit_fraud_proof_unsigned(consensus_best_hash, fraud_proof)?; + let mut consensus_runtime_api = self.consensus_client.runtime_api(); + let fraud_proof_api_version = consensus_runtime_api + .api_version::>(consensus_best_hash) + .map_err(sp_blockchain::Error::RuntimeApiError)? + .ok_or_else(|| { + sp_blockchain::Error::RuntimeApiError(ApiError::Application( + format!("FraudProofApi not found at: {:?}", consensus_best_hash).into(), + )) + })?; + let domains_api_version = consensus_runtime_api + .api_version::>(consensus_best_hash) + .map_err(sp_blockchain::Error::RuntimeApiError)? + .ok_or_else(|| { + sp_blockchain::Error::RuntimeApiError(ApiError::Application( + format!("DomainsApi not found at: {:?}", consensus_best_hash).into(), + )) + })?; + + // New `DomainsApi` introduced in version 4 is required for generating fraud proof and + // new `FraudProofApi` in version 2 is required for submitting fraud proof + // TODO: remove before next network + if domains_api_version >= 4 && fraud_proof_api_version >= 2 { + let fraud_proof_v2 = self.generate_fraud_proof(mismatched_receipts)?; + + tracing::info!("Submit fraud proof: {fraud_proof_v2:?}"); + consensus_runtime_api.register_extension( + self.consensus_offchain_tx_pool_factory + .offchain_transaction_pool(consensus_best_hash), + ); + consensus_runtime_api + .submit_fraud_proof_unsigned(consensus_best_hash, fraud_proof_v2)?; + } } Ok(()) @@ -850,10 +876,12 @@ where } } + #[allow(clippy::type_complexity)] pub fn generate_fraud_proof( &self, mismatched_receipts: MismatchedReceipts, - ) -> sp_blockchain::Result, CBlock::Hash, Block::Header>> { + ) -> sp_blockchain::Result, CBlock::Hash, Block::Header, H256>> + { let MismatchedReceipts { local_receipt, bad_receipt, @@ -869,14 +897,22 @@ where }) = find_inboxed_bundles_mismatch::(&local_receipt, &bad_receipt)? { return match mismatch_type { - BundleMismatchType::Valid => Ok(FraudProof::ValidBundle(ValidBundleProof { - domain_id: self.domain_id, - bad_receipt_hash, - bundle_index, - })), + BundleMismatchType::Valid => self + .fraud_proof_generator + .generate_valid_bundle_proof_v2( + self.domain_id, + &local_receipt, + bundle_index as usize, + bad_receipt_hash, + ) + .map_err(|err| { + sp_blockchain::Error::Application(Box::from(format!( + "Failed to generate valid bundles fraud proof: {err}" + ))) + }), _ => self .fraud_proof_generator - .generate_invalid_bundle_field_proof( + .generate_invalid_bundle_proof_v2( self.domain_id, &local_receipt, mismatch_type, @@ -885,7 +921,7 @@ where ) .map_err(|err| { sp_blockchain::Error::Application(Box::from(format!( - "Failed to generate invalid bundles field fraud proof: {err}" + "Failed to generate invalid bundles fraud proof: {err}" ))) }), }; @@ -894,7 +930,7 @@ where if bad_receipt.domain_block_extrinsic_root != local_receipt.domain_block_extrinsic_root { return self .fraud_proof_generator - .generate_invalid_domain_extrinsics_root_proof( + .generate_invalid_domain_extrinsics_root_proof_v2( self.domain_id, &local_receipt, bad_receipt_hash, @@ -921,7 +957,7 @@ where { return self .fraud_proof_generator - .generate_invalid_state_transition_proof( + .generate_invalid_state_transition_proof_v2( self.domain_id, execution_phase, &local_receipt, @@ -938,7 +974,11 @@ where if bad_receipt.block_fees != local_receipt.block_fees { return self .fraud_proof_generator - .generate_invalid_block_fees_proof(self.domain_id, &local_receipt, bad_receipt_hash) + .generate_invalid_block_fees_proof_v2( + self.domain_id, + &local_receipt, + bad_receipt_hash, + ) .map_err(|err| { sp_blockchain::Error::Application(Box::from(format!( "Failed to generate invalid block rewards fraud proof: {err}" @@ -949,7 +989,11 @@ where if bad_receipt.transfers != local_receipt.transfers { return self .fraud_proof_generator - .generate_invalid_transfers_proof(self.domain_id, &local_receipt, bad_receipt_hash) + .generate_invalid_transfers_proof_v2( + self.domain_id, + &local_receipt, + bad_receipt_hash, + ) .map_err(|err| { sp_blockchain::Error::Application(Box::from(format!( "Failed to generate invalid transfers fraud proof: {err}" @@ -960,7 +1004,7 @@ where if bad_receipt.domain_block_hash != local_receipt.domain_block_hash { return self .fraud_proof_generator - .generate_invalid_domain_block_hash_proof( + .generate_invalid_domain_block_hash_proof_v2( self.domain_id, &local_receipt, bad_receipt_hash, @@ -979,58 +1023,6 @@ where } } -/// Generate MMR proof for the block `to_prove` in the current best fork. The returned proof -/// can be later used to verify stateless (without query offchain MMR leaf) and extract the state -/// root at `to_prove`. -// TODO: remove `dead_code` after it is used in fraud proof generation -#[allow(dead_code)] -pub(crate) fn generate_mmr_proof( - consensus_client: &Arc, - to_prove: NumberFor, -) -> sp_blockchain::Result, CBlock::Hash, H256>> -where - CBlock: BlockT, - CClient: HeaderBackend + ProvideRuntimeApi + 'static, - CClient::Api: MmrApi>, -{ - let api = consensus_client.runtime_api(); - let prove_at_hash = consensus_client.info().best_hash; - let prove_at_number = consensus_client.info().best_number; - - if to_prove >= prove_at_number { - return Err(sp_blockchain::Error::Application(Box::from(format!( - "Can't generate MMR proof for block {to_prove:?} >= best block {prove_at_number:?}" - )))); - } - - let (mut leaves, proof) = api - // NOTE: the mmr leaf data is added in the next block so to generate the MMR proof of - // block `to_prove` we need to use `to_prove + 1` here. - .generate_proof( - prove_at_hash, - vec![to_prove + One::one()], - Some(prove_at_number), - )? - .map_err(|err| { - sp_blockchain::Error::Application(Box::from(format!( - "Failed to generate MMR proof: {err}" - ))) - })?; - debug_assert!(leaves.len() == 1, "should always be of length 1"); - let leaf = leaves - .pop() - .ok_or(sp_blockchain::Error::Application(Box::from( - "Unexpected missing mmr leaf".to_string(), - )))?; - - Ok(ConsensusChainMmrLeafProof { - consensus_block_number: prove_at_number, - consensus_block_hash: prove_at_hash, - opaque_mmr_leaf: leaf, - proof, - }) -} - #[cfg(test)] mod tests { use super::*; @@ -1172,12 +1164,17 @@ mod tests { ]), &create_test_execution_receipt(vec![ InboxedBundle::valid(Default::default(), Default::default()), - InboxedBundle::invalid(InvalidBundleType::InvalidXDM(3), Default::default()), + InboxedBundle::invalid( + InvalidBundleType::InherentExtrinsic(3), + Default::default() + ), ]), ) .unwrap(), Some(InboxedBundleMismatchInfo { - mismatch_type: BundleMismatchType::FalseInvalid(InvalidBundleType::InvalidXDM(3)), + mismatch_type: BundleMismatchType::FalseInvalid( + InvalidBundleType::InherentExtrinsic(3) + ), bundle_index: 1, }) ); @@ -1186,7 +1183,10 @@ mod tests { find_inboxed_bundles_mismatch::( &create_test_execution_receipt(vec![ InboxedBundle::valid(Default::default(), Default::default()), - InboxedBundle::invalid(InvalidBundleType::InvalidXDM(3), Default::default()), + InboxedBundle::invalid( + InvalidBundleType::InherentExtrinsic(3), + Default::default() + ), ]), &create_test_execution_receipt(vec![ InboxedBundle::valid(Default::default(), Default::default()), @@ -1195,7 +1195,9 @@ mod tests { ) .unwrap(), Some(InboxedBundleMismatchInfo { - mismatch_type: BundleMismatchType::TrueInvalid(InvalidBundleType::InvalidXDM(3)), + mismatch_type: BundleMismatchType::TrueInvalid( + InvalidBundleType::InherentExtrinsic(3) + ), bundle_index: 1, }) ); @@ -1205,7 +1207,10 @@ mod tests { find_inboxed_bundles_mismatch::( &create_test_execution_receipt(vec![ InboxedBundle::valid(H256::random(), Default::default()), - InboxedBundle::invalid(InvalidBundleType::InvalidXDM(3), Default::default()), + InboxedBundle::invalid( + InvalidBundleType::InherentExtrinsic(3), + Default::default() + ), ]), &create_test_execution_receipt(vec![ InboxedBundle::valid(H256::random(), Default::default()), diff --git a/domains/client/domain-operator/src/domain_worker.rs b/domains/client/domain-operator/src/domain_worker.rs index 7233d0fc64..72b162714a 100644 --- a/domains/client/domain-operator/src/domain_worker.rs +++ b/domains/client/domain-operator/src/domain_worker.rs @@ -33,6 +33,7 @@ use sp_domains::core_api::DomainCoreApi; use sp_domains::{BundleProducerElectionApi, DomainsApi, OpaqueBundle, OperatorId}; use sp_domains_fraud_proof::FraudProofApi; use sp_messenger::MessengerApi; +use sp_mmr_primitives::MmrApi; use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor}; use sp_transaction_pool::runtime_api::TaggedTransactionQueue; use std::pin::pin; @@ -92,7 +93,8 @@ pub(super) async fn start_worker< CClient::Api: DomainsApi + MessengerApi + BundleProducerElectionApi - + FraudProofApi, + + FraudProofApi + + MmrApi>, TransactionPool: sc_transaction_pool_api::TransactionPool::Hash> + 'static, Backend: sc_client_api::Backend + 'static, diff --git a/domains/client/domain-operator/src/fraud_proof.rs b/domains/client/domain-operator/src/fraud_proof.rs index 6a68d8aa68..0d11ce3918 100644 --- a/domains/client/domain-operator/src/fraud_proof.rs +++ b/domains/client/domain-operator/src/fraud_proof.rs @@ -5,6 +5,7 @@ use domain_block_builder::{BlockBuilder, RecordProof}; use domain_runtime_primitives::opaque::AccountId; use domain_runtime_primitives::CheckExtrinsicsValidityError; use sc_client_api::{AuxStore, BlockBackend, ProofProvider}; +use sc_domains::FPStorageKeyProvider; use sp_api::{ApiExt, ProvideRuntimeApi}; use sp_blockchain::HeaderBackend; use sp_core::traits::CodeExecutor; @@ -12,16 +13,22 @@ use sp_core::H256; use sp_domain_digests::AsPredigest; use sp_domains::core_api::DomainCoreApi; use sp_domains::proof_provider_and_verifier::StorageProofProvider; -use sp_domains::{DomainId, DomainsApi, ExtrinsicDigest, HeaderHashingFor, InvalidBundleType}; +use sp_domains::{ + DomainId, DomainsApi, DomainsDigestItem, ExtrinsicDigest, HeaderHashingFor, InvalidBundleType, + RuntimeId, +}; use sp_domains_fraud_proof::execution_prover::ExecutionProver; use sp_domains_fraud_proof::fraud_proof::{ - ApplyExtrinsicMismatch, ExecutionPhase, FinalizeBlockMismatch, FraudProof, - InvalidBlockFeesProof, InvalidBundlesFraudProof, InvalidDomainBlockHashProof, - InvalidExtrinsicsRootProof, InvalidStateTransitionProof, InvalidTransfersProof, - ValidBundleDigest, + ApplyExtrinsicMismatch, DomainRuntimeCodeAt, ExecutionPhase, FinalizeBlockMismatch, + FraudProofV2, FraudProofVariant, InvalidBlockFeesProofV2, InvalidBundlesProofData, + InvalidBundlesProofV2, InvalidDomainBlockHashProofV2, InvalidExtrinsicsRootProofV2, + InvalidStateTransitionProofV2, InvalidTransfersProofV2, ValidBundleDigest, ValidBundleProofV2, }; +use sp_domains_fraud_proof::storage_proof::{self, *}; +use sp_domains_fraud_proof::FraudProofApi; +use sp_mmr_primitives::MmrApi; use sp_runtime::generic::BlockId; -use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor}; +use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor, One}; use sp_runtime::{Digest, DigestItem}; use sp_trie::LayoutV1; use std::marker::PhantomData; @@ -68,17 +75,20 @@ pub enum FraudProofError { is_true_invalid: bool, extrinsics_validity_response: Result<(), CheckExtrinsicsValidityError>, }, + #[error("Fail to generate the storage proof")] + StorageProof(#[from] storage_proof::GenerationError), } -pub struct FraudProofGenerator { +pub struct FraudProofGenerator { client: Arc, consensus_client: Arc, backend: Arc, code_executor: Arc, + storage_key_provider: FPStorageKeyProvider, _phantom: PhantomData<(Block, CBlock)>, } -impl Clone +impl Clone for FraudProofGenerator { fn clone(&self) -> Self { @@ -87,13 +97,14 @@ impl Clone consensus_client: self.consensus_client.clone(), backend: self.backend.clone(), code_executor: self.code_executor.clone(), + storage_key_provider: self.storage_key_provider.clone(), _phantom: self._phantom, } } } -type FraudProofFor = - FraudProof, ::Hash, DomainHeader>; +type FraudProofV2For = + FraudProofV2, ::Hash, DomainHeader, H256>; impl FraudProofGenerator @@ -114,7 +125,9 @@ where + ProvideRuntimeApi + ProofProvider + 'static, - CClient::Api: DomainsApi, + CClient::Api: DomainsApi + + MmrApi> + + FraudProofApi, Backend: sc_client_api::Backend + Send + Sync + 'static, E: CodeExecutor, { @@ -126,216 +139,314 @@ where ) -> Self { Self { client, - consensus_client, + consensus_client: consensus_client.clone(), backend, code_executor, + storage_key_provider: FPStorageKeyProvider::new(consensus_client), _phantom: Default::default(), } } - pub(crate) fn generate_invalid_block_fees_proof( + #[allow(clippy::type_complexity)] + fn maybe_generate_domain_runtime_code_proof_for_receipt( &self, domain_id: DomainId, local_receipt: &ExecutionReceiptFor, - bad_receipt_hash: Block::Hash, - ) -> Result, FraudProofError> { - let block_hash = local_receipt.domain_block_hash; - let key = sp_domains::operator_block_fees_final_key(); - let proof = self - .client - .read_proof(block_hash, &mut [key.as_slice()].into_iter())?; - Ok(FraudProof::InvalidBlockFees(InvalidBlockFeesProof { - domain_id, - bad_receipt_hash, - storage_proof: proof, - })) + ) -> Result< + Option, ::Hash, H256>>, + FraudProofError, + > { + // NOTE: domain runtime code is take affect in the next block, so we need to get + // the doamin runtime code of the parent block, which is what used to derived the + // ER. + let (parent_consensus_number, parent_consensus_hash) = { + let consensus_header = self.consensus_header(local_receipt.consensus_block_hash)?; + ( + local_receipt.consensus_block_number - One::one(), + *consensus_header.parent_hash(), + ) + }; + + let best_hash = self.consensus_client.info().best_hash; + let runtime_api = self.consensus_client.runtime_api(); + let runtime_id = runtime_api + .runtime_id(best_hash, domain_id)? + .ok_or_else(|| { + sp_blockchain::Error::Application( + "Failed to get domain runtime id".to_string().into(), + ) + })?; + let is_domain_runtime_updraded_since = runtime_api + .is_domain_runtime_updraded_since(best_hash, domain_id, parent_consensus_number)? + .ok_or_else(|| { + sp_blockchain::Error::Application( + "Failed to get domain runtime object".to_string().into(), + ) + })?; + + if is_domain_runtime_updraded_since { + let mmr_proof = + sc_domains::generate_mmr_proof(&self.consensus_client, parent_consensus_number)?; + let domain_runtime_code_proof = DomainRuntimeCodeProof::generate( + self.consensus_client.as_ref(), + parent_consensus_hash, + runtime_id, + &self.storage_key_provider, + )?; + Ok(Some(DomainRuntimeCodeAt { + mmr_proof, + domain_runtime_code_proof, + })) + } else { + Ok(None) + } } - pub(crate) fn generate_invalid_transfers_proof( + pub(crate) fn generate_invalid_state_transition_proof_v2( &self, domain_id: DomainId, + execution_phase: ExecutionPhase, local_receipt: &ExecutionReceiptFor, + bad_receipt_trace_length: usize, bad_receipt_hash: Block::Hash, - ) -> Result, FraudProofError> { + ) -> Result, FraudProofError> { let block_hash = local_receipt.domain_block_hash; - let runtime_api = self.client.runtime_api(); - let key = runtime_api.transfers_storage_key(block_hash)?; - let proof = self - .client - .read_proof(block_hash, &mut [key.as_slice()].into_iter())?; - Ok(FraudProof::InvalidTransfers(InvalidTransfersProof { + let block_number = local_receipt.domain_block_number; + let header = self.header(block_hash)?; + let parent_header = self.header(*header.parent_hash())?; + + let prover = ExecutionProver::new(self.backend.clone(), self.code_executor.clone()); + + let inherent_digests = Digest { + logs: vec![DigestItem::consensus_block_info( + local_receipt.consensus_block_hash, + )], + }; + + let extrinsics = self.block_body(block_hash)?; + let max_extrinsic_index = extrinsics.len() - 1; + let encoded_extrinsics: Vec<_> = extrinsics.iter().map(Encode::encode).collect(); + + let block_builder = BlockBuilder::new( + &*self.client, + parent_header.hash(), + *parent_header.number(), + RecordProof::No, + inherent_digests.clone(), + &*self.backend, + extrinsics.into(), + // NOTE: the inherent extrinsic is already contained in the above `extrinsics`, which + // is getting from the block body, thus it is okay to pass `maybe_inherent_data` as + // `None` and `is_gemini_3h` as `false`, the latter is only used when `maybe_inherent_data` + // is `Some`. + None, + false, + )?; + + let (storage_changes, call_data) = match &execution_phase { + ExecutionPhase::InitializeBlock => ( + None, + Block::Header::new( + block_number, + Default::default(), + Default::default(), + parent_header.hash(), + inherent_digests, + ) + .encode(), + ), + ExecutionPhase::ApplyExtrinsic { mismatch, .. } => { + let extrinsic_index = match mismatch { + ApplyExtrinsicMismatch::StateRoot(trace_mismatch_index) => { + (trace_mismatch_index - 1) as usize + } + ApplyExtrinsicMismatch::Shorter => (bad_receipt_trace_length - 1) - 1, + }; + + let target_extrinsic = encoded_extrinsics.get(extrinsic_index).ok_or( + FraudProofError::OutOfBoundsExtrinsicIndex { + index: extrinsic_index, + max: max_extrinsic_index, + }, + )?; + + ( + Some(block_builder.prepare_storage_changes_before(extrinsic_index)?), + target_extrinsic.clone(), + ) + } + ExecutionPhase::FinalizeBlock { .. } => ( + Some(block_builder.prepare_storage_changes_before_finalize_block()?), + Vec::new(), + ), + }; + + let delta_changes = storage_changes.map(|storage_changes| { + ( + storage_changes.transaction, + storage_changes.transaction_storage_root, + ) + }); + + let execution_proof = prover.prove_execution( + parent_header.hash(), + &execution_phase, + call_data.as_slice(), + delta_changes, + )?; + + let maybe_domain_runtime_code_proof = + self.maybe_generate_domain_runtime_code_proof_for_receipt(domain_id, local_receipt)?; + + let invalid_state_transition_proof = FraudProofV2 { domain_id, bad_receipt_hash, - storage_proof: proof, - })) - } + maybe_mmr_proof: None, + maybe_domain_runtime_code_proof, + proof: FraudProofVariant::InvalidStateTransition(InvalidStateTransitionProofV2 { + execution_proof, + execution_phase, + }), + }; - pub(crate) fn generate_invalid_domain_block_hash_proof( - &self, - domain_id: DomainId, - local_receipt: &ExecutionReceiptFor, - bad_receipt_hash: Block::Hash, - ) -> Result, FraudProofError> { - let block_hash = local_receipt.domain_block_hash; - let digest_key = sp_domains::system_digest_final_key(); - let digest_storage_proof = self - .client - .read_proof(block_hash, &mut [digest_key.as_slice()].into_iter())?; - Ok(FraudProof::InvalidDomainBlockHash( - InvalidDomainBlockHashProof { - domain_id, - bad_receipt_hash, - digest_storage_proof, - }, - )) + Ok(invalid_state_transition_proof) } - pub(crate) fn generate_invalid_bundle_field_proof( + pub(crate) fn generate_valid_bundle_proof_v2( &self, domain_id: DomainId, local_receipt: &ExecutionReceiptFor, - mismatch_type: BundleMismatchType, - bundle_index: u32, + bundle_index: usize, bad_receipt_hash: Block::Hash, - ) -> Result, FraudProofError> { + ) -> Result, FraudProofError> { let consensus_block_hash = local_receipt.consensus_block_hash; + let consensus_block_number = local_receipt.consensus_block_number; let consensus_extrinsics = self .consensus_client .block_body(consensus_block_hash)? .ok_or(FraudProofError::MissingConsensusExtrinsics)?; - let bundles = self + let mut bundles = self .consensus_client .runtime_api() .extract_successful_bundles(consensus_block_hash, domain_id, consensus_extrinsics)?; - let bundle = bundles - .get(bundle_index as usize) - .ok_or(FraudProofError::MissingBundle { - bundle_index: bundle_index as usize, - })?; + if bundle_index >= bundles.len() { + return Err(FraudProofError::MissingBundle { bundle_index }); + } + let bundle = bundles.swap_remove(bundle_index); - let (invalid_type, is_true_invalid) = match mismatch_type { - BundleMismatchType::TrueInvalid(invalid_type) => (invalid_type, true), - BundleMismatchType::FalseInvalid(invalid_type) => (invalid_type, false), - BundleMismatchType::Valid => { - return Err(sp_blockchain::Error::Application( - "Unexpected bundle mismatch type, this should not happen" - .to_string() - .into(), - ) - .into()); - } + let bundle_with_proof = OpaqueBundleWithProof::generate( + &self.storage_key_provider, + self.consensus_client.as_ref(), + domain_id, + consensus_block_hash, + bundle, + bundle_index as u32, + )?; + + let mmr_proof = + sc_domains::generate_mmr_proof(&self.consensus_client, consensus_block_number)?; + + let maybe_domain_runtime_code_proof = + self.maybe_generate_domain_runtime_code_proof_for_receipt(domain_id, local_receipt)?; + + let valid_bundle_proof = FraudProofV2 { + domain_id, + bad_receipt_hash, + maybe_mmr_proof: Some(mmr_proof), + maybe_domain_runtime_code_proof, + proof: FraudProofVariant::ValidBundle(ValidBundleProofV2 { bundle_with_proof }), }; - let extrinsic_index = invalid_type.extrinsic_index(); - let encoded_extrinsics: Vec<_> = bundle.extrinsics.iter().map(Encode::encode).collect(); - let proof_data = if let InvalidBundleType::IllegalTx(expected_extrinsic_index) = - invalid_type - { - if expected_extrinsic_index as usize >= bundle.extrinsics.len() { - return Err(FraudProofError::OutOfBoundsExtrinsicIndex { - index: expected_extrinsic_index as usize, - max: bundle.extrinsics.len(), - }); - } + Ok(valid_bundle_proof) + } - let domain_block_parent_hash = *self - .client - .header(local_receipt.domain_block_hash)? - .ok_or_else(|| { - FraudProofError::Blockchain(sp_blockchain::Error::MissingHeader(format!( - "{:?}", - local_receipt.domain_block_hash - ))) - })? - .parent_hash(); - - let domain_block_parent_number = self - .client - .block_number_from_id(&BlockId::Hash(domain_block_parent_hash))? - .ok_or_else(|| { - FraudProofError::Blockchain(sp_blockchain::Error::Backend(format!( - "unable to get block number for domain block:{:?}", - domain_block_parent_hash - ))) - })?; + pub(crate) fn generate_invalid_domain_extrinsics_root_proof_v2( + &self, + domain_id: DomainId, + local_receipt: &ExecutionReceiptFor, + bad_receipt_hash: Block::Hash, + ) -> Result, FraudProofError> { + let consensus_block_hash = local_receipt.consensus_block_hash; + let consensus_block_number = local_receipt.consensus_block_number; - let mut runtime_api_instance = self.client.runtime_api(); - runtime_api_instance.record_proof(); - let proof_recorder = runtime_api_instance - .proof_recorder() - .expect("we enabled proof recording just above; qed"); - - let mut block_extrinsics = vec![]; - for (extrinsic_index, extrinsic) in bundle - .extrinsics - .iter() - .enumerate() - .take((expected_extrinsic_index + 1) as usize) - { - let encoded_extrinsic = extrinsic.encode(); - block_extrinsics.push( - ::Extrinsic::decode(&mut encoded_extrinsic.as_slice()) - .map_err(|decoding_error| { - FraudProofError::UnableToDecodeOpaqueBundleExtrinsic { - extrinsic_index, - decoding_error, - } - })?, - ); - } + let valid_bundle_digests = + self.generate_valid_bundle_digest_for_receipt(domain_id, local_receipt)?; - let validation_response = runtime_api_instance.check_extrinsics_and_do_pre_dispatch( - domain_block_parent_hash, - block_extrinsics, - domain_block_parent_number, - domain_block_parent_hash, - )?; + let mmr_proof = + sc_domains::generate_mmr_proof(&self.consensus_client, consensus_block_number)?; - // If the proof is true invalid then validation response should not be Ok. - // If the proof is false invalid then validation response should not be Err. - // OR - // If it is true invalid and expected extrinsic index does not match - if (is_true_invalid == validation_response.is_ok()) - || (is_true_invalid - && validation_response - .as_ref() - .is_err_and(|e| e.extrinsic_index != expected_extrinsic_index)) - { - return Err(FraudProofError::InvalidIllegalTxFraudProofExtrinsicIndex { - index: expected_extrinsic_index as usize, - is_true_invalid, - extrinsics_validity_response: validation_response, - }); - } + let maybe_domain_runtime_code_proof = + self.maybe_generate_domain_runtime_code_proof_for_receipt(domain_id, local_receipt)?; - proof_recorder.drain_storage_proof() - } else { - StorageProofProvider::< - LayoutV1>, - >::generate_enumerated_proof_of_inclusion( - encoded_extrinsics.as_slice(), extrinsic_index, - ) - .ok_or(FraudProofError::FailToGenerateProofOfInclusion)? - }; + let block_randomness_proof = BlockRandomnessProof::generate( + self.consensus_client.as_ref(), + consensus_block_hash, + (), + &self.storage_key_provider, + )?; - Ok(FraudProof::InvalidBundles(InvalidBundlesFraudProof::new( - bad_receipt_hash, + let maybe_runtime_id = + self.is_domain_runtime_updraded_at(domain_id, consensus_block_hash)?; + let domain_inherent_extrinsic_data_proof = DomainInherentExtrinsicDataProof::generate( + &self.storage_key_provider, + self.consensus_client.as_ref(), domain_id, - bundle_index, - invalid_type, - proof_data, - is_true_invalid, - ))) + consensus_block_hash, + maybe_runtime_id, + )?; + + let invalid_domain_extrinsics_root_proof = FraudProofV2 { + domain_id, + bad_receipt_hash, + maybe_mmr_proof: Some(mmr_proof), + maybe_domain_runtime_code_proof, + proof: FraudProofVariant::InvalidExtrinsicsRoot(InvalidExtrinsicsRootProofV2 { + valid_bundle_digests, + block_randomness_proof, + domain_inherent_extrinsic_data_proof, + }), + }; + + Ok(invalid_domain_extrinsics_root_proof) + } + + pub fn is_domain_runtime_updraded_at( + &self, + domain_id: DomainId, + at: CBlock::Hash, + ) -> Result, FraudProofError> { + let header = + self.consensus_client + .header(at)? + .ok_or(sp_blockchain::Error::MissingHeader(format!( + "No header found for {at:?}" + )))?; + + let runtime_id = self + .consensus_client + .runtime_api() + .runtime_id(at, domain_id)? + .ok_or(sp_blockchain::Error::Application(Box::from(format!( + "No RuntimeId found for {domain_id:?}" + ))))?; + + let is_runtime_upgraded = header + .digest() + .logs + .iter() + .filter_map(|log| log.as_domain_runtime_upgrade()) + .any(|upgraded_runtime_id| upgraded_runtime_id == runtime_id); + + Ok(is_runtime_upgraded.then_some(runtime_id)) } - pub(crate) fn generate_invalid_domain_extrinsics_root_proof( + pub(crate) fn generate_valid_bundle_digest_for_receipt( &self, domain_id: DomainId, local_receipt: &ExecutionReceiptFor, - bad_receipt_hash: Block::Hash, - ) -> Result, FraudProofError> { + ) -> Result, FraudProofError> { let consensus_block_hash = local_receipt.consensus_block_hash; let consensus_extrinsics = self .consensus_client @@ -391,118 +502,273 @@ where }); } - Ok(FraudProof::InvalidExtrinsicsRoot( - InvalidExtrinsicsRootProof { - domain_id, - bad_receipt_hash, - valid_bundle_digests, - }, - )) + Ok(valid_bundle_digests) } - pub(crate) fn generate_invalid_state_transition_proof( + pub(crate) fn generate_invalid_bundle_proof_v2( &self, domain_id: DomainId, - execution_phase: ExecutionPhase, local_receipt: &ExecutionReceiptFor, - bad_receipt_trace_length: usize, + mismatch_type: BundleMismatchType, + bundle_index: u32, bad_receipt_hash: Block::Hash, - ) -> Result, FraudProofError> { - let block_hash = local_receipt.domain_block_hash; - let block_number = local_receipt.domain_block_number; - let header = self.header(block_hash)?; - let parent_header = self.header(*header.parent_hash())?; + ) -> Result, FraudProofError> { + let consensus_block_hash = local_receipt.consensus_block_hash; + let consensus_block_number = local_receipt.consensus_block_number; + + let bundle = { + let consensus_extrinsics = self + .consensus_client + .block_body(consensus_block_hash)? + .ok_or(FraudProofError::MissingConsensusExtrinsics)?; + + let mut bundles = self + .consensus_client + .runtime_api() + .extract_successful_bundles( + consensus_block_hash, + domain_id, + consensus_extrinsics, + )?; + if bundles.len() <= bundle_index as usize { + return Err(FraudProofError::MissingBundle { + bundle_index: bundle_index as usize, + }); + } - let prover = ExecutionProver::new(self.backend.clone(), self.code_executor.clone()); + bundles.swap_remove(bundle_index as usize) + }; - let inherent_digests = Digest { - logs: vec![DigestItem::consensus_block_info( - local_receipt.consensus_block_hash, - )], + let (invalid_type, is_true_invalid) = match mismatch_type { + BundleMismatchType::TrueInvalid(invalid_type) => (invalid_type, true), + BundleMismatchType::FalseInvalid(invalid_type) => (invalid_type, false), + BundleMismatchType::Valid => { + return Err(sp_blockchain::Error::Application( + "Unexpected bundle mismatch type, this should not happen" + .to_string() + .into(), + ) + .into()); + } }; - let extrinsics = self.block_body(block_hash)?; - let max_extrinsic_index = extrinsics.len() - 1; - let encoded_extrinsics: Vec<_> = extrinsics.iter().map(Encode::encode).collect(); + let extrinsic_index = invalid_type.extrinsic_index(); + let encoded_extrinsics: Vec<_> = bundle.extrinsics.iter().map(Encode::encode).collect(); + let proof_data = match invalid_type { + InvalidBundleType::IllegalTx(expected_extrinsic_index) => { + if expected_extrinsic_index as usize >= bundle.extrinsics.len() { + return Err(FraudProofError::OutOfBoundsExtrinsicIndex { + index: expected_extrinsic_index as usize, + max: bundle.extrinsics.len(), + }); + } - let block_builder = BlockBuilder::new( - &*self.client, - parent_header.hash(), - *parent_header.number(), - RecordProof::No, - inherent_digests.clone(), - &*self.backend, - extrinsics.into(), - // NOTE: the inherent extrinsic is already contained in the above `extrinsics`, which - // is getting from the block body, thus it is okay to pass `maybe_inherent_data` as - // `None` and `is_gemini_3h` as `false`, the latter is only used when `maybe_inherent_data` - // is `Some`. - None, - false, - )?; + let domain_block_parent_hash = *self + .client + .header(local_receipt.domain_block_hash)? + .ok_or_else(|| { + FraudProofError::Blockchain(sp_blockchain::Error::MissingHeader(format!( + "{:?}", + local_receipt.domain_block_hash + ))) + })? + .parent_hash(); + + let domain_block_parent_number = self + .client + .block_number_from_id(&BlockId::Hash(domain_block_parent_hash))? + .ok_or_else(|| { + FraudProofError::Blockchain(sp_blockchain::Error::Backend(format!( + "unable to get block number for domain block:{:?}", + domain_block_parent_hash + ))) + })?; - let (storage_changes, call_data) = match &execution_phase { - ExecutionPhase::InitializeBlock => ( - None, - Block::Header::new( - block_number, - Default::default(), - Default::default(), - parent_header.hash(), - inherent_digests, - ) - .encode(), - ), - ExecutionPhase::ApplyExtrinsic { mismatch, .. } => { - let extrinsic_index = match mismatch { - ApplyExtrinsicMismatch::StateRoot(trace_mismatch_index) => { - (trace_mismatch_index - 1) as usize - } - ApplyExtrinsicMismatch::Shorter => (bad_receipt_trace_length - 1) - 1, - }; + let mut runtime_api_instance = self.client.runtime_api(); + runtime_api_instance.record_proof(); + let proof_recorder = runtime_api_instance + .proof_recorder() + .expect("we enabled proof recording just above; qed"); + + let mut block_extrinsics = vec![]; + for (extrinsic_index, extrinsic) in bundle + .extrinsics + .iter() + .enumerate() + .take((expected_extrinsic_index + 1) as usize) + { + let encoded_extrinsic = extrinsic.encode(); + block_extrinsics.push( + ::Extrinsic::decode(&mut encoded_extrinsic.as_slice()) + .map_err(|decoding_error| { + FraudProofError::UnableToDecodeOpaqueBundleExtrinsic { + extrinsic_index, + decoding_error, + } + })?, + ); + } - let target_extrinsic = encoded_extrinsics.get(extrinsic_index).ok_or( - FraudProofError::OutOfBoundsExtrinsicIndex { - index: extrinsic_index, - max: max_extrinsic_index, - }, + let validation_response = runtime_api_instance + .check_extrinsics_and_do_pre_dispatch( + domain_block_parent_hash, + block_extrinsics, + domain_block_parent_number, + domain_block_parent_hash, + )?; + + // If the proof is true invalid then validation response should not be Ok. + // If the proof is false invalid then validation response should not be Err. + // OR + // If it is true invalid and expected extrinsic index does not match + if (is_true_invalid == validation_response.is_ok()) + || (is_true_invalid + && validation_response + .as_ref() + .is_err_and(|e| e.extrinsic_index != expected_extrinsic_index)) + { + return Err(FraudProofError::InvalidIllegalTxFraudProofExtrinsicIndex { + index: expected_extrinsic_index as usize, + is_true_invalid, + extrinsics_validity_response: validation_response, + }); + } + + let execution_proof = proof_recorder.drain_storage_proof(); + + let bundle_with_proof = OpaqueBundleWithProof::generate( + &self.storage_key_provider, + self.consensus_client.as_ref(), + domain_id, + consensus_block_hash, + bundle, + bundle_index, )?; - ( - Some(block_builder.prepare_storage_changes_before(extrinsic_index)?), - target_extrinsic.clone(), + InvalidBundlesProofData::BundleAndExecution { + bundle_with_proof, + execution_proof, + } + } + InvalidBundleType::OutOfRangeTx(_) => { + let bundle_with_proof = OpaqueBundleWithProof::generate( + &self.storage_key_provider, + self.consensus_client.as_ref(), + domain_id, + consensus_block_hash, + bundle, + bundle_index, + )?; + + InvalidBundlesProofData::Bundle(bundle_with_proof) + } + _ => { + let extrinsic_proof = StorageProofProvider::< + LayoutV1>, + >::generate_enumerated_proof_of_inclusion( + encoded_extrinsics.as_slice(), + extrinsic_index, ) + .ok_or(FraudProofError::FailToGenerateProofOfInclusion)?; + + InvalidBundlesProofData::Extrinsic(extrinsic_proof) } - ExecutionPhase::FinalizeBlock { .. } => ( - Some(block_builder.prepare_storage_changes_before_finalize_block()?), - Vec::new(), - ), }; - let delta_changes = storage_changes.map(|storage_changes| { - ( - storage_changes.transaction, - storage_changes.transaction_storage_root, - ) - }); + let mmr_proof = + sc_domains::generate_mmr_proof(&self.consensus_client, consensus_block_number)?; - let execution_proof = prover.prove_execution( - parent_header.hash(), - &execution_phase, - call_data.as_slice(), - delta_changes, - )?; + let maybe_domain_runtime_code_proof = + self.maybe_generate_domain_runtime_code_proof_for_receipt(domain_id, local_receipt)?; + + let invalid_bundle_proof = FraudProofV2 { + domain_id, + bad_receipt_hash, + maybe_mmr_proof: Some(mmr_proof), + maybe_domain_runtime_code_proof, + proof: FraudProofVariant::InvalidBundles(InvalidBundlesProofV2 { + bundle_index, + invalid_bundle_type: invalid_type, + is_true_invalid_fraud_proof: is_true_invalid, + proof_data, + }), + }; + Ok(invalid_bundle_proof) + } + + pub(crate) fn generate_invalid_block_fees_proof_v2( + &self, + domain_id: DomainId, + local_receipt: &ExecutionReceiptFor, + bad_receipt_hash: Block::Hash, + ) -> Result, FraudProofError> { + let block_hash = local_receipt.domain_block_hash; + let key = sp_domains::operator_block_fees_final_key(); + let storage_proof = self + .client + .read_proof(block_hash, &mut [key.as_slice()].into_iter())?; + + let maybe_domain_runtime_code_proof = + self.maybe_generate_domain_runtime_code_proof_for_receipt(domain_id, local_receipt)?; + + let invalid_block_fees_proof = FraudProofV2 { + domain_id, + bad_receipt_hash, + maybe_mmr_proof: None, + maybe_domain_runtime_code_proof, + proof: FraudProofVariant::InvalidBlockFees(InvalidBlockFeesProofV2 { storage_proof }), + }; + Ok(invalid_block_fees_proof) + } + + pub(crate) fn generate_invalid_transfers_proof_v2( + &self, + domain_id: DomainId, + local_receipt: &ExecutionReceiptFor, + bad_receipt_hash: Block::Hash, + ) -> Result, FraudProofError> { + let block_hash = local_receipt.domain_block_hash; + let runtime_api = self.client.runtime_api(); + let key = runtime_api.transfers_storage_key(block_hash)?; + let storage_proof = self + .client + .read_proof(block_hash, &mut [key.as_slice()].into_iter())?; + + let maybe_domain_runtime_code_proof = + self.maybe_generate_domain_runtime_code_proof_for_receipt(domain_id, local_receipt)?; - let invalid_state_transition_proof = InvalidStateTransitionProof { + let invalid_transfers_proof = FraudProofV2 { domain_id, bad_receipt_hash, - proof: execution_proof, - execution_phase, + maybe_mmr_proof: None, + maybe_domain_runtime_code_proof, + proof: FraudProofVariant::InvalidTransfers(InvalidTransfersProofV2 { storage_proof }), }; + Ok(invalid_transfers_proof) + } + + pub(crate) fn generate_invalid_domain_block_hash_proof_v2( + &self, + domain_id: DomainId, + local_receipt: &ExecutionReceiptFor, + bad_receipt_hash: Block::Hash, + ) -> Result, FraudProofError> { + let block_hash = local_receipt.domain_block_hash; + let digest_key = sp_domains::system_digest_final_key(); + let digest_storage_proof = self + .client + .read_proof(block_hash, &mut [digest_key.as_slice()].into_iter())?; - Ok(FraudProof::InvalidStateTransition( - invalid_state_transition_proof, - )) + let invalid_domain_block_hash = FraudProofV2 { + domain_id, + bad_receipt_hash, + maybe_mmr_proof: None, + maybe_domain_runtime_code_proof: None, + proof: FraudProofVariant::InvalidDomainBlockHash(InvalidDomainBlockHashProofV2 { + digest_storage_proof, + }), + }; + Ok(invalid_domain_block_hash) } fn header(&self, hash: Block::Hash) -> Result { @@ -511,6 +777,12 @@ where .ok_or_else(|| sp_blockchain::Error::Backend(format!("Header not found for {hash:?}"))) } + fn consensus_header(&self, hash: CBlock::Hash) -> Result { + self.consensus_client.header(hash)?.ok_or_else(|| { + sp_blockchain::Error::Backend(format!("Consensus header not found for {hash:?}")) + }) + } + fn block_body(&self, at: Block::Hash) -> Result, sp_blockchain::Error> { self.client.block_body(at)?.ok_or_else(|| { sp_blockchain::Error::Backend(format!("Block body not found for {at:?}")) diff --git a/domains/client/domain-operator/src/operator.rs b/domains/client/domain-operator/src/operator.rs index df1c2d9163..5c38a34b5b 100644 --- a/domains/client/domain-operator/src/operator.rs +++ b/domains/client/domain-operator/src/operator.rs @@ -19,6 +19,7 @@ use sp_domains::{BundleProducerElectionApi, DomainsApi}; use sp_domains_fraud_proof::FraudProofApi; use sp_keystore::KeystorePtr; use sp_messenger::MessengerApi; +use sp_mmr_primitives::MmrApi; use sp_runtime::traits::{Block as BlockT, NumberFor}; use sp_transaction_pool::runtime_api::TaggedTransactionQueue; use std::sync::Arc; @@ -92,7 +93,8 @@ where CClient::Api: DomainsApi + MessengerApi + BundleProducerElectionApi - + FraudProofApi, + + FraudProofApi + + MmrApi>, Backend: sc_client_api::Backend + Send + Sync + 'static, TransactionPool: sc_transaction_pool_api::TransactionPool::Hash> + 'static, diff --git a/domains/client/domain-operator/src/tests.rs b/domains/client/domain-operator/src/tests.rs index ec58720f94..30b3bfc5c1 100644 --- a/domains/client/domain-operator/src/tests.rs +++ b/domains/client/domain-operator/src/tests.rs @@ -1,6 +1,4 @@ -use crate::domain_block_processor::{ - generate_mmr_proof, DomainBlockProcessor, PendingConsensusBlocks, -}; +use crate::domain_block_processor::{DomainBlockProcessor, PendingConsensusBlocks}; use crate::domain_bundle_producer::DomainBundleProducer; use crate::domain_bundle_proposer::DomainBundleProposer; use crate::fraud_proof::{FraudProofGenerator, TraceDiffType}; @@ -17,6 +15,7 @@ use futures::StreamExt; use pallet_messenger::ChainAllowlistUpdate; use sc_client_api::{Backend, BlockBackend, BlockchainEvents, HeaderBackend}; use sc_consensus::SharedBlockImport; +use sc_domains::generate_mmr_proof; use sc_service::{BasePath, Role}; use sc_transaction_pool::error::Error as PoolError; use sc_transaction_pool_api::error::Error as TxPoolError; @@ -34,9 +33,9 @@ use sp_domains::{ InvalidBundleType, Transfers, }; use sp_domains_fraud_proof::fraud_proof::{ - ApplyExtrinsicMismatch, ExecutionPhase, FinalizeBlockMismatch, FraudProof, - InvalidBlockFeesProof, InvalidDomainBlockHashProof, InvalidExtrinsicsRootProof, - InvalidTransfersProof, + ApplyExtrinsicMismatch, ExecutionPhase, FinalizeBlockMismatch, FraudProofVariant, + InvalidBlockFeesProofV2, InvalidDomainBlockHashProofV2, InvalidExtrinsicsRootProofV2, + InvalidTransfersProofV2, }; use sp_domains_fraud_proof::InvalidTransactionCode; use sp_messenger::messages::{CrossDomainMessage, FeeModel, InitiateChannelParams, Proof}; @@ -921,7 +920,7 @@ async fn test_bad_invalid_state_transition_proof_is_rejected() { .expect("we already checked for None above; qed"); let mut fraud_proof = fraud_proof_generator - .generate_invalid_state_transition_proof( + .generate_invalid_state_transition_proof_v2( GENESIS_DOMAIN_ID, execution_phase, &valid_receipt, @@ -956,8 +955,8 @@ async fn test_bad_invalid_state_transition_proof_is_rejected() { ferdie.clear_tx_pool().await.unwrap(); // Modify fraud proof's mismatch index to a higher value and try to submit it. - match &fraud_proof { - FraudProof::InvalidStateTransition(invalid_state_transition_fraud_proof) => { + match &fraud_proof.proof { + FraudProofVariant::InvalidStateTransition(invalid_state_transition_fraud_proof) => { match &invalid_state_transition_fraud_proof.execution_phase { ExecutionPhase::ApplyExtrinsic { extrinsic_proof, @@ -970,7 +969,7 @@ async fn test_bad_invalid_state_transition_proof_is_rejected() { extrinsic_proof: extrinsic_proof.clone(), mismatch: ApplyExtrinsicMismatch::StateRoot(u32::MAX), }; - fraud_proof = FraudProof::InvalidStateTransition( + fraud_proof.proof = FraudProofVariant::InvalidStateTransition( modified_invalid_state_transition_fraud_proof, ); } @@ -983,7 +982,7 @@ async fn test_bad_invalid_state_transition_proof_is_rejected() { ExecutionPhase::FinalizeBlock { mismatch: FinalizeBlockMismatch::Longer(u32::MAX), }; - fraud_proof = FraudProof::InvalidStateTransition( + fraud_proof.proof = FraudProofVariant::InvalidStateTransition( modified_invalid_state_transition_fraud_proof, ); } @@ -1174,7 +1173,7 @@ async fn test_invalid_state_transition_proof_creation_and_verification( // Wait for the fraud proof that target the bad ER let wait_for_fraud_proof_fut = ferdie.wait_for_fraud_proof(move |fp| { - if let FraudProof::InvalidStateTransition(proof) = fp { + if let FraudProofVariant::InvalidStateTransition(proof) = &fp.proof { match (trace_diff_type, mismatch_trace_index) { (TraceDiffType::Mismatch, mismatch_trace_index) => match mismatch_trace_index { 0 => assert!(matches!( @@ -1359,7 +1358,7 @@ async fn test_true_invalid_bundles_inherent_extrinsic_proof_creation_and_verific // Wait for the fraud proof that target the bad ER let wait_for_fraud_proof_fut = ferdie.wait_for_fraud_proof(move |fp| { - if let FraudProof::InvalidBundles(proof) = fp { + if let FraudProofVariant::InvalidBundles(proof) = &fp.proof { if let InvalidBundleType::InherentExtrinsic(_) = proof.invalid_bundle_type { assert!(proof.is_true_invalid_fraud_proof); return true; @@ -1472,7 +1471,7 @@ async fn test_false_invalid_bundles_inherent_extrinsic_proof_creation_and_verifi // Wait for the fraud proof that target the bad ER let wait_for_fraud_proof_fut = ferdie.wait_for_fraud_proof(move |fp| { - if let FraudProof::InvalidBundles(proof) = fp { + if let FraudProofVariant::InvalidBundles(proof) = &fp.proof { if let InvalidBundleType::InherentExtrinsic(_) = proof.invalid_bundle_type { assert!(!proof.is_true_invalid_fraud_proof); return true; @@ -1504,7 +1503,7 @@ async fn test_false_invalid_bundles_inherent_extrinsic_proof_creation_and_verifi } #[tokio::test(flavor = "multi_thread")] -async fn test_invalid_xdm_proof_creation_and_verification() { +async fn test_true_invalid_bundles_illegal_xdm_proof_creation_and_verification() { let directory = TempDir::new().expect("Must be able to create temporary directory"); let mut builder = sc_cli::LoggerBuilder::new(""); @@ -1625,8 +1624,8 @@ async fn test_invalid_xdm_proof_creation_and_verification() { // Wait for the fraud proof that target the bad ER let wait_for_fraud_proof_fut = ferdie.wait_for_fraud_proof(move |fp| { - if let FraudProof::InvalidBundles(proof) = fp { - if let InvalidBundleType::InvalidXDM(extrinsic_index) = proof.invalid_bundle_type { + if let FraudProofVariant::InvalidBundles(proof) = &fp.proof { + if let InvalidBundleType::IllegalTx(extrinsic_index) = proof.invalid_bundle_type { assert!(proof.is_true_invalid_fraud_proof); assert_eq!(extrinsic_index, 0); return true; @@ -1790,7 +1789,7 @@ async fn test_true_invalid_bundles_illegal_extrinsic_proof_creation_and_verifica // Wait for the fraud proof that target the bad ER let wait_for_fraud_proof_fut = ferdie.wait_for_fraud_proof(move |fp| { - if let FraudProof::InvalidBundles(proof) = fp { + if let FraudProofVariant::InvalidBundles(proof) = &fp.proof { if let InvalidBundleType::IllegalTx(extrinsic_index) = proof.invalid_bundle_type { assert!(proof.is_true_invalid_fraud_proof); assert_eq!(extrinsic_index, 2); @@ -1923,7 +1922,7 @@ async fn test_false_invalid_bundles_illegal_extrinsic_proof_creation_and_verific // Wait for the fraud proof that target the bad ER let wait_for_fraud_proof_fut = ferdie.wait_for_fraud_proof(move |fp| { - if let FraudProof::InvalidBundles(proof) = fp { + if let FraudProofVariant::InvalidBundles(proof) = &fp.proof { if let InvalidBundleType::IllegalTx(extrinsic_index) = proof.invalid_bundle_type { assert!(!proof.is_true_invalid_fraud_proof); assert_eq!(extrinsic_index, 1); @@ -1955,122 +1954,6 @@ async fn test_false_invalid_bundles_illegal_extrinsic_proof_creation_and_verific assert!(!ferdie.does_receipt_exist(bad_receipt_hash).unwrap()); } -#[tokio::test(flavor = "multi_thread")] -async fn test_false_invalid_xdm_extrinsic_proof_creation_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")), - ); - - // 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; - - let bundle_to_tx = |opaque_bundle| { - subspace_test_runtime::UncheckedExtrinsic::new_unsigned( - pallet_domains::Call::submit_bundle { opaque_bundle }.into(), - ) - .into() - }; - - produce_blocks!(ferdie, alice, 5).await.unwrap(); - - // transfer some balance from alice - let alice_balance = alice.free_balance(Alice.to_account_id()); - let alice_nonce = alice.account_nonce(); - - let transfer_to_charlie_with_tip = alice.construct_extrinsic_with_tip( - alice_nonce, - alice_balance / 3, - pallet_balances::Call::transfer_allow_death { - dest: Charlie.to_account_id(), - value: 1, - }, - ); - - alice - .send_extrinsic(transfer_to_charlie_with_tip) - .await - .expect("Failed to send extrinsic"); - - // Produce a bundle that contains the previously sent extrinsic and record that bundle for later use - let (slot, target_bundle) = ferdie.produce_slot_and_wait_for_bundle_submission().await; - assert_eq!(target_bundle.extrinsics.len(), 1); - let bundle_extrinsic_root = target_bundle.extrinsics_root(); - produce_block_with!(ferdie.produce_block_with_slot(slot), alice) - .await - .unwrap(); - - // produce another bundle that marks the previous valid extrinsic as invalid. - let (slot, mut opaque_bundle) = ferdie.produce_slot_and_wait_for_bundle_submission().await; - - let (bad_receipt_hash, bad_submit_bundle_tx) = { - let bad_receipt = &mut opaque_bundle.sealed_header.header.receipt; - // bad receipt marks this particular bundle as invalid even though the call is not XDM - bad_receipt.inboxed_bundles = vec![InboxedBundle::invalid( - InvalidBundleType::InvalidXDM(0), - bundle_extrinsic_root, - )]; - - opaque_bundle.sealed_header.signature = Sr25519Keyring::Alice - .pair() - .sign(opaque_bundle.sealed_header.pre_hash().as_ref()) - .into(); - ( - opaque_bundle.receipt().hash::(), - bundle_to_tx(opaque_bundle), - ) - }; - - // Wait for the fraud proof that target the bad ER - let wait_for_fraud_proof_fut = ferdie.wait_for_fraud_proof(move |fp| { - if let FraudProof::InvalidBundles(proof) = fp { - if let InvalidBundleType::InvalidXDM(extrinsic_index) = proof.invalid_bundle_type { - assert!(!proof.is_true_invalid_fraud_proof); - assert_eq!(extrinsic_index, 0); - return true; - } - } - false - }); - - // Produce a consensus block that contains the `bad_submit_bundle_tx` and the bad receipt should - // be added to the consensus chain block tree - produce_block_with!( - ferdie.produce_block_with_slot_at( - slot, - ferdie.client.info().best_hash, - Some(vec![bad_submit_bundle_tx]) - ), - alice - ) - .await - .unwrap(); - assert!(ferdie.does_receipt_exist(bad_receipt_hash).unwrap()); - - let _ = wait_for_fraud_proof_fut.await; - - // Produce a consensus block that contains the fraud proof, the fraud proof wil be verified - // and executed, thus pruned the bad receipt from the block tree - ferdie.produce_blocks(1).await.unwrap(); - assert!(!ferdie.does_receipt_exist(bad_receipt_hash).unwrap()); -} - #[tokio::test(flavor = "multi_thread")] async fn test_invalid_block_fees_proof_creation() { let directory = TempDir::new().expect("Must be able to create temporary directory"); @@ -2139,8 +2022,8 @@ async fn test_invalid_block_fees_proof_creation() { // Wait for the fraud proof that target the bad ER let wait_for_fraud_proof_fut = ferdie.wait_for_fraud_proof(move |fp| { matches!( - fp, - FraudProof::InvalidBlockFees(InvalidBlockFeesProof { .. }) + fp.proof, + FraudProofVariant::InvalidBlockFees(InvalidBlockFeesProofV2 { .. }) ) }); @@ -2241,8 +2124,8 @@ async fn test_invalid_transfers_fraud_proof() { // Wait for the fraud proof that target the bad ER let wait_for_fraud_proof_fut = ferdie.wait_for_fraud_proof(move |fp| { matches!( - fp, - FraudProof::InvalidTransfers(InvalidTransfersProof { .. }) + fp.proof, + FraudProofVariant::InvalidTransfers(InvalidTransfersProofV2 { .. }) ) }); @@ -2338,8 +2221,8 @@ async fn test_invalid_domain_block_hash_proof_creation() { // Wait for the fraud proof that target the bad ER let wait_for_fraud_proof_fut = ferdie.wait_for_fraud_proof(move |fp| { matches!( - fp, - FraudProof::InvalidDomainBlockHash(InvalidDomainBlockHashProof { .. }) + fp.proof, + FraudProofVariant::InvalidDomainBlockHash(InvalidDomainBlockHashProofV2 { .. }) ) }); @@ -2435,8 +2318,8 @@ async fn test_invalid_domain_extrinsics_root_proof_creation() { // Wait for the fraud proof that target the bad ER let wait_for_fraud_proof_fut = ferdie.wait_for_fraud_proof(move |fp| { matches!( - fp, - FraudProof::InvalidExtrinsicsRoot(InvalidExtrinsicsRootProof { .. }) + fp.proof, + FraudProofVariant::InvalidExtrinsicsRoot(InvalidExtrinsicsRootProofV2 { .. }) ) }); @@ -2464,97 +2347,6 @@ async fn test_invalid_domain_extrinsics_root_proof_creation() { assert!(!ferdie.does_receipt_exist(bad_receipt_hash).unwrap()); } -#[tokio::test(flavor = "multi_thread")] -async fn test_bundle_equivocation_fraud_proof() { - 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")), - ); - - // Run Alice (a evm domain authority node) - let 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; - - produce_blocks!(ferdie, alice, 3).await.unwrap(); - - let bundle_to_tx = |opaque_bundle| { - subspace_test_runtime::UncheckedExtrinsic::new_unsigned( - pallet_domains::Call::submit_bundle { opaque_bundle }.into(), - ) - .into() - }; - - let (slot, mut opaque_bundle) = ferdie.produce_slot_and_wait_for_bundle_submission().await; - let original_submit_bundle_tx = bundle_to_tx(opaque_bundle.clone()); - - // Remove the original bundle submission and resubmit it again. - // This is done since when the bundle is submitted through offchain transaction submission - // the validation is skipped for local transactions and this bundle slot is not stored in the Aux storage - // so when we resubmit the transaction through `submit_transaction`, it will go through validation - // process and the first bundle after validating will go through the validation and Aux storage - // updated. When the equivocated bundle is submitted next, the Aux storage is used to check equivocation. - // - // In the production behaviour will not cause any issues, since we trust the local transactions - // and will only check equivocations for the transactions coming from the network. - ferdie - .prune_tx_from_pool(&original_submit_bundle_tx) - .await - .unwrap(); - assert!(ferdie.get_bundle_from_tx_pool(slot).is_none()); - - ferdie - .submit_transaction(original_submit_bundle_tx) - .await - .unwrap(); - - // change the bundle contents such that we derive a new bundle - // with same slot and proof of election such that this leads to bundle equivocation. - let equivocated_bundle_tx = { - let receipt = &mut opaque_bundle.sealed_header.header.receipt; - receipt.domain_block_extrinsic_root = Default::default(); - opaque_bundle.sealed_header.signature = Sr25519Keyring::Alice - .pair() - .sign(opaque_bundle.sealed_header.pre_hash().as_ref()) - .into(); - bundle_to_tx(opaque_bundle) - }; - - let wait_for_fraud_proof_fut = - ferdie.wait_for_fraud_proof(move |fp| matches!(fp, FraudProof::BundleEquivocation(_))); - - match ferdie - .submit_transaction(equivocated_bundle_tx) - .await - .unwrap_err() - { - sc_transaction_pool::error::Error::Pool( - sc_transaction_pool_api::error::Error::InvalidTransaction(_), - ) => {} - e => panic!("Unexpected error while submitting fraud proof: {e}"), - } - - let _ = wait_for_fraud_proof_fut.await; - - // 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")] async fn test_domain_block_builder_include_ext_with_failed_execution() { let directory = TempDir::new().expect("Must be able to create temporary directory"); @@ -2799,12 +2591,9 @@ async fn test_valid_bundle_proof_generation_and_verification() { ) .into() }; - let proof_to_tx = |proof| { + let proof_to_tx = |fraud_proof| { subspace_test_runtime::UncheckedExtrinsic::new_unsigned( - pallet_domains::Call::submit_fraud_proof { - fraud_proof: Box::new(FraudProof::ValidBundle(proof)), - } - .into(), + pallet_domains::Call::submit_fraud_proof { fraud_proof }.into(), ) .into() }; @@ -2858,26 +2647,27 @@ async fn test_valid_bundle_proof_generation_and_verification() { pallet_domains::Call::submit_fraud_proof { fraud_proof }, ) = ext.function { - if let FraudProof::ValidBundle(proof) = *fraud_proof { + if let FraudProofVariant::ValidBundle(ref proof) = fraud_proof.proof { // The fraud proof is targetting the `bad_receipt` assert_eq!( - proof.bad_receipt_hash, + fraud_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(); + let mut bad_fraud_proof = fraud_proof.clone(); + bad_fraud_proof.bad_receipt_hash = H256::random(); assert!(ferdie - .submit_transaction(proof_to_tx(bad_proof)) + .submit_transaction(proof_to_tx(bad_fraud_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; + let (mut bad_fraud_proof, mut bad_proof) = (fraud_proof.clone(), proof.clone()); + bad_proof.bundle_with_proof.bundle_index = u32::MAX; + bad_fraud_proof.proof = FraudProofVariant::ValidBundle(bad_proof); assert!(ferdie - .submit_transaction(proof_to_tx(bad_proof)) + .submit_transaction(proof_to_tx(bad_fraud_proof)) .await .is_err()); @@ -4288,9 +4078,9 @@ async fn test_bad_receipt_chain() { // Wait for a fraud proof that target the first bad ER let wait_for_fraud_proof_fut = ferdie.wait_for_fraud_proof(move |fp| { matches!( - fp, - FraudProof::InvalidDomainBlockHash(InvalidDomainBlockHashProof { .. }) - ) && fp.targeted_bad_receipt_hash() == Some(bad_receipt_hash) + fp.proof, + FraudProofVariant::InvalidDomainBlockHash(InvalidDomainBlockHashProofV2 { .. }) + ) && fp.targeted_bad_receipt_hash() == bad_receipt_hash }); // Produce more bundle with bad ER that use previous bad ER as parent @@ -4572,6 +4362,7 @@ async fn test_handle_duplicated_tx_with_diff_nonce_in_previous_bundle() { assert_eq!(alice.account_nonce(), nonce + 3); } +// TODO: add test to ensure MMR proof from diff fork wil be rejected #[tokio::test(flavor = "multi_thread")] async fn test_verify_mmr_proof_stateless() { use subspace_test_primitives::OnchainStateApi as _; @@ -4621,11 +4412,9 @@ async fn test_verify_mmr_proof_stateless() { let res = ferdie .client .runtime_api() - .verify_proof_and_extract_consensus_state_root( - ferdie.client.info().best_hash, - proof.clone(), - ) - .unwrap(); + .verify_proof_and_extract_leaf(ferdie.client.info().best_hash, proof.clone()) + .unwrap() + .map(|leaf| leaf.state_root()); produce_blocks!(ferdie, alice, 1).await.unwrap(); @@ -4643,11 +4432,9 @@ async fn test_verify_mmr_proof_stateless() { let res = ferdie .client .runtime_api() - .verify_proof_and_extract_consensus_state_root( - ferdie.client.info().best_hash, - proof.clone(), - ) - .unwrap(); + .verify_proof_and_extract_leaf(ferdie.client.info().best_hash, proof.clone()) + .unwrap() + .map(|leaf| leaf.state_root()); assert_eq!(res, Some(expected_state_root)); produce_blocks!(ferdie, alice, 1).await.unwrap(); diff --git a/domains/pallets/messenger/src/lib.rs b/domains/pallets/messenger/src/lib.rs index 49ef7209ad..f753eba642 100644 --- a/domains/pallets/messenger/src/lib.rs +++ b/domains/pallets/messenger/src/lib.rs @@ -1149,10 +1149,10 @@ mod pallet { // nonce should be either be next or in future. ensure!(xdm.nonce >= next_nonce, InvalidTransaction::Call); - let state_root = T::MmrProofVerifier::verify_proof_and_extract_consensus_state_root( - xdm.proof.consensus_mmr_proof(), - ) - .ok_or(InvalidTransaction::BadProof)?; + let state_root = + T::MmrProofVerifier::verify_proof_and_extract_leaf(xdm.proof.consensus_mmr_proof()) + .ok_or(InvalidTransaction::BadProof)? + .state_root(); // if the message is from domain, verify domain confirmation proof let state_root = if let Some(domain_proof) = xdm.proof.domain_proof().clone() diff --git a/domains/runtime/auto-id/src/lib.rs b/domains/runtime/auto-id/src/lib.rs index 5d2c5d545b..21f30ac013 100644 --- a/domains/runtime/auto-id/src/lib.rs +++ b/domains/runtime/auto-id/src/lib.rs @@ -321,9 +321,9 @@ type MmrHash = ::Output; pub struct MmrProofVerifier; impl sp_subspace_mmr::MmrProofVerifier, Hash> for MmrProofVerifier { - fn verify_proof_and_extract_consensus_state_root( + fn verify_proof_and_extract_leaf( mmr_leaf_proof: ConsensusChainMmrLeafProof, Hash, MmrHash>, - ) -> Option { + ) -> Option> { let ConsensusChainMmrLeafProof { opaque_mmr_leaf: opaque_leaf, proof, @@ -332,9 +332,9 @@ impl sp_subspace_mmr::MmrProofVerifier, Hash> for MmrP let leaf: MmrLeaf = opaque_leaf.into_opaque_leaf().try_decode()?; - let state_root = leaf.state_root(); + verify_mmr_proof(vec![EncodableOpaqueLeaf::from_leaf(&leaf)], proof.encode()) - .then_some(state_root) + .then_some(leaf) } } diff --git a/domains/runtime/evm/src/lib.rs b/domains/runtime/evm/src/lib.rs index 639968f579..a5c83be475 100644 --- a/domains/runtime/evm/src/lib.rs +++ b/domains/runtime/evm/src/lib.rs @@ -450,9 +450,9 @@ type MmrHash = ::Output; pub struct MmrProofVerifier; impl sp_subspace_mmr::MmrProofVerifier, Hash> for MmrProofVerifier { - fn verify_proof_and_extract_consensus_state_root( + fn verify_proof_and_extract_leaf( mmr_leaf_proof: ConsensusChainMmrLeafProof, Hash, MmrHash>, - ) -> Option { + ) -> Option> { let ConsensusChainMmrLeafProof { opaque_mmr_leaf: opaque_leaf, proof, @@ -461,9 +461,9 @@ impl sp_subspace_mmr::MmrProofVerifier, Hash> for MmrP let leaf: MmrLeaf = opaque_leaf.into_opaque_leaf().try_decode()?; - let state_root = leaf.state_root(); + verify_mmr_proof(vec![EncodableOpaqueLeaf::from_leaf(&leaf)], proof.encode()) - .then_some(state_root) + .then_some(leaf) } } diff --git a/domains/test/runtime/evm/src/lib.rs b/domains/test/runtime/evm/src/lib.rs index f4e57ccd14..3ad492455a 100644 --- a/domains/test/runtime/evm/src/lib.rs +++ b/domains/test/runtime/evm/src/lib.rs @@ -436,9 +436,9 @@ type MmrHash = ::Output; pub struct MmrProofVerifier; impl sp_subspace_mmr::MmrProofVerifier, Hash> for MmrProofVerifier { - fn verify_proof_and_extract_consensus_state_root( + fn verify_proof_and_extract_leaf( mmr_leaf_proof: ConsensusChainMmrLeafProof, Hash, MmrHash>, - ) -> Option { + ) -> Option> { let ConsensusChainMmrLeafProof { opaque_mmr_leaf: opaque_leaf, proof, @@ -447,9 +447,9 @@ impl sp_subspace_mmr::MmrProofVerifier, Hash> for MmrP let leaf: MmrLeaf = opaque_leaf.into_opaque_leaf().try_decode()?; - let state_root = leaf.state_root(); + verify_mmr_proof(vec![EncodableOpaqueLeaf::from_leaf(&leaf)], proof.encode()) - .then_some(state_root) + .then_some(leaf) } } diff --git a/test/subspace-test-primitives/src/lib.rs b/test/subspace-test-primitives/src/lib.rs index 7a6151aab0..8271c81bf1 100644 --- a/test/subspace-test-primitives/src/lib.rs +++ b/test/subspace-test-primitives/src/lib.rs @@ -5,7 +5,7 @@ use codec::{Decode, Encode}; use sp_core::H256; use sp_messenger::messages::{ChainId, ChannelId}; use sp_runtime::traits::NumberFor; -use sp_subspace_mmr::ConsensusChainMmrLeafProof; +use sp_subspace_mmr::{ConsensusChainMmrLeafProof, MmrLeaf}; sp_api::decl_runtime_apis! { /// Api for querying onchain state in the test @@ -21,6 +21,6 @@ sp_api::decl_runtime_apis! { fn get_open_channel_for_chain(dst_chain_id: ChainId) -> Option; /// Verify the mmr proof statelessly and extract the state root. - fn verify_proof_and_extract_consensus_state_root(proof: ConsensusChainMmrLeafProof, Block::Hash, H256>) -> Option; + fn verify_proof_and_extract_leaf(proof: ConsensusChainMmrLeafProof, Block::Hash, H256>) -> Option, Block::Hash>>; } } diff --git a/test/subspace-test-runtime/src/lib.rs b/test/subspace-test-runtime/src/lib.rs index 55a9aa93dc..00a18fc784 100644 --- a/test/subspace-test-runtime/src/lib.rs +++ b/test/subspace-test-runtime/src/lib.rs @@ -60,7 +60,7 @@ use sp_domains::{ OperatorPublicKey, StakingHoldIdentifier, DOMAIN_STORAGE_FEE_MULTIPLIER, INITIAL_DOMAIN_TX_RANGE, }; -use sp_domains_fraud_proof::fraud_proof::FraudProof; +use sp_domains_fraud_proof::fraud_proof::FraudProofV2; use sp_domains_fraud_proof::storage_proof::{ FraudProofStorageKeyProvider, FraudProofStorageKeyRequest, }; @@ -544,9 +544,9 @@ parameter_types! { pub struct MmrProofVerifier; impl sp_subspace_mmr::MmrProofVerifier, Hash> for MmrProofVerifier { - fn verify_proof_and_extract_consensus_state_root( + fn verify_proof_and_extract_leaf( mmr_leaf_proof: ConsensusChainMmrLeafProof, Hash, mmr::Hash>, - ) -> Option { + ) -> Option { let ConsensusChainMmrLeafProof { consensus_block_number, opaque_mmr_leaf, @@ -567,7 +567,7 @@ impl sp_subspace_mmr::MmrProofVerifier, Hash> for Mm let leaf: mmr::Leaf = opaque_mmr_leaf.into_opaque_leaf().try_decode()?; - Some(leaf.state_root()) + Some(leaf) } } @@ -743,6 +743,9 @@ impl pallet_domains::Config for Runtime { type DomainBundleSubmitted = Messenger; type OnDomainInstantiated = Messenger; type Balance = Balance; + type MmrHash = mmr::Hash; + type MmrProofVerifier = MmrProofVerifier; + type FraudProofStorageKeyProvider = StorageKeyProvider; } parameter_types! { @@ -1065,25 +1068,6 @@ fn extract_bundle( } } -pub(crate) fn extract_fraud_proofs( - domain_id: DomainId, - extrinsics: Vec, -) -> Vec, Hash, DomainHeader>> { - let successful_fraud_proofs = Domains::successful_fraud_proofs(domain_id); - extrinsics - .into_iter() - .filter_map(|uxt| match uxt.function { - RuntimeCall::Domains(pallet_domains::Call::submit_fraud_proof { fraud_proof }) - if fraud_proof.domain_id() == domain_id - && successful_fraud_proofs.contains(&fraud_proof.hash()) => - { - Some(*fraud_proof) - } - _ => None, - }) - .collect() -} - struct RewardAddress([u8; 32]); impl From for RewardAddress { @@ -1425,6 +1409,10 @@ impl_runtime_apis! { fn storage_fund_account_balance(operator_id: OperatorId) -> Balance { Domains::storage_fund_account_balance(operator_id) } + + fn is_domain_runtime_updraded_since(domain_id: DomainId, at: NumberFor) -> Option { + Domains::is_domain_runtime_updraded_since(domain_id, at) + } } impl sp_domains::BundleProducerElectionApi for Runtime { @@ -1523,17 +1511,10 @@ impl_runtime_apis! { } impl sp_domains_fraud_proof::FraudProofApi for Runtime { - fn submit_fraud_proof_unsigned(fraud_proof: FraudProof, ::Hash, DomainHeader>) { + fn submit_fraud_proof_unsigned(fraud_proof: FraudProofV2, ::Hash, DomainHeader, H256>) { Domains::submit_fraud_proof_unsigned(fraud_proof) } - fn extract_fraud_proofs( - domain_id: DomainId, - extrinsics: Vec<::Extrinsic>, - ) -> Vec, ::Hash, DomainHeader>> { - extract_fraud_proofs(domain_id, extrinsics) - } - fn fraud_proof_storage_key(req: FraudProofStorageKeyRequest) -> Vec { ::storage_key(req) } @@ -1594,8 +1575,8 @@ impl_runtime_apis! { Messenger::get_open_channel_for_chain(dst_chain_id).map(|(c, _)| c) } - fn verify_proof_and_extract_consensus_state_root(mmr_leaf_proof: ConsensusChainMmrLeafProof, ::Hash, H256>) -> Option { - >::verify_proof_and_extract_consensus_state_root(mmr_leaf_proof) + fn verify_proof_and_extract_leaf(mmr_leaf_proof: ConsensusChainMmrLeafProof, ::Hash, H256>) -> Option { + >::verify_proof_and_extract_leaf(mmr_leaf_proof) } } diff --git a/test/subspace-test-service/src/lib.rs b/test/subspace-test-service/src/lib.rs index ac11d26668..262dfc1878 100644 --- a/test/subspace-test-service/src/lib.rs +++ b/test/subspace-test-service/src/lib.rs @@ -63,7 +63,7 @@ use sp_core::offchain::OffchainDbExt; use sp_core::traits::{CodeExecutor, SpawnEssentialNamed}; use sp_core::{Get, H256}; use sp_domains::{BundleProducerElectionApi, ChainId, DomainsApi, OpaqueBundle}; -use sp_domains_fraud_proof::fraud_proof::FraudProof; +use sp_domains_fraud_proof::fraud_proof::FraudProofV2; use sp_domains_fraud_proof::{FraudProofExtension, FraudProofHostFunctionsImpl}; use sp_externalities::Extensions; use sp_inherents::{InherentData, InherentDataProvider}; @@ -82,7 +82,6 @@ use std::collections::HashMap; use std::error::Error; use std::marker::PhantomData; use std::pin::Pin; -use std::sync::atomic::AtomicU32; use std::sync::Arc; use std::time; use subspace_core_primitives::{PotOutput, Solution}; @@ -98,8 +97,8 @@ use subspace_test_runtime::{ use substrate_frame_rpc_system::AccountNonceApi; use substrate_test_client::{RpcHandlersExt, RpcTransactionError, RpcTransactionOutput}; -type FraudProofFor = - FraudProof, ::Hash, ::Header>; +type FraudProofV2For = + FraudProofV2, ::Hash, ::Header, H256>; const MAX_PRODUCE_BUNDLE_TRY: usize = 10; @@ -423,14 +422,12 @@ impl MockConsensusNode { .state_pruning .clone() .unwrap_or(PruningMode::ArchiveCanonical); - let sync_target_block_number = Arc::new(AtomicU32::new(0)); let transaction_pool = subspace_service::transaction_pool::new_full( config.transaction_pool.clone(), config.role.is_authority(), config.prometheus_registry(), &task_manager, client.clone(), - sync_target_block_number.clone(), ) .expect("failed to create transaction pool"); @@ -800,7 +797,7 @@ impl MockConsensusNode { fraud_proof_predict: FP, ) -> Pin + Send>> where - FP: Fn(&FraudProofFor) -> bool + Send + 'static, + FP: Fn(&FraudProofV2For) -> bool + Send + 'static, { let tx_pool = self.transaction_pool.clone(); let mut import_tx_stream = self.transaction_pool.import_notification_stream();