From 694fa7323e0c0e0c5581c5274d5fe58c72a5186e Mon Sep 17 00:00:00 2001 From: vedhavyas Date: Wed, 1 Nov 2023 13:43:47 +0100 Subject: [PATCH 1/8] add infra to check bundle equivocation --- Cargo.lock | 1 + crates/sp-domains-fraud-proof/Cargo.toml | 1 + .../src/bundle_equivocation.rs | 247 ++++++++++++++++++ .../sp-domains-fraud-proof/src/fraud_proof.rs | 4 +- crates/sp-domains-fraud-proof/src/lib.rs | 2 + 5 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 crates/sp-domains-fraud-proof/src/bundle_equivocation.rs diff --git a/Cargo.lock b/Cargo.lock index 263df095fd..96b7e04757 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10747,6 +10747,7 @@ dependencies = [ "subspace-runtime-primitives", "subspace-test-client", "subspace-test-service", + "substrate-test-runtime-client", "tempfile", "thiserror", "tokio", diff --git a/crates/sp-domains-fraud-proof/Cargo.toml b/crates/sp-domains-fraud-proof/Cargo.toml index 7caefc15fb..caf77bf6f4 100644 --- a/crates/sp-domains-fraud-proof/Cargo.toml +++ b/crates/sp-domains-fraud-proof/Cargo.toml @@ -48,6 +48,7 @@ sc-service = { version = "0.10.0-dev", git = "https://github.com/subspace/polkad 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" } +substrate-test-runtime-client = { version = "2.0.0", git = "https://github.com/subspace/polkadot-sdk", rev = "892bf8e938c6bd2b893d3827d1093cd81baa59a1" } tempfile = "3.8.0" tokio = "1.32.0" diff --git a/crates/sp-domains-fraud-proof/src/bundle_equivocation.rs b/crates/sp-domains-fraud-proof/src/bundle_equivocation.rs new file mode 100644 index 0000000000..2593f69770 --- /dev/null +++ b/crates/sp-domains-fraud-proof/src/bundle_equivocation.rs @@ -0,0 +1,247 @@ +//! 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_api::{BlockT, HeaderT}; +use sp_blockchain::{Error as ClientError, Result as ClientResult}; +use sp_consensus_slots::Slot; +use sp_domains::SealedBundleHeader; +use sp_runtime::traits::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"; + +/// 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. +#[allow(dead_code)] +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() { + // A proof of equivocation consists of two headers: + // 1) signed by the same operator, + if previous_bundle_header.header.proof_of_election.operator_id + == bundle_header.header.proof_of_election.operator_id + { + // 2) with different hash + return if bundle_header.hash() != previous_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 sp_core::crypto::UncheckedFrom; + use sp_domains::{ + BundleHeader, DomainId, ExecutionReceipt, OperatorId, OperatorSignature, ProofOfElection, + SealedBundleHeader, + }; + 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(), + total_rewards: 0, + }, + bundle_size: 0, + estimated_bundle_weight: Default::default(), + bundle_extrinsics_root: Default::default(), + }, + signature: OperatorSignature::unchecked_from([0u8; 64]), + } + } + + #[test] + fn test_check_equivocation() { + let client = Arc::new(substrate_test_runtime_client::new()); + 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 d2f7be3abd..952db58be5 100644 --- a/crates/sp-domains-fraud-proof/src/fraud_proof.rs +++ b/crates/sp-domains-fraud-proof/src/fraud_proof.rs @@ -12,7 +12,7 @@ use sp_runtime::traits::{Block as BlockT, Hash as HashT, Header as HeaderT, Numb use sp_runtime::{Digest, DigestItem}; use sp_std::vec::Vec; use sp_trie::StorageProof; -use subspace_runtime_primitives::{AccountId, Balance}; +use subspace_runtime_primitives::Balance; use trie_db::TrieLayout; type ExecutionReceiptFor = ExecutionReceipt< @@ -475,8 +475,6 @@ pub fn dummy_invalid_state_transition_proof( pub struct BundleEquivocationProof { /// The id of the domain this fraud proof targeted pub domain_id: DomainId, - /// The authority id of the equivocator. - pub offender: AccountId, /// The slot at which the equivocation happened. pub slot: Slot, // TODO: The generic type should be `` diff --git a/crates/sp-domains-fraud-proof/src/lib.rs b/crates/sp-domains-fraud-proof/src/lib.rs index 676c1dbc23..0d10d759e7 100644 --- a/crates/sp-domains-fraud-proof/src/lib.rs +++ b/crates/sp-domains-fraud-proof/src/lib.rs @@ -17,6 +17,8 @@ //! Subspace fraud proof primitives for consensus chain. #![cfg_attr(not(feature = "std"), no_std)] +#[cfg(feature = "std")] +mod bundle_equivocation; #[cfg(feature = "std")] pub mod execution_prover; pub mod fraud_proof; From 2420575076f358878b7f692d0899a62d6fd8ff9a Mon Sep 17 00:00:00 2001 From: vedhavyas Date: Thu, 2 Nov 2023 12:18:29 +0100 Subject: [PATCH 2/8] re-introduce custom transaction pool for consensus node and check bundle equivocation for validated bundles. Substrate does not implement LocaltransactionPool for Basic pool over generic ChainApi but instead impl on Substrate's FullchainApi. This essentially makes us to reimplement BasicPoolWrapper which is quiet unnecessary :( --- .../src/bundle_equivocation.rs | 1 - crates/sp-domains-fraud-proof/src/lib.rs | 16 +- crates/sp-domains/src/lib.rs | 4 + crates/subspace-runtime/src/domains.rs | 14 + crates/subspace-runtime/src/lib.rs | 20 +- crates/subspace-service/src/lib.rs | 36 +- .../subspace-service/src/transaction_pool.rs | 554 ++++++++++++++++++ test/subspace-test-runtime/src/lib.rs | 28 + test/subspace-test-service/src/lib.rs | 18 +- 9 files changed, 664 insertions(+), 27 deletions(-) create mode 100644 crates/subspace-service/src/transaction_pool.rs diff --git a/crates/sp-domains-fraud-proof/src/bundle_equivocation.rs b/crates/sp-domains-fraud-proof/src/bundle_equivocation.rs index 2593f69770..f751250385 100644 --- a/crates/sp-domains-fraud-proof/src/bundle_equivocation.rs +++ b/crates/sp-domains-fraud-proof/src/bundle_equivocation.rs @@ -42,7 +42,6 @@ pub type CheckEquivocationResult = /// 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. -#[allow(dead_code)] pub fn check_equivocation( backend: &Arc, slot_now: Slot, diff --git a/crates/sp-domains-fraud-proof/src/lib.rs b/crates/sp-domains-fraud-proof/src/lib.rs index 0d10d759e7..4d31094e66 100644 --- a/crates/sp-domains-fraud-proof/src/lib.rs +++ b/crates/sp-domains-fraud-proof/src/lib.rs @@ -18,7 +18,7 @@ #![cfg_attr(not(feature = "std"), no_std)] #[cfg(feature = "std")] -mod bundle_equivocation; +pub mod bundle_equivocation; #[cfg(feature = "std")] pub mod execution_prover; pub mod fraud_proof; @@ -29,6 +29,7 @@ mod runtime_interface; mod tests; pub mod verification; +use crate::fraud_proof::FraudProof; use codec::{Decode, Encode}; #[cfg(feature = "std")] pub use host_functions::{ @@ -38,7 +39,9 @@ pub use runtime_interface::fraud_proof_runtime_interface; #[cfg(feature = "std")] pub use runtime_interface::fraud_proof_runtime_interface::HostFunctions; use sp_api::scale_info::TypeInfo; +use sp_api::HeaderT; use sp_domains::DomainId; +use sp_runtime::traits::NumberFor; use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidity}; use sp_runtime::OpaqueExtrinsic; use sp_runtime_interface::pass_by; @@ -49,7 +52,7 @@ use subspace_core_primitives::Randomness; /// Custom invalid validity code for the extrinsics in pallet-domains. #[repr(u8)] pub enum InvalidTransactionCode { - BundleEquivocationProof = 101, + BundleEquivocation = 101, TransactionProof = 102, ExecutionReceipt = 103, Bundle = 104, @@ -190,3 +193,12 @@ impl FraudProofVerificationInfoResponse { } } } + +sp_api::decl_runtime_apis! { + pub trait FraudProofsApi { + /// Submits the fraud proof via an unsigned extrinsic. + fn submit_fraud_proof_unsigned( + fraud_proof: FraudProof, Block::Hash, DomainHeader>, + ); + } +} diff --git a/crates/sp-domains/src/lib.rs b/crates/sp-domains/src/lib.rs index 6476f5019d..fef7d5c8fb 100644 --- a/crates/sp-domains/src/lib.rs +++ b/crates/sp-domains/src/lib.rs @@ -888,6 +888,10 @@ sp_api::decl_runtime_apis! { extrinsics: Vec, ) -> OpaqueBundles; + /// Extract bundle from the extrinsic if the extrinsic is `submit_bundle`. + #[api_version(2)] + fn extract_bundle(extrinsic: Block::Extrinsic) -> Option, Block::Hash, DomainHeader, Balance>>; + /// Generates a randomness seed for extrinsics shuffling. fn extrinsics_shuffling_seed() -> Randomness; diff --git a/crates/subspace-runtime/src/domains.rs b/crates/subspace-runtime/src/domains.rs index a75ac75aff..13928207d7 100644 --- a/crates/subspace-runtime/src/domains.rs +++ b/crates/subspace-runtime/src/domains.rs @@ -1,5 +1,6 @@ use crate::{Balance, Block, Domains, RuntimeCall, UncheckedExtrinsic}; use domain_runtime_primitives::opaque::Header as DomainHeader; +use sp_api::{BlockT, NumberFor}; use sp_domains::DomainId; use sp_std::vec::Vec; @@ -21,3 +22,16 @@ pub(crate) fn extract_successful_bundles( }) .collect() } + +pub(crate) fn extract_bundle( + extrinsic: UncheckedExtrinsic, +) -> Option< + sp_domains::OpaqueBundle, ::Hash, DomainHeader, Balance>, +> { + match extrinsic.function { + RuntimeCall::Domains(pallet_domains::Call::submit_bundle { opaque_bundle }) => { + Some(opaque_bundle) + } + _ => None, + } +} diff --git a/crates/subspace-runtime/src/lib.rs b/crates/subspace-runtime/src/lib.rs index abd1a14d42..33a813eae7 100644 --- a/crates/subspace-runtime/src/lib.rs +++ b/crates/subspace-runtime/src/lib.rs @@ -58,9 +58,10 @@ use sp_core::storage::StateVersion; use sp_core::{OpaqueMetadata, H256}; use sp_domains::bundle_producer_election::BundleProducerElectionParams; use sp_domains::{ - DomainId, DomainInstanceData, DomainsHoldIdentifier, ExecutionReceipt, OperatorId, - OperatorPublicKey, StakingHoldIdentifier, + DomainId, DomainInstanceData, DomainsHoldIdentifier, ExecutionReceipt, OpaqueBundle, + OperatorId, OperatorPublicKey, StakingHoldIdentifier, }; +use sp_domains_fraud_proof::fraud_proof::FraudProof; use sp_messenger::endpoint::{Endpoint, EndpointHandler as EndpointHandlerT, EndpointId}; use sp_messenger::messages::{ BlockInfo, BlockMessagesWithStorageKey, ChainId, CrossDomainMessage, @@ -970,6 +971,7 @@ impl_runtime_apis! { } } + #[api_version(2)] impl sp_domains::DomainsApi for Runtime { fn submit_bundle_unsigned( opaque_bundle: sp_domains::OpaqueBundle, ::Hash, DomainHeader, Balance>, @@ -984,6 +986,12 @@ impl_runtime_apis! { crate::domains::extract_successful_bundles(domain_id, extrinsics) } + fn extract_bundle( + extrinsic: ::Extrinsic + ) -> Option, ::Hash, DomainHeader, Balance>> { + crate::domains::extract_bundle(extrinsic) + } + fn extrinsics_shuffling_seed() -> Randomness { Randomness::from(Domains::extrinsics_shuffling_seed().to_fixed_bytes()) } @@ -1045,6 +1053,14 @@ impl_runtime_apis! { } } + impl sp_domains_fraud_proof::FraudProofsApi for Runtime { + fn submit_fraud_proof_unsigned( + fraud_proof: FraudProof, ::Hash, DomainHeader>, + ){ + Domains::submit_fraud_proof_unsigned(fraud_proof) + } + } + impl sp_domains::BundleProducerElectionApi for Runtime { fn bundle_producer_election_params(domain_id: DomainId) -> Option> { Domains::bundle_producer_election_params(domain_id) diff --git a/crates/subspace-service/src/lib.rs b/crates/subspace-service/src/lib.rs index db3aab9914..77673a8446 100644 --- a/crates/subspace-service/src/lib.rs +++ b/crates/subspace-service/src/lib.rs @@ -28,9 +28,11 @@ pub mod dsn; mod metrics; pub mod rpc; mod sync_from_dsn; +pub mod transaction_pool; use crate::dsn::{create_dsn_instance, DsnConfigurationError}; use crate::metrics::NodeMetrics; +use crate::transaction_pool::FullPool; use core::sync::atomic::{AtomicU32, Ordering}; use cross_domain_message_gossip::cdm_gossip_peers_set_config; use domain_runtime_primitives::opaque::{Block as DomainBlock, Header as DomainHeader}; @@ -44,7 +46,9 @@ use parking_lot::Mutex; use prometheus_client::registry::Registry; use sc_basic_authorship::ProposerFactory; use sc_client_api::execution_extensions::ExtensionsFactory; -use sc_client_api::{Backend, BlockBackend, BlockchainEvents, ExecutorProvider, HeaderBackend}; +use sc_client_api::{ + AuxStore, Backend, BlockBackend, BlockchainEvents, ExecutorProvider, HeaderBackend, +}; use sc_consensus::{BasicQueue, DefaultImportQueue, ImportQueue, SharedBlockImport}; use sc_consensus_slots::SlotProportion; use sc_consensus_subspace::archiver::{create_subspace_archiver, SegmentHeadersStore}; @@ -65,7 +69,6 @@ use sc_subspace_block_relay::{ build_consensus_relay, BlockRelayConfigurationError, NetworkWrapper, }; use sc_telemetry::{Telemetry, TelemetryWorker}; -use sc_transaction_pool::FullPool; use sc_transaction_pool_api::OffchainTransactionPoolFactory; use sp_api::{ApiExt, ConstructRuntimeApi, Metadata, ProvideRuntimeApi}; use sp_block_builder::BlockBuilder; @@ -78,7 +81,7 @@ use sp_consensus_subspace::{ use sp_core::traits::{CodeExecutor, SpawnEssentialNamed}; use sp_core::H256; use sp_domains::DomainsApi; -use sp_domains_fraud_proof::{FraudProofExtension, FraudProofHostFunctionsImpl}; +use sp_domains_fraud_proof::{FraudProofExtension, FraudProofHostFunctionsImpl, FraudProofsApi}; use sp_externalities::Extensions; use sp_objects::ObjectsApi; use sp_offchain::OffchainWorkerApi; @@ -377,7 +380,7 @@ type PartialComponents = sc_service::PartialCompon FullBackend, FullSelectChain, DefaultImportQueue, - FullPool>, + FullPool, Block, DomainHeader>, OtherPartialComponents, >; @@ -401,6 +404,7 @@ where + TaggedTransactionQueue + SubspaceApi + DomainsApi + + FraudProofsApi + ObjectsApi, ExecutorDispatch: NativeExecutionDispatch + 'static, { @@ -454,14 +458,6 @@ where let select_chain = sc_consensus::LongestChain::new(backend.clone()); - let transaction_pool = sc_transaction_pool::BasicPool::new_full( - config.transaction_pool.clone(), - config.role.is_authority().into(), - config.prometheus_registry(), - task_manager.spawn_essential_handle(), - client.clone(), - ); - let segment_headers_store = SegmentHeadersStore::new(client.clone()) .map_err(|error| ServiceError::Application(error.into()))?; @@ -509,6 +505,13 @@ where )?; let sync_target_block_number = Arc::new(AtomicU32::new(0)); + let transaction_pool = crate::transaction_pool::new_full( + config, + &task_manager, + client.clone(), + sync_target_block_number.clone(), + )?; + let verifier = SubspaceVerifier::::new(SubspaceVerifierOptions { client: client.clone(), kzg, @@ -555,12 +558,16 @@ where pub struct NewFull where Client: ProvideRuntimeApi + + AuxStore + BlockBackend + BlockIdTo + HeaderBackend + HeaderMetadata + 'static, - Client::Api: TaggedTransactionQueue + DomainsApi, + Client::Api: TaggedTransactionQueue + + DomainsApi + + FraudProofsApi + + SubspaceApi, { /// Task manager. pub task_manager: TaskManager, @@ -589,7 +596,7 @@ where /// Network starter. pub network_starter: NetworkStarter, /// Transaction pool. - pub transaction_pool: Arc>, + pub transaction_pool: Arc>, } type FullNode = NewFull>; @@ -617,6 +624,7 @@ where + TransactionPaymentApi + SubspaceApi + DomainsApi + + FraudProofsApi + ObjectsApi, ExecutorDispatch: NativeExecutionDispatch + 'static, { diff --git a/crates/subspace-service/src/transaction_pool.rs b/crates/subspace-service/src/transaction_pool.rs new file mode 100644 index 0000000000..aceb1da130 --- /dev/null +++ b/crates/subspace-service/src/transaction_pool.rs @@ -0,0 +1,554 @@ +use async_trait::async_trait; +use futures::future::{Future, FutureExt, Ready}; +use sc_client_api::blockchain::HeaderBackend; +use sc_client_api::{AuxStore, BlockBackend, ExecutorProvider, UsageProvider}; +use sc_service::{Configuration, TaskManager}; +use sc_transaction_pool::error::{Error as TxPoolError, 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, +}; +use sp_api::{ApiExt, HeaderT, ProvideRuntimeApi}; +use sp_blockchain::{HeaderMetadata, TreeRoute}; +use sp_consensus_slots::Slot; +use sp_consensus_subspace::{ChainConstants, 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::{FraudProofsApi, InvalidTransactionCode}; +use sp_runtime::generic::BlockId; +use sp_runtime::traits::{Block as BlockT, BlockIdTo, Header, NumberFor}; +use sp_runtime::transaction_validity::{TransactionValidity, TransactionValidityError}; +use sp_runtime::SaturatedConversion; +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; + +/// Extrinsic hash type for a pool. +type ExtrinsicHash = <::Block as BlockT>::Hash; + +/// Extrinsic type for a pool. +type ExtrinsicFor = <::Block as BlockT>::Extrinsic; + +/// A transaction pool for a full node. +pub type FullPool = + BasicPoolWrapper>; + +type BoxedReadyIterator = + Box>> + Send>; + +type ReadyIteratorFor = BoxedReadyIterator, ExtrinsicFor>; + +type PolledIterator = Pin> + Send>>; + +pub type BlockExtrinsicOf = ::Extrinsic; + +#[derive(Clone)] +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, +} + +impl FullChainApiWrapper +where + Block: BlockT, + Client: ProvideRuntimeApi + + BlockBackend + + BlockIdTo + + HeaderBackend + + HeaderMetadata + + Send + + Sync + + 'static, + Client::Api: TaggedTransactionQueue + SubspaceApi, + DomainHeader: HeaderT, +{ + fn new( + 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(), + prometheus, + &task_manager.spawn_essential_handle(), + )), + client, + sync_target_block_number, + chain_constants, + fraud_proof_submit_sink, + marker: Default::default(), + }) + } + + fn validate_transaction_blocking( + &self, + at: &BlockId, + source: TransactionSource, + uxt: BlockExtrinsicOf, + ) -> TxPoolResult { + self.inner.validate_transaction_blocking(at, source, uxt) + } +} + +pub type ValidationFuture = Pin> + Send>>; + +impl ChainApi for FullChainApiWrapper +where + Block: BlockT, + <<::Header as HeaderT>::Number as TryInto>::Error: std::fmt::Debug, + Client: ProvideRuntimeApi + + AuxStore + + BlockBackend + + BlockIdTo + + HeaderBackend + + HeaderMetadata + + Send + + Sync + + 'static, + DomainHeader: Header, + Client::Api: TaggedTransactionQueue + + SubspaceApi + + DomainsApi, +{ + type Block = Block; + type Error = sc_transaction_pool::error::Error; + type ValidationFuture = ValidationFuture; + type BodyFuture = Ready::Extrinsic>>>>; + + fn validate_transaction( + &self, + at: &BlockId, + source: TransactionSource, + uxt: ExtrinsicFor, + ) -> Self::ValidationFuture { + let at = match self.client.block_hash_from_id(at) { + Ok(Some(at)) => at, + Ok(None) => { + let error = sc_transaction_pool::error::Error::BlockIdConversion(format!( + "Failed to convert block id {at} to hash: block not found" + )); + return Box::pin(async move { Err(error) }); + } + Err(error) => { + let error = sc_transaction_pool::error::Error::BlockIdConversion(format!( + "Failed to convert block id {at} to hash: {error}" + )); + return Box::pin(async move { Err(error) }); + } + }; + + 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 { + let uxt_validity = chain_api + .validate_transaction(&BlockId::Hash(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 domains_api_version = runtime_api + .api_version::>(at) + .map_err(|err| { + TxPoolError::RuntimeApi(format!( + "Failed to get `DomainsApi` version for block {at:?}: {err:?}." + )) + })? + // safe to return default version as 1 since there will always be version 1. + .unwrap_or(1); + if domains_api_version >= 2 { + 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() + } + + fn block_id_to_number( + &self, + at: &BlockId, + ) -> TxPoolResult>> { + self.inner.block_id_to_number(at) + } + + fn block_id_to_hash(&self, at: &BlockId) -> TxPoolResult>> { + self.inner.block_id_to_hash(at) + } + + fn hash_and_length(&self, ex: &ExtrinsicFor) -> (ExtrinsicHash, usize) { + self.inner.hash_and_length(ex) + } + + fn block_body(&self, id: ::Hash) -> Self::BodyFuture { + self.inner.block_body(id) + } + + fn block_header( + &self, + hash: ::Hash, + ) -> Result::Header>, Self::Error> { + self.inner.block_header(hash) + } + + fn tree_route( + &self, + from: ::Hash, + to: ::Hash, + ) -> Result, Self::Error> { + sp_blockchain::tree_route::(&*self.client, from, to).map_err(Into::into) + } +} + +pub struct BasicPoolWrapper +where + Block: BlockT, + PoolApi: ChainApi, +{ + inner: BasicPool, +} + +impl BasicPoolWrapper +where + Block: BlockT, + PoolApi: ChainApi + 'static, +{ + fn with_revalidation_type( + config: &Configuration, + pool_api: Arc, + prometheus: Option<&PrometheusRegistry>, + spawner: Spawn, + client: Arc, + ) -> Self + where + Client: UsageProvider, + Spawn: SpawnEssentialNamed, + { + let basic_pool = BasicPool::with_revalidation_type( + config.transaction_pool.clone(), + config.role.is_authority().into(), + pool_api, + prometheus, + RevalidationType::Full, + spawner, + client.usage_info().chain.best_number, + client.usage_info().chain.best_hash, + client.usage_info().chain.finalized_hash, + ); + + Self { inner: basic_pool } + } + + /// Gets shared reference to the underlying pool. + pub fn pool(&self) -> &Arc> { + self.inner.pool() + } + + pub fn api(&self) -> &PoolApi { + self.inner.api() + } +} + +impl LocalTransactionPool + for BasicPoolWrapper> +where + Block: BlockT, + <<::Header as HeaderT>::Number as TryInto>::Error: std::fmt::Debug, + DomainHeader: HeaderT, + Client: ProvideRuntimeApi + + AuxStore + + BlockBackend + + BlockIdTo + + HeaderBackend + + HeaderMetadata + + Send + + Sync + + 'static, + Client::Api: TaggedTransactionQueue + + SubspaceApi + + FraudProofsApi + + DomainsApi, +{ + type Block = Block; + type Hash = ExtrinsicHash>; + type Error = as ChainApi>::Error; + + fn submit_local( + &self, + at: Block::Hash, + xt: sc_transaction_pool_api::LocalTransactionFor, + ) -> Result { + let at = BlockId::Hash(at); + let validity = self + .api() + .validate_transaction_blocking(&at, TransactionSource::Local, xt.clone())? + .map_err(|e| { + Self::Error::Pool(match e { + TransactionValidityError::Invalid(i) => { + sc_transaction_pool_api::error::Error::InvalidTransaction(i) + } + TransactionValidityError::Unknown(u) => { + sc_transaction_pool_api::error::Error::UnknownTransaction(u) + } + }) + })?; + let (hash, bytes) = self.pool().validated_pool().api().hash_and_length(&xt); + let block_number = self + .api() + .block_id_to_number(&at)? + .ok_or_else(|| sc_transaction_pool::error::Error::BlockIdConversion(at.to_string()))?; + let validated = ValidatedTransaction::valid_at( + block_number.saturated_into::(), + hash, + TransactionSource::Local, + xt, + bytes, + validity, + ); + self.pool() + .validated_pool() + .submit(vec![validated]) + .remove(0) + } +} + +impl TransactionPool for BasicPoolWrapper +where + Block: BlockT, + PoolApi: ChainApi + 'static, +{ + type Block = Block; + type Hash = ExtrinsicHash; + type InPoolTransaction = Transaction, TransactionFor>; + type Error = PoolApi::Error; + + fn submit_at( + &self, + at: &BlockId, + source: TransactionSource, + xts: Vec>, + ) -> PoolFuture, Self::Error>>, Self::Error> { + self.inner.submit_at(at, source, xts) + } + + fn submit_one( + &self, + at: &BlockId, + source: TransactionSource, + xt: TransactionFor, + ) -> PoolFuture, Self::Error> { + self.inner.submit_one(at, source, xt) + } + + fn submit_and_watch( + &self, + at: &BlockId, + source: TransactionSource, + xt: TransactionFor, + ) -> PoolFuture>>, Self::Error> { + self.inner.submit_and_watch(at, source, xt) + } + + fn ready_at(&self, at: NumberFor) -> PolledIterator { + self.inner.ready_at(at) + } + + fn ready(&self) -> ReadyIteratorFor { + self.inner.ready() + } + + fn remove_invalid(&self, hashes: &[TxHash]) -> Vec> { + self.inner.remove_invalid(hashes) + } + + fn status(&self) -> PoolStatus { + self.inner.status() + } + + fn futures(&self) -> Vec { + self.inner.futures() + } + + fn import_notification_stream(&self) -> ImportNotificationStream> { + self.inner.import_notification_stream() + } + + fn on_broadcasted(&self, propagations: HashMap, Vec>) { + self.inner.on_broadcasted(propagations) + } + + fn hash_of(&self, xt: &TransactionFor) -> TxHash { + self.inner.hash_of(xt) + } + + fn ready_transaction(&self, hash: &TxHash) -> Option> { + self.inner.ready_transaction(hash) + } +} + +#[async_trait] +impl MaintainedTransactionPool for BasicPoolWrapper +where + Block: BlockT, + PoolApi: ChainApi + 'static, +{ + async fn maintain(&self, event: ChainEvent) { + self.inner.maintain(event).await + } +} + +pub fn new_full( + config: &Configuration, + task_manager: &TaskManager, + client: Arc, + sync_target_block_number: Arc, +) -> sp_blockchain::Result>> +where + Block: BlockT, + <<::Header as HeaderT>::Number as TryInto>::Error: std::fmt::Debug, + Client: ProvideRuntimeApi + + AuxStore + + BlockBackend + + HeaderBackend + + HeaderMetadata + + ExecutorProvider + + UsageProvider + + BlockIdTo + + Send + + Sync + + 'static, + DomainHeader: HeaderT, + Client::Api: TaggedTransactionQueue + + SubspaceApi + + FraudProofsApi + + DomainsApi, +{ + let prometheus = config.prometheus_registry(); + let (fraud_proof_submit_sink, mut fraud_proof_submit_stream) = mpsc::unbounded_channel(); + let pool_api = Arc::new(FullChainApiWrapper::new( + client.clone(), + prometheus, + task_manager, + sync_target_block_number, + fraud_proof_submit_sink, + )?); + + let basic_pool = Arc::new(BasicPoolWrapper::with_revalidation_type( + config, + pool_api, + prometheus, + task_manager.spawn_essential_handle(), + client.clone(), + )); + + let offchain_tx_pool_factory = OffchainTransactionPoolFactory::new(basic_pool.clone()); + + 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/test/subspace-test-runtime/src/lib.rs b/test/subspace-test-runtime/src/lib.rs index afea9bfed9..e84da228a7 100644 --- a/test/subspace-test-runtime/src/lib.rs +++ b/test/subspace-test-runtime/src/lib.rs @@ -934,6 +934,19 @@ fn extract_successful_bundles( .collect() } +fn extract_bundle( + extrinsic: UncheckedExtrinsic, +) -> Option< + sp_domains::OpaqueBundle, ::Hash, DomainHeader, Balance>, +> { + match extrinsic.function { + RuntimeCall::Domains(pallet_domains::Call::submit_bundle { opaque_bundle }) => { + Some(opaque_bundle) + } + _ => None, + } +} + // TODO: Remove when proceeding to fraud proof v2. #[allow(unused)] fn extract_receipts( @@ -1161,6 +1174,7 @@ impl_runtime_apis! { } } + #[api_version(2)] impl sp_domains::DomainsApi for Runtime { fn submit_bundle_unsigned( opaque_bundle: OpaqueBundle, ::Hash, DomainHeader, Balance>, @@ -1175,6 +1189,12 @@ impl_runtime_apis! { extract_successful_bundles(domain_id, extrinsics) } + fn extract_bundle( + extrinsic: ::Extrinsic + ) -> Option, ::Hash, DomainHeader, Balance>> { + extract_bundle(extrinsic) + } + fn extrinsics_shuffling_seed() -> Randomness { Randomness::from(Domains::extrinsics_shuffling_seed().to_fixed_bytes()) } @@ -1236,6 +1256,14 @@ impl_runtime_apis! { } } + impl sp_domains_fraud_proof::FraudProofsApi for Runtime { + fn submit_fraud_proof_unsigned( + fraud_proof: FraudProof, ::Hash, DomainHeader>, + ){ + Domains::submit_fraud_proof_unsigned(fraud_proof) + } + } + impl sp_domains::BundleProducerElectionApi for Runtime { fn bundle_producer_election_params(domain_id: DomainId) -> Option> { Domains::bundle_producer_election_params(domain_id) diff --git a/test/subspace-test-service/src/lib.rs b/test/subspace-test-service/src/lib.rs index 7365bd8078..30ef341f6f 100644 --- a/test/subspace-test-service/src/lib.rs +++ b/test/subspace-test-service/src/lib.rs @@ -45,7 +45,6 @@ use sc_service::{ BasePath, BlocksPruning, Configuration, NetworkStarter, Role, SpawnTasksParams, TaskManager, }; use sc_transaction_pool::error::Error as PoolError; -use sc_transaction_pool::FullPool; use sc_transaction_pool_api::{InPoolTransaction, TransactionPool, TransactionSource}; use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender}; use sp_api::{ApiExt, HashT, HeaderT, ProvideRuntimeApi}; @@ -68,11 +67,13 @@ use sp_runtime::{DigestItem, OpaqueExtrinsic}; use sp_timestamp::Timestamp; use std::error::Error; use std::marker::PhantomData; +use std::sync::atomic::AtomicU32; use std::sync::Arc; use std::time; use subspace_core_primitives::{Randomness, Solution}; use subspace_runtime_primitives::opaque::Block; use subspace_runtime_primitives::{AccountId, Balance, Hash}; +use subspace_service::transaction_pool::FullPool; use subspace_service::FullSelectChain; use subspace_test_client::{chain_spec, Backend, Client, TestExecutorDispatch}; use subspace_test_runtime::{RuntimeApi, RuntimeCall, UncheckedExtrinsic, SLOT_DURATION}; @@ -220,7 +221,7 @@ pub struct MockConsensusNode { /// Code executor. pub executor: NativeElseWasmExecutor, /// Transaction pool. - pub transaction_pool: Arc>, + pub transaction_pool: Arc>, /// The SelectChain Strategy pub select_chain: FullSelectChain, /// Network service. @@ -286,13 +287,14 @@ impl MockConsensusNode { let select_chain = sc_consensus::LongestChain::new(backend.clone()); - let transaction_pool = sc_transaction_pool::BasicPool::new_full( - config.transaction_pool.clone(), - config.role.is_authority().into(), - config.prometheus_registry(), - task_manager.spawn_essential_handle(), + let sync_target_block_number = Arc::new(AtomicU32::new(0)); + let transaction_pool = subspace_service::transaction_pool::new_full( + &config, + &task_manager, client.clone(), - ); + sync_target_block_number.clone(), + ) + .expect("failed to create transaction pool"); let block_import = MockBlockImport::<_, _>::new(client.clone()); From bde438e58bcc2b1242dc5c2d8e88f9965b262bad Mon Sep 17 00:00:00 2001 From: vedhavyas Date: Thu, 2 Nov 2023 14:48:37 +0100 Subject: [PATCH 3/8] refactor fraud proof processing such that bundle equivocation fraud proof can use the same code path --- crates/pallet-domains/src/lib.rs | 303 +++++++++--------- .../sp-domains-fraud-proof/src/fraud_proof.rs | 36 ++- .../src/domain_block_processor.rs | 31 +- 3 files changed, 195 insertions(+), 175 deletions(-) diff --git a/crates/pallet-domains/src/lib.rs b/crates/pallet-domains/src/lib.rs index 7b06de7195..3a77793796 100644 --- a/crates/pallet-domains/src/lib.rs +++ b/crates/pallet-domains/src/lib.rs @@ -728,7 +728,7 @@ mod pallet { }, FraudProofProcessed { domain_id: DomainId, - new_head_receipt_number: DomainBlockNumberFor, + new_head_receipt_number: Option>, }, DomainOperatorAllowListUpdated { domain_id: DomainId, @@ -880,59 +880,68 @@ mod pallet { ensure_none(origin)?; log::trace!(target: "runtime::domains", "Processing fraud proof: {fraud_proof:?}"); - + let mut operators_to_slash = BTreeSet::new(); let domain_id = fraud_proof.domain_id(); - let head_receipt_number = HeadReceiptNumber::::get(domain_id); - let bad_receipt_number = BlockTreeNodes::::get(fraud_proof.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), - ); - // Starting from the head receipt, prune all ER between [bad_receipt_number..head_receipt_number] - let mut to_prune = head_receipt_number; - let mut operator_to_slash = BTreeSet::new(); - while to_prune >= bad_receipt_number { - let receipt_hash = BlockTree::::take(domain_id, to_prune) - .ok_or::>(FraudProofError::BadReceiptNotFound.into())?; + 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), + ); - let BlockTreeNode { - execution_receipt, - operator_ids, - } = BlockTreeNodes::::take(receipt_hash) - .ok_or::>(FraudProofError::BadReceiptNotFound.into())?; + // Starting from the head receipt, prune all ER between [bad_receipt_number..head_receipt_number] + let mut to_prune = head_receipt_number; - let _ = StateRoots::::take(( - domain_id, - execution_receipt.domain_block_number, - execution_receipt.domain_block_hash, - )); + while to_prune >= bad_receipt_number { + let receipt_hash = BlockTree::::take(domain_id, to_prune) + .ok_or::>(FraudProofError::BadReceiptNotFound.into())?; - // NOTE: the operator id will be deduplicated since we are using `BTreeSet` - operator_ids.into_iter().for_each(|id| { - operator_to_slash.insert(id); - }); + let BlockTreeNode { + execution_receipt, + operator_ids, + } = BlockTreeNodes::::take(receipt_hash) + .ok_or::>(FraudProofError::BadReceiptNotFound.into())?; - to_prune -= One::one(); - } + let _ = StateRoots::::take(( + domain_id, + execution_receipt.domain_block_number, + execution_receipt.domain_block_hash, + )); - // 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); + // NOTE: the operator id will be deduplicated since we are using `BTreeSet` + operator_ids.into_iter().for_each(|id| { + operators_to_slash.insert(id); + }); - // Slash operator who have submitted the pruned fraudulent ER - do_slash_operators::(operator_to_slash.into_iter()).map_err(Error::::from)?; + to_prune -= One::one(); + } - Self::deposit_event(Event::FraudProofProcessed { - domain_id, - new_head_receipt_number, - }); + // 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) = fraud_proof.targeted_bad_operator() { + operators_to_slash.insert(targeted_bad_operator); + Self::deposit_event(Event::FraudProofProcessed { + domain_id, + new_head_receipt_number: None, + }); + } + + // Slash bad operators + do_slash_operators::(operators_to_slash.into_iter()).map_err(Error::::from)?; Ok(()) } @@ -1499,118 +1508,122 @@ impl Pallet { fn validate_fraud_proof( fraud_proof: &FraudProof, T::Hash, T::DomainHeader>, ) -> Result<(), FraudProofError> { - let bad_receipt = BlockTreeNodes::::get(fraud_proof.bad_receipt_hash()) - .ok_or(FraudProofError::BadReceiptNotFound)? - .execution_receipt; + 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; - ensure!( - !bad_receipt.domain_block_number.is_zero(), - FraudProofError::ChallengingGenesisReceipt - ); + ensure!( + !bad_receipt.domain_block_number.is_zero(), + FraudProofError::ChallengingGenesisReceipt + ); - match fraud_proof { - FraudProof::InvalidTotalRewards(InvalidTotalRewardsProof { storage_proof, .. }) => { - verify_invalid_total_rewards_fraud_proof::< + match fraud_proof { + FraudProof::InvalidTotalRewards(InvalidTotalRewardsProof { + storage_proof, .. + }) => { + verify_invalid_total_rewards_fraud_proof::< + T::Block, + DomainBlockNumberFor, + T::DomainHash, + BalanceOf, + DomainHashingFor, + >(bad_receipt, storage_proof) + .map_err(|err| { + log::error!( + target: "runtime::domains", + "Total rewards proof verification failed: {err:?}" + ); + FraudProofError::InvalidTotalRewardsFraudProof + })?; + } + 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) => { + verify_invalid_bundles_fraud_proof::>( + bad_receipt, + 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::< T::Block, DomainBlockNumberFor, T::DomainHash, BalanceOf, - DomainHashingFor, - >(bad_receipt, storage_proof) - .map_err(|err| { - log::error!( - target: "runtime::domains", - "Total rewards proof verification failed: {err:?}" - ); - FraudProofError::InvalidTotalRewardsFraudProof - })?; - } - 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:?}" + "Valid bundle proof verification failed: {err:?}" ); - FraudProofError::InvalidStateTransitionFraudProof - })?; + FraudProofError::BadValidBundleFraudProof + })?, + _ => {} } - FraudProof::InvalidBundles(invalid_bundles_fraud_proof) => { - verify_invalid_bundles_fraud_proof::>( - bad_receipt, - 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::< - T::Block, - DomainBlockNumberFor, - T::DomainHash, - BalanceOf, - >(bad_receipt, proof) - .map_err(|err| { - log::error!( - target: "runtime::domains", - "Valid bundle proof verification failed: {err:?}" - ); - FraudProofError::BadValidBundleFraudProof - })?, - _ => {} } Ok(()) diff --git a/crates/sp-domains-fraud-proof/src/fraud_proof.rs b/crates/sp-domains-fraud-proof/src/fraud_proof.rs index 952db58be5..1d80441675 100644 --- a/crates/sp-domains-fraud-proof/src/fraud_proof.rs +++ b/crates/sp-domains-fraud-proof/src/fraud_proof.rs @@ -6,7 +6,7 @@ use sp_domain_digests::AsPredigest; use sp_domains::proof_provider_and_verifier::StorageProofVerifier; use sp_domains::{ BundleValidity, DomainId, ExecutionReceipt, HeaderHashFor, HeaderHashingFor, InvalidBundleType, - SealedBundleHeader, + OperatorId, SealedBundleHeader, }; use sp_runtime::traits::{Block as BlockT, Hash as HashT, Header as HeaderT, NumberFor}; use sp_runtime::{Digest, DigestItem}; @@ -400,24 +400,30 @@ impl FraudProof } } - pub fn bad_receipt_hash(&self) -> HeaderHashFor { + pub fn targeted_bad_receipt_hash(&self) -> Option> { match self { - Self::InvalidStateTransition(proof) => proof.bad_receipt_hash, - Self::InvalidTransaction(proof) => proof.bad_receipt_hash, - Self::ImproperTransactionSortition(proof) => proof.bad_receipt_hash, - // TODO: the `BundleEquivocation` fraud proof is different from other fraud proof, - // which target equivocate bundle instead of bad receipt, revisit this when fraud - // proof v2 is implemented. - Self::BundleEquivocation(_) => Default::default(), + 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, .. - } => *bad_receipt_hash, - Self::InvalidExtrinsicsRoot(proof) => proof.bad_receipt_hash, - Self::InvalidTotalRewards(proof) => proof.bad_receipt_hash(), - Self::ValidBundle(proof) => proof.bad_receipt_hash, - Self::InvalidBundles(proof) => proof.bad_receipt_hash, - Self::InvalidDomainBlockHash(proof) => proof.bad_receipt_hash, + } => Some(*bad_receipt_hash), + Self::InvalidExtrinsicsRoot(proof) => Some(proof.bad_receipt_hash), + Self::InvalidTotalRewards(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), + } + } + + pub fn targeted_bad_operator(&self) -> Option { + match self { + Self::BundleEquivocation(proof) => { + Some(proof.first_header.header.proof_of_election.operator_id) + } + _ => None, } } diff --git a/domains/client/domain-operator/src/domain_block_processor.rs b/domains/client/domain-operator/src/domain_block_processor.rs index 2ce603f259..1a19086739 100644 --- a/domains/client/domain-operator/src/domain_block_processor.rs +++ b/domains/client/domain-operator/src/domain_block_processor.rs @@ -852,22 +852,23 @@ where let parent_chain_parent_hash = self.parent_chain.parent_hash(parent_chain_block_hash)?; let mut bad_receipts_to_delete = vec![]; for fraud_proof in fraud_proofs { - let bad_receipt_hash = fraud_proof.bad_receipt_hash(); - if let Some(bad_receipt) = self - .parent_chain - .execution_receipt(parent_chain_parent_hash, bad_receipt_hash)? - { - // In order to not delete a receipt which was just inserted, accumulate the write&delete operations - // in case the bad receipt and corresponding fraud proof are included in the same block. - if let Some(index) = bad_receipts_to_write - .iter() - .map(|(_, receipt_hash, _)| receipt_hash) - .position(|v| *v == bad_receipt_hash) + if let Some(bad_receipt_hash) = fraud_proof.targeted_bad_receipt_hash() { + if let Some(bad_receipt) = self + .parent_chain + .execution_receipt(parent_chain_parent_hash, bad_receipt_hash)? { - bad_receipts_to_write.swap_remove(index); - } else { - bad_receipts_to_delete - .push((bad_receipt.consensus_block_number, bad_receipt_hash)); + // In order to not delete a receipt which was just inserted, accumulate the write&delete operations + // in case the bad receipt and corresponding fraud proof are included in the same block. + if let Some(index) = bad_receipts_to_write + .iter() + .map(|(_, receipt_hash, _)| receipt_hash) + .position(|v| *v == bad_receipt_hash) + { + bad_receipts_to_write.swap_remove(index); + } else { + bad_receipts_to_delete + .push((bad_receipt.consensus_block_number, bad_receipt_hash)); + } } } } From 15926487f6561e791a76a554b82b69572af0141c Mon Sep 17 00:00:00 2001 From: vedhavyas Date: Thu, 2 Nov 2023 17:40:29 +0100 Subject: [PATCH 4/8] add infra to verify bundle equivocation fraud proof --- crates/pallet-domains/src/lib.rs | 55 ++++------- crates/pallet-domains/src/tests.rs | 24 ++++- .../sp-domains-fraud-proof/src/fraud_proof.rs | 13 +++ .../src/host_functions.rs | 47 ++++++++- crates/sp-domains-fraud-proof/src/lib.rs | 31 +++++- .../src/verification.rs | 98 ++++++++++++++++++- .../src/bundle_producer_election.rs | 31 +++++- crates/sp-domains/src/lib.rs | 22 +++-- crates/subspace-service/src/lib.rs | 7 +- .../src/bundle_producer_election_solver.rs | 3 +- .../src/domain_bundle_proposer.rs | 2 +- test/subspace-test-service/src/lib.rs | 4 +- 12 files changed, 278 insertions(+), 59 deletions(-) diff --git a/crates/pallet-domains/src/lib.rs b/crates/pallet-domains/src/lib.rs index 3a77793796..a63804a529 100644 --- a/crates/pallet-domains/src/lib.rs +++ b/crates/pallet-domains/src/lib.rs @@ -35,7 +35,7 @@ pub mod weights; extern crate alloc; use crate::block_tree::verify_execution_receipt; -use crate::staking::{do_nominate_operator, Operator, OperatorStatus}; +use crate::staking::{do_nominate_operator, OperatorStatus}; use codec::{Decode, Encode}; use frame_support::ensure; use frame_support::traits::fungible::{Inspect, InspectHold}; @@ -45,11 +45,10 @@ use frame_system::pallet_prelude::*; pub use pallet::*; use scale_info::TypeInfo; use sp_core::H256; -use sp_domains::bundle_producer_election::{is_below_threshold, BundleProducerElectionParams}; +use sp_domains::bundle_producer_election::BundleProducerElectionParams; use sp_domains::{ DomainBlockLimit, DomainId, DomainInstanceData, ExecutionReceipt, OpaqueBundle, OperatorId, - OperatorPublicKey, ProofOfElection, RuntimeId, DOMAIN_EXTRINSICS_SHUFFLING_SEED_SUBJECT, - EMPTY_EXTRINSIC_ROOT, + OperatorPublicKey, RuntimeId, DOMAIN_EXTRINSICS_SHUFFLING_SEED_SUBJECT, EMPTY_EXTRINSIC_ROOT, }; use sp_domains_fraud_proof::fraud_proof::{ FraudProof, InvalidDomainBlockHashProof, InvalidTotalRewardsProof, @@ -151,6 +150,7 @@ mod pallet { use frame_support::{Identity, PalletError}; use frame_system::pallet_prelude::*; use sp_core::H256; + use sp_domains::bundle_producer_election::ProofOfElectionError; use sp_domains::{ BundleDigest, DomainId, EpochIndex, GenesisDomain, OperatorAllowList, OperatorId, RuntimeId, RuntimeType, @@ -656,6 +656,15 @@ mod pallet { } } + impl From for BundleError { + fn from(err: ProofOfElectionError) -> Self { + match err { + ProofOfElectionError::BadVrfProof => Self::BadVrfSignature, + ProofOfElectionError::ThresholdUnsatisfied => Self::ThresholdUnsatisfied, + } + } + } + #[pallet::error] pub enum Error { /// Invalid fraud proof. @@ -1432,33 +1441,6 @@ impl Pallet { Ok(()) } - fn check_proof_of_election( - domain_id: DomainId, - operator_id: OperatorId, - operator: Operator, T::Share>, - bundle_slot_probability: (u64, u64), - proof_of_election: &ProofOfElection, - ) -> Result<(), BundleError> { - proof_of_election - .verify_vrf_signature(&operator.signing_key) - .map_err(|_| BundleError::BadVrfSignature)?; - - let (operator_stake, total_domain_stake) = - Self::fetch_operator_stake_info(domain_id, &operator_id)?; - - let threshold = sp_domains::bundle_producer_election::calculate_threshold( - operator_stake.saturated_into(), - total_domain_stake.saturated_into(), - bundle_slot_probability, - ); - - if !is_below_threshold(&proof_of_election.vrf_signature.output, threshold) { - return Err(BundleError::ThresholdUnsatisfied); - } - - Ok(()) - } - fn validate_bundle(opaque_bundle: &OpaqueBundleOf) -> Result<(), BundleError> { let domain_id = opaque_bundle.domain_id(); let operator_id = opaque_bundle.operator_id(); @@ -1491,12 +1473,15 @@ impl Pallet { Self::check_extrinsics_root(opaque_bundle)?; let proof_of_election = &sealed_header.header.proof_of_election; - Self::check_proof_of_election( - domain_id, - operator_id, - operator, + let (operator_stake, total_domain_stake) = + Self::fetch_operator_stake_info(domain_id, &operator_id)?; + + sp_domains::bundle_producer_election::check_proof_of_election( + &operator.signing_key, domain_config.bundle_slot_probability, proof_of_election, + operator_stake.saturated_into(), + total_domain_stake.saturated_into(), )?; let receipt = &sealed_header.header.receipt; diff --git a/crates/pallet-domains/src/tests.rs b/crates/pallet-domains/src/tests.rs index d6d9cda0a2..696efdbf88 100644 --- a/crates/pallet-domains/src/tests.rs +++ b/crates/pallet-domains/src/tests.rs @@ -1,9 +1,10 @@ use crate::block_tree::BlockTreeNode; use crate::domain_registry::{DomainConfig, DomainObject}; +use crate::staking::Operator; use crate::{ self as pallet_domains, BalanceOf, BlockTree, BlockTreeNodes, BundleError, Config, ConsensusBlockHash, DomainBlockNumberFor, DomainHashingFor, DomainRegistry, ExecutionInbox, - ExecutionReceiptOf, FraudProofError, FungibleHoldId, HeadReceiptNumber, NextDomainId, Operator, + ExecutionReceiptOf, FraudProofError, FungibleHoldId, HeadReceiptNumber, NextDomainId, OperatorStatus, Operators, ReceiptHashFor, }; use codec::{Decode, Encode, MaxEncodedLen}; @@ -260,6 +261,9 @@ pub(crate) struct MockDomainFraudProofExtension { runtime_code: Vec, tx_range: bool, is_inherent: bool, + domain_total_stake: Balance, + bundle_slot_probability: (u64, u64), + operator_stake: Balance, } impl FraudProofHostFunctions for MockDomainFraudProofExtension { @@ -308,6 +312,15 @@ impl FraudProofHostFunctions for MockDomainFraudProofExtension { FraudProofVerificationInfoRequest::InherentExtrinsicCheck { .. } => { FraudProofVerificationInfoResponse::InherentExtrinsicCheck(self.is_inherent) } + 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) + } }; Some(response) @@ -994,6 +1007,9 @@ fn test_invalid_domain_extrinsic_root_proof() { runtime_code: vec![1, 2, 3, 4], tx_range: true, is_inherent: true, + domain_total_stake: 100 * SSC, + operator_stake: 10 * SSC, + bundle_slot_probability: (0, 0), })); ext.register_extension(fraud_proof_ext); @@ -1070,6 +1086,9 @@ fn test_true_invalid_bundles_inherent_extrinsic_proof() { tx_range: true, // return `true` indicating this is an inherent extrinsic is_inherent: true, + domain_total_stake: 100 * SSC, + operator_stake: 10 * SSC, + bundle_slot_probability: (0, 0), })); ext.register_extension(fraud_proof_ext); @@ -1132,6 +1151,9 @@ fn test_false_invalid_bundles_inherent_extrinsic_proof() { tx_range: true, // return `false` indicating this is not an inherent extrinsic is_inherent: false, + domain_total_stake: 100 * SSC, + operator_stake: 10 * SSC, + bundle_slot_probability: (0, 0), })); ext.register_extension(fraud_proof_ext); diff --git a/crates/sp-domains-fraud-proof/src/fraud_proof.rs b/crates/sp-domains-fraud-proof/src/fraud_proof.rs index 1d80441675..ddfaf8190c 100644 --- a/crates/sp-domains-fraud-proof/src/fraud_proof.rs +++ b/crates/sp-domains-fraud-proof/src/fraud_proof.rs @@ -1,3 +1,4 @@ +use crate::verification::InvalidBundleEquivocationError; use codec::{Decode, Encode}; use scale_info::TypeInfo; use sp_consensus_slots::Slot; @@ -327,6 +328,18 @@ pub enum VerificationError { error("Failed to check if a given extrinsic is inherent or not") )] FailedToCheckInherentExtrinsic, + /// Invalid bundle equivocation fraud proof. + #[cfg_attr( + feature = "thiserror", + error("Invalid bundle equivocation fraud proof: {0}") + )] + InvalidBundleEquivocationFraudProof(InvalidBundleEquivocationError), +} + +impl From for VerificationError { + fn from(err: InvalidBundleEquivocationError) -> Self { + Self::InvalidBundleEquivocationFraudProof(err) + } } #[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] diff --git a/crates/sp-domains-fraud-proof/src/host_functions.rs b/crates/sp-domains-fraud-proof/src/host_functions.rs index bef967f70c..0026019b9f 100644 --- a/crates/sp-domains-fraud-proof/src/host_functions.rs +++ b/crates/sp-domains-fraud-proof/src/host_functions.rs @@ -13,7 +13,8 @@ use sp_api::{BlockT, HashT, ProvideRuntimeApi}; use sp_blockchain::HeaderBackend; use sp_core::traits::{CodeExecutor, FetchRuntimeCode, RuntimeCode}; use sp_core::H256; -use sp_domains::{DomainId, DomainsApi}; +use sp_domains::bundle_producer_election::BundleProducerElectionParams; +use sp_domains::{BundleProducerElectionApi, DomainId, DomainsApi, OperatorId}; use sp_runtime::traits::Header as HeaderT; use sp_runtime::OpaqueExtrinsic; use sp_std::vec::Vec; @@ -22,6 +23,7 @@ use std::borrow::Cow; use std::marker::PhantomData; use std::sync::Arc; use subspace_core_primitives::{Randomness, U256}; +use subspace_runtime_primitives::Balance; struct DomainRuntimeCodeFetcher(Vec); @@ -99,7 +101,7 @@ where Block::Hash: From, DomainBlock: BlockT, Client: BlockBackend + HeaderBackend + ProvideRuntimeApi, - Client::Api: DomainsApi, + Client::Api: DomainsApi + BundleProducerElectionApi, Executor: CodeExecutor + RuntimeVersionOf, { fn get_block_randomness(&self, consensus_block_hash: H256) -> Option { @@ -277,6 +279,32 @@ where &extrinsic ).ok() } + + fn get_domain_election_params( + &self, + consensus_block_hash: H256, + domain_id: DomainId, + ) -> Option> { + let runtime_api = self.consensus_client.runtime_api(); + let consensus_block_hash = consensus_block_hash.into(); + let election_params = runtime_api + .bundle_producer_election_params(consensus_block_hash, domain_id) + .ok()??; + Some(election_params) + } + + fn get_operator_stake( + &self, + consensus_block_hash: H256, + operator_id: OperatorId, + ) -> Option { + let runtime_api = self.consensus_client.runtime_api(); + let consensus_block_hash = consensus_block_hash.into(); + let (_, operator_stake) = runtime_api + .operator(consensus_block_hash, operator_id) + .ok()??; + Some(operator_stake) + } } impl FraudProofHostFunctions @@ -287,7 +315,7 @@ where DomainBlock: BlockT, DomainBlock::Hash: From + Into, Client: BlockBackend + HeaderBackend + ProvideRuntimeApi, - Client::Api: DomainsApi, + Client::Api: DomainsApi + BundleProducerElectionApi, Executor: CodeExecutor + RuntimeVersionOf, { fn get_fraud_proof_verification_info( @@ -350,6 +378,19 @@ where .map(|is_inherent| { FraudProofVerificationInfoResponse::InherentExtrinsicCheck(is_inherent) }), + FraudProofVerificationInfoRequest::DomainElectionParams { domain_id } => self + .get_domain_election_params(consensus_block_hash, domain_id) + .map(|domain_election_params| { + FraudProofVerificationInfoResponse::DomainElectionParams { + domain_total_stake: domain_election_params.total_domain_stake, + bundle_slot_probability: domain_election_params.bundle_slot_probability, + } + }), + FraudProofVerificationInfoRequest::OperatorStake { operator_id } => self + .get_operator_stake(consensus_block_hash, operator_id) + .map(|operator_stake| { + FraudProofVerificationInfoResponse::OperatorStake(operator_stake) + }), } } diff --git a/crates/sp-domains-fraud-proof/src/lib.rs b/crates/sp-domains-fraud-proof/src/lib.rs index 4d31094e66..2404b20557 100644 --- a/crates/sp-domains-fraud-proof/src/lib.rs +++ b/crates/sp-domains-fraud-proof/src/lib.rs @@ -40,7 +40,7 @@ pub use runtime_interface::fraud_proof_runtime_interface; pub use runtime_interface::fraud_proof_runtime_interface::HostFunctions; use sp_api::scale_info::TypeInfo; use sp_api::HeaderT; -use sp_domains::DomainId; +use sp_domains::{DomainId, OperatorId}; use sp_runtime::traits::NumberFor; use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidity}; use sp_runtime::OpaqueExtrinsic; @@ -48,6 +48,7 @@ use sp_runtime_interface::pass_by; use sp_runtime_interface::pass_by::PassBy; use sp_std::vec::Vec; use subspace_core_primitives::Randomness; +use subspace_runtime_primitives::Balance; /// Custom invalid validity code for the extrinsics in pallet-domains. #[repr(u8)] @@ -103,6 +104,10 @@ pub enum FraudProofVerificationInfoRequest { /// Extrinsic for which we need to if it is inherent or not. opaque_extrinsic: OpaqueExtrinsic, }, + /// Request to get Domain election params. + DomainElectionParams { domain_id: DomainId }, + /// Request to get Operator stake. + OperatorStake { operator_id: OperatorId }, } impl PassBy for FraudProofVerificationInfoRequest { @@ -135,6 +140,13 @@ pub enum FraudProofVerificationInfoResponse { TxRangeCheck(bool), /// If the particular extrinsic provided is either inherent or not. InherentExtrinsicCheck(bool), + /// Domain's total stake at a given Consensus hash. + DomainElectionParams { + domain_total_stake: Balance, + bundle_slot_probability: (u64, u64), + }, + /// Operators Stake at a given Consensus hash. + OperatorStake(Balance), } impl FraudProofVerificationInfoResponse { @@ -192,6 +204,23 @@ impl FraudProofVerificationInfoResponse { _ => None, } } + + pub fn into_domain_election_params(self) -> Option<(Balance, (u64, u64))> { + match self { + FraudProofVerificationInfoResponse::DomainElectionParams { + domain_total_stake, + bundle_slot_probability, + } => Some((domain_total_stake, bundle_slot_probability)), + _ => None, + } + } + + pub fn into_operator_stake(self) -> Option { + match self { + FraudProofVerificationInfoResponse::OperatorStake(stake) => Some(stake), + _ => None, + } + } } sp_api::decl_runtime_apis! { diff --git a/crates/sp-domains-fraud-proof/src/verification.rs b/crates/sp-domains-fraud-proof/src/verification.rs index 2be2b41122..9c20fac709 100644 --- a/crates/sp-domains-fraud-proof/src/verification.rs +++ b/crates/sp-domains-fraud-proof/src/verification.rs @@ -11,15 +11,17 @@ use codec::{Decode, Encode}; 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::{ BundleValidity, ExecutionReceipt, HeaderHashFor, HeaderHashingFor, HeaderNumberFor, - InboxedBundle, InvalidBundleType, + InboxedBundle, InvalidBundleType, OperatorPublicKey, SealedBundleHeader, }; use sp_runtime::generic::Digest; use sp_runtime::traits::{Block as BlockT, Hash, Header as HeaderT, NumberFor}; +use sp_runtime::{RuntimeAppPublic, SaturatedConversion}; use sp_std::vec::Vec; use sp_trie::{LayoutV1, StorageProof}; use subspace_core_primitives::Randomness; @@ -458,3 +460,97 @@ where _ => 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, +} + +/// Verifies Bundle equivocation fraud proof. +#[allow(dead_code)] +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 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/bundle_producer_election.rs b/crates/sp-domains/src/bundle_producer_election.rs index fc179a7978..f9e764c907 100644 --- a/crates/sp-domains/src/bundle_producer_election.rs +++ b/crates/sp-domains/src/bundle_producer_election.rs @@ -1,4 +1,4 @@ -use crate::{DomainId, OperatorId, OperatorPublicKey, StakeWeight}; +use crate::{DomainId, OperatorId, OperatorPublicKey, ProofOfElection, StakeWeight}; use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; use sp_core::crypto::{VrfPublic, Wraps}; @@ -60,9 +60,11 @@ pub struct BundleProducerElectionParams { } #[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] -pub enum VrfProofError { +pub enum ProofOfElectionError { /// Invalid vrf proof. - BadProof, + BadVrfProof, + /// Threshold unsatisfied error. + ThresholdUnsatisfied, } /// Verify the vrf proof generated in the bundle election. @@ -71,12 +73,31 @@ pub(crate) fn verify_vrf_signature( public_key: &OperatorPublicKey, vrf_signature: &VrfSignature, global_challenge: &Blake3Hash, -) -> Result<(), VrfProofError> { +) -> Result<(), ProofOfElectionError> { if !public_key.as_inner_ref().vrf_verify( &make_transcript(domain_id, global_challenge).into(), vrf_signature, ) { - return Err(VrfProofError::BadProof); + return Err(ProofOfElectionError::BadVrfProof); + } + + Ok(()) +} + +pub fn check_proof_of_election( + operator_signing_key: &OperatorPublicKey, + bundle_slot_probability: (u64, u64), + proof_of_election: &ProofOfElection, + operator_stake: StakeWeight, + total_domain_stake: StakeWeight, +) -> Result<(), ProofOfElectionError> { + proof_of_election.verify_vrf_signature(operator_signing_key)?; + + let threshold = + calculate_threshold(operator_stake, total_domain_stake, bundle_slot_probability); + + if !is_below_threshold(&proof_of_election.vrf_signature.output, threshold) { + return Err(ProofOfElectionError::ThresholdUnsatisfied); } Ok(()) diff --git a/crates/sp-domains/src/lib.rs b/crates/sp-domains/src/lib.rs index fef7d5c8fb..589cf3204d 100644 --- a/crates/sp-domains/src/lib.rs +++ b/crates/sp-domains/src/lib.rs @@ -30,7 +30,7 @@ extern crate alloc; use crate::storage::{RawGenesis, StorageKey}; use alloc::string::String; -use bundle_producer_election::{BundleProducerElectionParams, VrfProofError}; +use bundle_producer_election::{BundleProducerElectionParams, ProofOfElectionError}; use hexlit::hex; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; @@ -184,7 +184,7 @@ impl PassBy for DomainId { #[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] pub struct BundleHeader { /// Proof of bundle producer election. - pub proof_of_election: ProofOfElection, + pub proof_of_election: ProofOfElection, /// Execution receipt that should extend the receipt chain or add confirmations /// to the head receipt. pub receipt: ExecutionReceipt< @@ -341,7 +341,12 @@ impl } #[cfg(any(feature = "std", feature = "runtime-benchmarks"))] -pub fn dummy_opaque_bundle( +pub fn dummy_opaque_bundle< + Number: Encode, + Hash: Default + Encode, + DomainHeader: HeaderT, + Balance: Encode, +>( domain_id: DomainId, operator_id: OperatorId, receipt: ExecutionReceipt< @@ -526,7 +531,7 @@ impl< } #[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] -pub struct ProofOfElection { +pub struct ProofOfElection { /// Domain id. pub domain_id: DomainId, /// The slot number. @@ -537,13 +542,15 @@ pub struct ProofOfElection { pub vrf_signature: VrfSignature, /// Operator index in the OperatorRegistry. pub operator_id: OperatorId, + /// Consensus block hash at which proof of election was derived. + pub consensus_block_hash: CHash, } -impl ProofOfElection { +impl ProofOfElection { pub fn verify_vrf_signature( &self, operator_signing_key: &OperatorPublicKey, - ) -> Result<(), VrfProofError> { + ) -> Result<(), ProofOfElectionError> { let global_challenge = self .global_randomness .derive_global_challenge(self.slot_number); @@ -563,7 +570,7 @@ impl ProofOfElection { } } -impl ProofOfElection { +impl ProofOfElection { #[cfg(any(feature = "std", feature = "runtime-benchmarks"))] pub fn dummy(domain_id: DomainId, operator_id: OperatorId) -> Self { let output_bytes = sp_std::vec![0u8; VrfOutput::max_encoded_len()]; @@ -578,6 +585,7 @@ impl ProofOfElection { global_randomness: Randomness::default(), vrf_signature, operator_id, + consensus_block_hash: Default::default(), } } } diff --git a/crates/subspace-service/src/lib.rs b/crates/subspace-service/src/lib.rs index 77673a8446..c11ce23d15 100644 --- a/crates/subspace-service/src/lib.rs +++ b/crates/subspace-service/src/lib.rs @@ -80,7 +80,7 @@ use sp_consensus_subspace::{ }; use sp_core::traits::{CodeExecutor, SpawnEssentialNamed}; use sp_core::H256; -use sp_domains::DomainsApi; +use sp_domains::{BundleProducerElectionApi, DomainsApi}; use sp_domains_fraud_proof::{FraudProofExtension, FraudProofHostFunctionsImpl, FraudProofsApi}; use sp_externalities::Extensions; use sp_objects::ObjectsApi; @@ -224,7 +224,9 @@ where + Send + Sync + 'static, - Client::Api: SubspaceApi + DomainsApi, + Client::Api: SubspaceApi + + DomainsApi + + BundleProducerElectionApi, ExecutorDispatch: CodeExecutor + sc_executor::RuntimeVersionOf, { fn extensions_for( @@ -405,6 +407,7 @@ where + SubspaceApi + DomainsApi + FraudProofsApi + + BundleProducerElectionApi + ObjectsApi, ExecutorDispatch: NativeExecutionDispatch + 'static, { diff --git a/domains/client/domain-operator/src/bundle_producer_election_solver.rs b/domains/client/domain-operator/src/bundle_producer_election_solver.rs index 9973fee423..a1fc8ca1ca 100644 --- a/domains/client/domain-operator/src/bundle_producer_election_solver.rs +++ b/domains/client/domain-operator/src/bundle_producer_election_solver.rs @@ -49,7 +49,7 @@ where consensus_block_hash: CBlock::Hash, domain_id: DomainId, global_randomness: Randomness, - ) -> sp_blockchain::Result> { + ) -> sp_blockchain::Result, OperatorPublicKey)>> { let BundleProducerElectionParams { current_operators, total_domain_stake, @@ -94,6 +94,7 @@ where global_randomness, vrf_signature, operator_id, + consensus_block_hash, }; return Ok(Some((proof_of_election, operator_signing_key))); } diff --git a/domains/client/domain-operator/src/domain_bundle_proposer.rs b/domains/client/domain-operator/src/domain_bundle_proposer.rs index d81403c274..ffd9c81c01 100644 --- a/domains/client/domain-operator/src/domain_bundle_proposer.rs +++ b/domains/client/domain-operator/src/domain_bundle_proposer.rs @@ -69,7 +69,7 @@ where pub(crate) async fn propose_bundle_at( &self, - proof_of_election: ProofOfElection, + proof_of_election: ProofOfElection, parent_chain: ParentChain, tx_range: U256, ) -> sp_blockchain::Result> diff --git a/test/subspace-test-service/src/lib.rs b/test/subspace-test-service/src/lib.rs index 30ef341f6f..aaa5faa8cd 100644 --- a/test/subspace-test-service/src/lib.rs +++ b/test/subspace-test-service/src/lib.rs @@ -56,7 +56,7 @@ use sp_consensus_subspace::digests::{CompatibleDigestItem, PreDigest, PreDigestP use sp_consensus_subspace::FarmerPublicKey; use sp_core::traits::{CodeExecutor, SpawnEssentialNamed}; use sp_core::H256; -use sp_domains::{DomainsApi, OpaqueBundle}; +use sp_domains::{BundleProducerElectionApi, DomainsApi, OpaqueBundle}; use sp_domains_fraud_proof::{FraudProofExtension, FraudProofHostFunctionsImpl}; use sp_externalities::Extensions; use sp_inherents::{InherentData, InherentDataProvider}; @@ -192,7 +192,7 @@ where DomainBlock: BlockT, DomainBlock::Hash: Into + From, Client: BlockBackend + HeaderBackend + ProvideRuntimeApi + 'static, - Client::Api: DomainsApi, + Client::Api: DomainsApi + BundleProducerElectionApi, Executor: CodeExecutor + sc_executor::RuntimeVersionOf, { fn extensions_for( From 47414972ca239fce7b31c58ac0dd51aae7e1aa86 Mon Sep 17 00:00:00 2001 From: vedhavyas Date: Fri, 3 Nov 2023 16:41:27 +0100 Subject: [PATCH 5/8] verify bundle equivocation fraud proof on runtime and add bundle equivocation fraud proof test --- crates/pallet-domains/src/lib.rs | 34 +++++- .../src/verification.rs | 5 +- domains/client/domain-operator/src/tests.rs | 108 ++++++++++++++++++ 3 files changed, 142 insertions(+), 5 deletions(-) diff --git a/crates/pallet-domains/src/lib.rs b/crates/pallet-domains/src/lib.rs index a63804a529..7e2ecd0b49 100644 --- a/crates/pallet-domains/src/lib.rs +++ b/crates/pallet-domains/src/lib.rs @@ -54,7 +54,8 @@ use sp_domains_fraud_proof::fraud_proof::{ FraudProof, InvalidDomainBlockHashProof, InvalidTotalRewardsProof, }; use sp_domains_fraud_proof::verification::{ - verify_invalid_bundles_fraud_proof, verify_invalid_domain_block_hash_fraud_proof, + verify_bundle_equivocation_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_total_rewards_fraud_proof, verify_valid_bundle_fraud_proof, }; @@ -64,6 +65,7 @@ use sp_std::boxed::Box; use sp_std::collections::btree_map::BTreeMap; use sp_std::vec::Vec; use subspace_core_primitives::U256; +use subspace_runtime_primitives::Balance; pub(crate) type BalanceOf = <::Currency as Inspect<::AccountId>>::Balance; @@ -618,6 +620,12 @@ mod pallet { InvalidBundleFraudProof, /// Bad/Invalid valid bundle fraud proof BadValidBundleFraudProof, + /// Missing operator. + MissingOperator, + /// Unexpected fraud proof. + UnexpectedFraudProof, + /// Bad/Invalid bundle equivocation fraud proof. + BadBundleEquivocationFraudProof, } impl From for Error { @@ -1607,7 +1615,29 @@ impl Pallet { ); FraudProofError::BadValidBundleFraudProof })?, - _ => {} + _ => return Err(FraudProofError::UnexpectedFraudProof), + } + } else if let Some(bad_operator_id) = fraud_proof.targeted_bad_operator() { + 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 + })?; + } + + _ => return Err(FraudProofError::UnexpectedFraudProof), } } diff --git a/crates/sp-domains-fraud-proof/src/verification.rs b/crates/sp-domains-fraud-proof/src/verification.rs index 9c20fac709..db7ed571d1 100644 --- a/crates/sp-domains-fraud-proof/src/verification.rs +++ b/crates/sp-domains-fraud-proof/src/verification.rs @@ -486,11 +486,10 @@ pub enum InvalidBundleEquivocationError { } /// Verifies Bundle equivocation fraud proof. -#[allow(dead_code)] 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>, + header_1: &SealedBundleHeader, CBlock::Hash, DomainHeader, Balance>, + header_2: &SealedBundleHeader, CBlock::Hash, DomainHeader, Balance>, ) -> Result<(), InvalidBundleEquivocationError> where CBlock: BlockT, diff --git a/domains/client/domain-operator/src/tests.rs b/domains/client/domain-operator/src/tests.rs index 07a8620288..ec44bca337 100644 --- a/domains/client/domain-operator/src/tests.rs +++ b/domains/client/domain-operator/src/tests.rs @@ -1506,6 +1506,114 @@ async fn test_invalid_domain_extrinsics_root_proof_creation() { ferdie.produce_blocks(1).await.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, bundle) = ferdie.produce_slot_and_wait_for_bundle_submission().await; + let original_submit_bundle_tx = bundle_to_tx(bundle.clone().unwrap()); + + // 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.into()).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 mut opaque_bundle = bundle.unwrap(); + 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 mut import_tx_stream = ferdie.transaction_pool.import_notification_stream(); + + 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}"), + } + + while let Some(ready_tx_hash) = import_tx_stream.next().await { + let ready_tx = ferdie + .transaction_pool + .ready_transaction(&ready_tx_hash) + .unwrap(); + let ext = subspace_test_runtime::UncheckedExtrinsic::decode( + &mut ready_tx.data.encode().as_slice(), + ) + .unwrap(); + if let subspace_test_runtime::RuntimeCall::Domains( + pallet_domains::Call::submit_fraud_proof { fraud_proof }, + ) = ext.function + { + if let FraudProof::BundleEquivocation(_) = *fraud_proof { + break; + } + } + } + + // Produce a consensus block that contains the fraud proof, the fraud proof wil be verified on + // on the runtime itself + ferdie.produce_blocks(1).await.unwrap(); +} + #[tokio::test(flavor = "multi_thread")] #[ignore] async fn test_valid_bundle_proof_generation_and_verification() { From d047ed897c3f21dc432a5b542798480cb2459163 Mon Sep 17 00:00:00 2001 From: vedhavyas Date: Tue, 7 Nov 2023 11:15:17 +0100 Subject: [PATCH 6/8] add further comments and todo --- crates/subspace-service/src/transaction_pool.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/subspace-service/src/transaction_pool.rs b/crates/subspace-service/src/transaction_pool.rs index aceb1da130..3f7d389605 100644 --- a/crates/subspace-service/src/transaction_pool.rs +++ b/crates/subspace-service/src/transaction_pool.rs @@ -195,6 +195,7 @@ where })? // safe to return default version as 1 since there will always be version 1. .unwrap_or(1); + // TODO: this is keep gemini-3g compatible. Remove before a new network launch. if domains_api_version >= 2 { let maybe_opaque_bundle = runtime_api .extract_bundle(at, uxt) @@ -524,6 +525,10 @@ where 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( From 512caff544e37cbf81c0a274f073f284effce1b5 Mon Sep 17 00:00:00 2001 From: vedhavyas Date: Tue, 7 Nov 2023 14:12:35 +0100 Subject: [PATCH 7/8] short circuit earlier of the operator and domain mismatch to save host function calls and potential threshold match --- .../src/bundle_equivocation.rs | 16 ++++++++++++---- .../sp-domains-fraud-proof/src/verification.rs | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/crates/sp-domains-fraud-proof/src/bundle_equivocation.rs b/crates/sp-domains-fraud-proof/src/bundle_equivocation.rs index f751250385..3d398de7c6 100644 --- a/crates/sp-domains-fraud-proof/src/bundle_equivocation.rs +++ b/crates/sp-domains-fraud-proof/src/bundle_equivocation.rs @@ -16,6 +16,7 @@ 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. @@ -80,11 +81,18 @@ where } 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, - if previous_bundle_header.header.proof_of_election.operator_id - == bundle_header.header.proof_of_election.operator_id - { + // 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() { Ok(Some(FraudProof::BundleEquivocation( diff --git a/crates/sp-domains-fraud-proof/src/verification.rs b/crates/sp-domains-fraud-proof/src/verification.rs index db7ed571d1..58c7b2d391 100644 --- a/crates/sp-domains-fraud-proof/src/verification.rs +++ b/crates/sp-domains-fraud-proof/src/verification.rs @@ -483,6 +483,9 @@ pub enum InvalidBundleEquivocationError { /// 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. @@ -504,6 +507,20 @@ where 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; From 0e24833ad81ebcc4edabbd92cb204ad2481886c9 Mon Sep 17 00:00:00 2001 From: vedhavyas Date: Tue, 7 Nov 2023 18:03:04 +0100 Subject: [PATCH 8/8] remove dupplpicate er type alias --- crates/sp-domains-fraud-proof/src/fraud_proof.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/crates/sp-domains-fraud-proof/src/fraud_proof.rs b/crates/sp-domains-fraud-proof/src/fraud_proof.rs index ec30135b6c..9242d52e1b 100644 --- a/crates/sp-domains-fraud-proof/src/fraud_proof.rs +++ b/crates/sp-domains-fraud-proof/src/fraud_proof.rs @@ -6,23 +6,15 @@ use sp_core::H256; use sp_domain_digests::AsPredigest; use sp_domains::proof_provider_and_verifier::StorageProofVerifier; use sp_domains::{ - BundleValidity, DomainId, ExecutionReceipt, ExtrinsicDigest, HeaderHashFor, HeaderHashingFor, - InvalidBundleType, OperatorId, SealedBundleHeader, + BundleValidity, DomainId, ExecutionReceiptFor, ExtrinsicDigest, HeaderHashFor, + HeaderHashingFor, InvalidBundleType, OperatorId, SealedBundleHeader, }; -use sp_runtime::traits::{Block as BlockT, Hash as HashT, Header as HeaderT, NumberFor}; +use sp_runtime::traits::{Block as BlockT, Hash as HashT, Header as HeaderT}; use sp_runtime::{Digest, DigestItem}; use sp_std::vec::Vec; use sp_trie::StorageProof; use subspace_runtime_primitives::Balance; -type ExecutionReceiptFor = ExecutionReceipt< - NumberFor, - ::Hash, - ::Number, - ::Hash, - Balance, ->; - /// A phase of a block's execution, carrying necessary information needed for verifying the /// invalid state transition proof. #[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)]