From 26d2c25000937e0f7eda551f6750536b100629df Mon Sep 17 00:00:00 2001 From: linning Date: Fri, 8 Dec 2023 21:23:26 +0800 Subject: [PATCH 1/9] Pure refactoring for DomainBundleProducer::produce_bundle operator_id as argument So the DomainBundleProducer can be reused to produce bundle for different operator when different operator ID is pass Signed-off-by: linning --- .../src/bundle_producer_election_solver.rs | 106 +++++++++--------- .../src/domain_bundle_producer.rs | 7 +- .../src/domain_worker_starter.rs | 2 +- .../client/domain-operator/src/operator.rs | 1 - 4 files changed, 55 insertions(+), 61 deletions(-) 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 c54da1c98e..c71930716f 100644 --- a/domains/client/domain-operator/src/bundle_producer_election_solver.rs +++ b/domains/client/domain-operator/src/bundle_producer_election_solver.rs @@ -53,70 +53,68 @@ where slot: Slot, consensus_block_hash: CBlock::Hash, domain_id: DomainId, - maybe_operator_id: Option, + operator_id: OperatorId, global_randomness: Randomness, ) -> sp_blockchain::Result, OperatorPublicKey)>> { - if let Some(operator_id) = maybe_operator_id { - let BundleProducerElectionParams { - total_domain_stake, - bundle_slot_probability, - .. - } = match self - .consensus_client - .runtime_api() - .bundle_producer_election_params(consensus_block_hash, domain_id)? - { - Some(params) => params, - None => return Ok(None), - }; + let BundleProducerElectionParams { + total_domain_stake, + bundle_slot_probability, + .. + } = match self + .consensus_client + .runtime_api() + .bundle_producer_election_params(consensus_block_hash, domain_id)? + { + Some(params) => params, + None => return Ok(None), + }; - let global_challenge = global_randomness.derive_global_challenge(slot.into()); - let vrf_sign_data = make_transcript(domain_id, &global_challenge).into_sign_data(); + let global_challenge = global_randomness.derive_global_challenge(slot.into()); + let vrf_sign_data = make_transcript(domain_id, &global_challenge).into_sign_data(); - // Ideally, we can already cache operator signing key since we do not allow changing such key - // in the protocol right now. Leaving this as is since we anyway need to need to fetch operator's - // latest stake and this also returns the signing key with it. - if let Some((operator_signing_key, operator_stake)) = self - .consensus_client - .runtime_api() - .operator(consensus_block_hash, operator_id)? - { - if let Ok(maybe_vrf_signature) = Keystore::sr25519_vrf_sign( - &*self.keystore, - OperatorPublicKey::ID, - &operator_signing_key.clone().into(), - &vrf_sign_data, - ) { - if let Some(vrf_signature) = maybe_vrf_signature { - let threshold = calculate_threshold( - operator_stake, - total_domain_stake, - bundle_slot_probability, - ); + // Ideally, we can already cache operator signing key since we do not allow changing such key + // in the protocol right now. Leaving this as is since we anyway need to need to fetch operator's + // latest stake and this also returns the signing key with it. + if let Some((operator_signing_key, operator_stake)) = self + .consensus_client + .runtime_api() + .operator(consensus_block_hash, operator_id)? + { + if let Ok(maybe_vrf_signature) = Keystore::sr25519_vrf_sign( + &*self.keystore, + OperatorPublicKey::ID, + &operator_signing_key.clone().into(), + &vrf_sign_data, + ) { + if let Some(vrf_signature) = maybe_vrf_signature { + let threshold = calculate_threshold( + operator_stake, + total_domain_stake, + bundle_slot_probability, + ); - if is_below_threshold(&vrf_signature.output, threshold) { - let proof_of_election = ProofOfElection { - domain_id, - slot_number: slot.into(), - global_randomness, - vrf_signature, - operator_id, - consensus_block_hash, - }; - return Ok(Some((proof_of_election, operator_signing_key))); - } - } else { - log::warn!( + if is_below_threshold(&vrf_signature.output, threshold) { + let proof_of_election = ProofOfElection { + domain_id, + slot_number: slot.into(), + global_randomness, + vrf_signature, + operator_id, + consensus_block_hash, + }; + return Ok(Some((proof_of_election, operator_signing_key))); + } + } else { + log::warn!( "Operator[{operator_id}]'s Signing key[{}] pair is not available in keystore.", to_hex(operator_signing_key.as_slice(), false) ); - return Ok(None); - } + return Ok(None); } - } else { - log::warn!("Operator[{operator_id}] is not registered on the Runtime",); - return Ok(None); } + } else { + log::warn!("Operator[{operator_id}] is not registered on the Runtime",); + return Ok(None); } Ok(None) diff --git a/domains/client/domain-operator/src/domain_bundle_producer.rs b/domains/client/domain-operator/src/domain_bundle_producer.rs index ff6b0ee3a5..5b60a495c9 100644 --- a/domains/client/domain-operator/src/domain_bundle_producer.rs +++ b/domains/client/domain-operator/src/domain_bundle_producer.rs @@ -34,7 +34,6 @@ where CBlock: BlockT, { domain_id: DomainId, - maybe_operator_id: Option, consensus_client: Arc, client: Arc, bundle_sender: Arc>, @@ -53,7 +52,6 @@ where fn clone(&self) -> Self { Self { domain_id: self.domain_id, - maybe_operator_id: self.maybe_operator_id, consensus_client: self.consensus_client.clone(), client: self.client.clone(), bundle_sender: self.bundle_sender.clone(), @@ -81,7 +79,6 @@ where #[allow(clippy::too_many_arguments)] pub(super) fn new( domain_id: DomainId, - maybe_operator_id: Option, consensus_client: Arc, client: Arc, domain_bundle_proposer: DomainBundleProposer< @@ -101,7 +98,6 @@ where ); Self { domain_id, - maybe_operator_id, consensus_client, client, bundle_sender, @@ -114,6 +110,7 @@ where pub(super) async fn produce_bundle( self, + operator_id: OperatorId, consensus_block_info: HashAndNumber, slot_info: OperatorSlotInfo, ) -> sp_blockchain::Result>> { @@ -150,7 +147,7 @@ where slot, consensus_block_info.hash, self.domain_id, - self.maybe_operator_id, + operator_id, global_randomness, )? { diff --git a/domains/client/domain-operator/src/domain_worker_starter.rs b/domains/client/domain-operator/src/domain_worker_starter.rs index 1a9fab1cc0..7b0dc05da7 100644 --- a/domains/client/domain-operator/src/domain_worker_starter.rs +++ b/domains/client/domain-operator/src/domain_worker_starter.rs @@ -126,7 +126,7 @@ pub(super) async fn start_worker< move |consensus_block_info: sp_blockchain::HashAndNumber, slot_info| { bundle_producer .clone() - .produce_bundle(consensus_block_info.clone(), slot_info) + .produce_bundle(operator_id, consensus_block_info.clone(), slot_info) .instrument(span.clone()) .unwrap_or_else(move |error| { tracing::error!( diff --git a/domains/client/domain-operator/src/operator.rs b/domains/client/domain-operator/src/operator.rs index 6f2858a9c4..ae9c5a1da3 100644 --- a/domains/client/domain-operator/src/operator.rs +++ b/domains/client/domain-operator/src/operator.rs @@ -126,7 +126,6 @@ where let bundle_producer = DomainBundleProducer::new( params.domain_id, - params.maybe_operator_id, params.consensus_client.clone(), params.client.clone(), domain_bundle_proposer, From 4180b85a43c8bd09122c831d0dec499e9da7b586 Mon Sep 17 00:00:00 2001 From: linning Date: Fri, 8 Dec 2023 21:48:43 +0800 Subject: [PATCH 2/9] Add domain runtime API for querying operator info This commit add some domain runtime API that required by the upcoming intentional malicious operator Signed-off-by: linning --- crates/pallet-domains/src/lib.rs | 2 ++ crates/sp-domains/src/lib.rs | 10 ++++++++++ crates/subspace-runtime/src/lib.rs | 16 ++++++++++++++++ test/subspace-test-runtime/src/lib.rs | 16 ++++++++++++++++ 4 files changed, 44 insertions(+) diff --git a/crates/pallet-domains/src/lib.rs b/crates/pallet-domains/src/lib.rs index 6427baaeea..5411e1bb21 100644 --- a/crates/pallet-domains/src/lib.rs +++ b/crates/pallet-domains/src/lib.rs @@ -358,10 +358,12 @@ mod pallet { /// Indexes operator signing key against OperatorId. #[pallet::storage] + #[pallet::getter(fn operator_signing_key)] pub(super) type OperatorSigningKey = StorageMap<_, Identity, OperatorPublicKey, OperatorId, OptionQuery>; #[pallet::storage] + #[pallet::getter(fn domain_staking_summary)] pub(super) type DomainStakingSummary = StorageMap<_, Identity, DomainId, StakingSummary>, OptionQuery>; diff --git a/crates/sp-domains/src/lib.rs b/crates/sp-domains/src/lib.rs index 8d966dfd99..04616ce674 100644 --- a/crates/sp-domains/src/lib.rs +++ b/crates/sp-domains/src/lib.rs @@ -49,6 +49,7 @@ use sp_runtime::traits::{ use sp_runtime::{Digest, DigestItem, OpaqueExtrinsic, Percent}; use sp_runtime_interface::pass_by; use sp_runtime_interface::pass_by::PassBy; +use sp_std::collections::btree_map::BTreeMap; use sp_std::collections::btree_set::BTreeSet; use sp_std::fmt::{Display, Formatter}; use sp_std::vec::Vec; @@ -1007,6 +1008,15 @@ sp_api::decl_runtime_apis! { /// Returns the execution receipt fn execution_receipt(receipt_hash: HeaderHashFor) -> Option>; + + /// Returns the current epoch and the next epoch operators of the given domain + fn domain_operators(domain_id: DomainId) -> Option<(BTreeMap, Vec)>; + + /// Get operator id by signing key + fn operator_id_by_signing_key(signing_key: OperatorPublicKey) -> Option; + + /// Get the consensus chain sudo account id, currently only used in the intentional malicious operator + fn sudo_account_id() -> subspace_runtime_primitives::AccountId; } pub trait BundleProducerElectionApi { diff --git a/crates/subspace-runtime/src/lib.rs b/crates/subspace-runtime/src/lib.rs index d093598990..e275d30b4f 100644 --- a/crates/subspace-runtime/src/lib.rs +++ b/crates/subspace-runtime/src/lib.rs @@ -70,6 +70,7 @@ use sp_messenger::messages::{ use sp_runtime::traits::{AccountIdConversion, AccountIdLookup, BlakeTwo256, Convert, NumberFor}; use sp_runtime::transaction_validity::{TransactionSource, TransactionValidity}; use sp_runtime::{create_runtime_str, generic, AccountId32, ApplyExtrinsicResult, Perbill}; +use sp_std::collections::btree_map::BTreeMap; use sp_std::marker::PhantomData; use sp_std::prelude::*; #[cfg(feature = "std")] @@ -1061,6 +1062,21 @@ impl_runtime_apis! { fn execution_receipt(receipt_hash: DomainHash) -> Option> { Domains::execution_receipt(receipt_hash) } + + fn domain_operators(domain_id: DomainId) -> Option<(BTreeMap, Vec)> { + Domains::domain_staking_summary(domain_id).map(|summary| { + let next_operators = summary.next_operators.into_iter().collect(); + (summary.current_operators, next_operators) + }) + } + + fn operator_id_by_signing_key(signing_key: OperatorPublicKey) -> Option { + Domains::operator_signing_key(signing_key) + } + + fn sudo_account_id() -> AccountId { + SudoId::get() + } } impl sp_domains::BundleProducerElectionApi for Runtime { diff --git a/test/subspace-test-runtime/src/lib.rs b/test/subspace-test-runtime/src/lib.rs index 172137f47a..62aefcfc7a 100644 --- a/test/subspace-test-runtime/src/lib.rs +++ b/test/subspace-test-runtime/src/lib.rs @@ -71,6 +71,7 @@ use sp_runtime::transaction_validity::{ InvalidTransaction, TransactionSource, TransactionValidity, TransactionValidityError, }; use sp_runtime::{create_runtime_str, generic, AccountId32, ApplyExtrinsicResult, Perbill}; +use sp_std::collections::btree_map::BTreeMap; use sp_std::iter::Peekable; use sp_std::marker::PhantomData; use sp_std::prelude::*; @@ -1254,6 +1255,21 @@ impl_runtime_apis! { fn execution_receipt(receipt_hash: DomainHash) -> Option> { Domains::execution_receipt(receipt_hash) } + + fn domain_operators(domain_id: DomainId) -> Option<(BTreeMap, Vec)> { + Domains::domain_staking_summary(domain_id).map(|summary| { + let next_operators = summary.next_operators.into_iter().collect(); + (summary.current_operators, next_operators) + }) + } + + fn operator_id_by_signing_key(signing_key: OperatorPublicKey) -> Option { + Domains::operator_signing_key(signing_key) + } + + fn sudo_account_id() -> AccountId { + SudoId::get() + } } impl sp_domains::BundleProducerElectionApi for Runtime { From 72ced915e7dec91302648ab6f1ce5c8f3178b3cd Mon Sep 17 00:00:00 2001 From: linning Date: Fri, 8 Dec 2023 21:51:18 +0800 Subject: [PATCH 3/9] Export necessary structs/functions that required by the intentional malicious operator Signed-off-by: linning --- crates/pallet-domains/src/lib.rs | 5 ++++- crates/subspace-node/src/domain.rs | 6 +++--- .../subspace-node/src/domain/domain_instance_starter.rs | 2 +- crates/subspace-node/src/lib.rs | 2 +- crates/subspace-runtime/src/lib.rs | 2 +- .../client/domain-operator/src/domain_block_processor.rs | 8 ++++++++ .../client/domain-operator/src/domain_bundle_producer.rs | 6 +++--- .../client/domain-operator/src/domain_bundle_proposer.rs | 4 ++-- domains/client/domain-operator/src/domain_worker.rs | 2 +- domains/client/domain-operator/src/lib.rs | 7 ++++--- domains/client/domain-operator/src/utils.rs | 6 +++--- 11 files changed, 31 insertions(+), 19 deletions(-) diff --git a/crates/pallet-domains/src/lib.rs b/crates/pallet-domains/src/lib.rs index 5411e1bb21..481187276a 100644 --- a/crates/pallet-domains/src/lib.rs +++ b/crates/pallet-domains/src/lib.rs @@ -65,6 +65,7 @@ use sp_runtime::{RuntimeAppPublic, SaturatedConversion, Saturating}; use sp_std::boxed::Box; use sp_std::collections::btree_map::BTreeMap; use sp_std::vec::Vec; +pub use staking::OperatorConfig; use subspace_core_primitives::U256; use subspace_runtime_primitives::Balance; @@ -1390,7 +1391,9 @@ mod pallet { _ => { log::warn!( target: "runtime::domains", - "Bad bundle {:?}, error: {e:?}", opaque_bundle.domain_id(), + "Bad bundle {:?}, operator {}, error: {e:?}", + opaque_bundle.domain_id(), + opaque_bundle.operator_id(), ); } } diff --git a/crates/subspace-node/src/domain.rs b/crates/subspace-node/src/domain.rs index cd16a48b3b..2ff56d9b83 100644 --- a/crates/subspace-node/src/domain.rs +++ b/crates/subspace-node/src/domain.rs @@ -16,11 +16,11 @@ pub(crate) mod cli; pub(crate) mod domain_instance_starter; -pub(crate) mod evm_chain_spec; +pub mod evm_chain_spec; pub use self::cli::{DomainCli, Subcommand as DomainSubcommand}; -pub use self::domain_instance_starter::DomainInstanceStarter; -use evm_domain_runtime::AccountId as AccountId20; +pub use self::domain_instance_starter::{create_configuration, DomainInstanceStarter}; +pub use evm_domain_runtime::AccountId as AccountId20; use sc_executor::NativeExecutionDispatch; /// EVM domain executor instance. diff --git a/crates/subspace-node/src/domain/domain_instance_starter.rs b/crates/subspace-node/src/domain/domain_instance_starter.rs index 83f4e935d7..42ad22d47c 100644 --- a/crates/subspace-node/src/domain/domain_instance_starter.rs +++ b/crates/subspace-node/src/domain/domain_instance_starter.rs @@ -179,7 +179,7 @@ pub(crate) const DEFAULT_NETWORK_CONFIG_PATH: &str = "network"; /// Create a Configuration object from the current object, port from `sc_cli::create_configuration` /// and changed to take `chain_spec` as argument instead of construct one internally. -fn create_configuration< +pub fn create_configuration< DCV: DefaultConfigurationValues, CC: CliConfiguration, Cli: SubstrateCli, diff --git a/crates/subspace-node/src/lib.rs b/crates/subspace-node/src/lib.rs index a465e47931..71b7e58c2a 100644 --- a/crates/subspace-node/src/lib.rs +++ b/crates/subspace-node/src/lib.rs @@ -16,7 +16,7 @@ //! Subspace Node library. -mod chain_spec; +pub mod chain_spec; mod chain_spec_utils; pub mod domain; diff --git a/crates/subspace-runtime/src/lib.rs b/crates/subspace-runtime/src/lib.rs index e275d30b4f..a3a0ffb632 100644 --- a/crates/subspace-runtime/src/lib.rs +++ b/crates/subspace-runtime/src/lib.rs @@ -30,7 +30,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); use crate::fees::{OnChargeTransaction, TransactionByteFee}; use crate::object_mapping::extract_block_object_mapping; -use crate::signed_extensions::{CheckStorageAccess, DisablePallets}; +pub use crate::signed_extensions::{CheckStorageAccess, DisablePallets}; use codec::{Decode, Encode, MaxEncodedLen}; use core::num::NonZeroU64; use domain_runtime_primitives::opaque::Header as DomainHeader; diff --git a/domains/client/domain-operator/src/domain_block_processor.rs b/domains/client/domain-operator/src/domain_block_processor.rs index 5f923567ed..522e59ed48 100644 --- a/domains/client/domain-operator/src/domain_block_processor.rs +++ b/domains/client/domain-operator/src/domain_block_processor.rs @@ -956,6 +956,14 @@ where )) })?; + tracing::info!( + ?bad_receipt_hash, + ?mismatch_info, + "Generating fraud proof, domain block {}#{}", + local_receipt.domain_block_number, + local_receipt.domain_block_hash + ); + let fraud_proof = match mismatch_info { ReceiptMismatchInfo::Trace { trace_index, .. } => self .fraud_proof_generator diff --git a/domains/client/domain-operator/src/domain_bundle_producer.rs b/domains/client/domain-operator/src/domain_bundle_producer.rs index 5b60a495c9..6e19101d0e 100644 --- a/domains/client/domain-operator/src/domain_bundle_producer.rs +++ b/domains/client/domain-operator/src/domain_bundle_producer.rs @@ -28,7 +28,7 @@ type OpaqueBundle = sp_domains::OpaqueBundle< Balance, >; -pub(super) struct DomainBundleProducer +pub struct DomainBundleProducer where Block: BlockT, CBlock: BlockT, @@ -77,7 +77,7 @@ where TransactionPool: sc_transaction_pool_api::TransactionPool, { #[allow(clippy::too_many_arguments)] - pub(super) fn new( + pub fn new( domain_id: DomainId, consensus_client: Arc, client: Arc, @@ -108,7 +108,7 @@ where } } - pub(super) async fn produce_bundle( + pub async fn produce_bundle( self, operator_id: OperatorId, consensus_block_info: HashAndNumber, diff --git a/domains/client/domain-operator/src/domain_bundle_proposer.rs b/domains/client/domain-operator/src/domain_bundle_proposer.rs index 971024a1fb..567a7dd958 100644 --- a/domains/client/domain-operator/src/domain_bundle_proposer.rs +++ b/domains/client/domain-operator/src/domain_bundle_proposer.rs @@ -19,7 +19,7 @@ use std::time; use subspace_core_primitives::U256; use subspace_runtime_primitives::Balance; -pub(super) struct DomainBundleProposer { +pub struct DomainBundleProposer { domain_id: DomainId, client: Arc, consensus_client: Arc, @@ -58,7 +58,7 @@ where CClient::Api: DomainsApi, TransactionPool: sc_transaction_pool_api::TransactionPool, { - pub(crate) fn new( + pub fn new( domain_id: DomainId, client: Arc, consensus_client: Arc, diff --git a/domains/client/domain-operator/src/domain_worker.rs b/domains/client/domain-operator/src/domain_worker.rs index 6fd4165699..f8ba54f9a7 100644 --- a/domains/client/domain-operator/src/domain_worker.rs +++ b/domains/client/domain-operator/src/domain_worker.rs @@ -15,7 +15,7 @@ use std::pin::Pin; use std::sync::Arc; use subspace_runtime_primitives::Balance; -type OpaqueBundleFor = +pub type OpaqueBundleFor = OpaqueBundle, ::Hash, ::Header, Balance>; /// Throttle the consensus block import notification based on the `consensus_block_import_throttling_buffer_size` diff --git a/domains/client/domain-operator/src/lib.rs b/domains/client/domain-operator/src/lib.rs index 903889ad84..9b28d7ec94 100644 --- a/domains/client/domain-operator/src/lib.rs +++ b/domains/client/domain-operator/src/lib.rs @@ -67,7 +67,7 @@ mod bundle_processor; mod bundle_producer_election_solver; mod domain_block_processor; pub mod domain_bundle_producer; -mod domain_bundle_proposer; +pub mod domain_bundle_proposer; mod domain_worker; mod domain_worker_starter; mod fraud_proof; @@ -79,7 +79,8 @@ mod utils; pub use self::aux_schema::load_execution_receipt; pub use self::bootstrapper::{BootstrapResult, Bootstrapper}; pub use self::operator::Operator; -pub use self::utils::{DomainBlockImportNotification, DomainImportNotifications}; +pub use self::utils::{DomainBlockImportNotification, DomainImportNotifications, OperatorSlotInfo}; +pub use domain_worker::OpaqueBundleFor; use futures::channel::mpsc; use futures::Stream; use sc_client_api::{AuxStore, BlockImportNotification}; @@ -99,7 +100,7 @@ use std::sync::Arc; use subspace_core_primitives::Randomness; use subspace_runtime_primitives::Balance; -type ExecutionReceiptFor = ExecutionReceipt< +pub type ExecutionReceiptFor = ExecutionReceipt< NumberFor, ::Hash, NumberFor, diff --git a/domains/client/domain-operator/src/utils.rs b/domains/client/domain-operator/src/utils.rs index 02fee232a6..ab94c1a8bf 100644 --- a/domains/client/domain-operator/src/utils.rs +++ b/domains/client/domain-operator/src/utils.rs @@ -7,11 +7,11 @@ use subspace_core_primitives::Randomness; /// Data required to produce bundles on executor node. #[derive(PartialEq, Clone, Debug)] -pub(super) struct OperatorSlotInfo { +pub struct OperatorSlotInfo { /// Slot - pub(super) slot: Slot, + pub slot: Slot, /// Global randomness - pub(super) global_randomness: Randomness, + pub global_randomness: Randomness, } #[derive(Debug, Clone)] From da5aa206a26a574eb9a247e21d1e9f23db6f45dc Mon Sep 17 00:00:00 2001 From: linning Date: Fri, 8 Dec 2023 21:58:28 +0800 Subject: [PATCH 4/9] Introduce the subspace-malicious-operator crate This crate contains the minimal required infra for setup an operator node, most of the code are ported from subspace-node, it will be used as the base of the malicious operator Signed-off-by: linning --- crates/subspace-malicious-operator/Cargo.toml | 80 ++++ crates/subspace-malicious-operator/build.rs | 23 + .../src/bin/subspace-malicious-operator.rs | 432 ++++++++++++++++++ crates/subspace-malicious-operator/src/lib.rs | 19 + .../src/malicious_domain_instance_starter.rs | 180 ++++++++ 5 files changed, 734 insertions(+) create mode 100644 crates/subspace-malicious-operator/Cargo.toml create mode 100644 crates/subspace-malicious-operator/build.rs create mode 100644 crates/subspace-malicious-operator/src/bin/subspace-malicious-operator.rs create mode 100644 crates/subspace-malicious-operator/src/lib.rs create mode 100644 crates/subspace-malicious-operator/src/malicious_domain_instance_starter.rs diff --git a/crates/subspace-malicious-operator/Cargo.toml b/crates/subspace-malicious-operator/Cargo.toml new file mode 100644 index 0000000000..05596cdbd1 --- /dev/null +++ b/crates/subspace-malicious-operator/Cargo.toml @@ -0,0 +1,80 @@ +[package] +name = "subspace-malicious-operator" +version = "0.1.0" +authors = ["Subspace Labs "] +description = "A Subspace Network Blockchain node." +edition = "2021" +license = "GPL-3.0-or-later" +build = "build.rs" +homepage = "https://subspace.network" +repository = "https://github.com/subspace/subspace" +include = [ + "/src", + "/build.rs", + "/Cargo.toml", + "/README.md" +] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +cross-domain-message-gossip = { version = "0.1.0", path = "../../domains/client/cross-domain-message-gossip" } +domain-client-message-relayer = { version = "0.1.0", path = "../../domains/client/relayer" } +domain-client-operator = { version = "0.1.0", path = "../../domains/client/domain-operator" } +domain-eth-service = { version = "0.1.0", path = "../../domains/client/eth-service" } +domain-service = { version = "0.1.0", path = "../../domains/service" } +domain-runtime-primitives = { version = "0.1.0", path = "../../domains/primitives/runtime" } +evm-domain-runtime = { version = "0.1.0", path = "../../domains/runtime/evm" } +frame-system = { version = "4.0.0-dev", default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +frame-system-rpc-runtime-api = { version = "4.0.0-dev", default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +futures = "0.3.29" +log = "0.4.20" +mimalloc = "0.1.39" +pallet-domains = { version = "0.1.0", default-features = false, path = "../pallet-domains" } +pallet-transaction-payment = { version = "4.0.0-dev", default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +parity-scale-codec = "3.6.5" +pallet-sudo = { version = "4.0.0-dev", default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +sc-chain-spec = { version = "4.0.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +sc-cli = { version = "0.10.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c", default-features = false } +sc-client-api = { version = "4.0.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +sc-consensus-slots = { version = "0.10.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +sc-consensus-subspace = { version = "0.1.0", path = "../sc-consensus-subspace" } +sc-network = { version = "0.10.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +sc-service = { version = "0.10.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c", default-features = false } +sc-storage-monitor = { version = "0.1.0", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c", default-features = false } +sc-tracing = { version = "4.0.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +sc-transaction-pool-api = { version = "4.0.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +sc-network-sync = { version = "0.10.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +sc-utils = { version = "4.0.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +serde_json = "1.0.106" +sp-api = { version = "4.0.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +sp-blockchain = { version = "4.0.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +sp-block-builder = { git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c", default-features = false, version = "4.0.0-dev" } +sp-consensus-subspace = { version = "0.1.0", path = "../sp-consensus-subspace" } +sp-consensus-slots = { version = "0.10.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +sp-core = { version = "21.0.0", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +sp-domains = { version = "0.1.0", path = "../sp-domains" } +sp-domain-digests = { version = "0.1.0", path = "../../domains/primitives/digests" } +sp-transaction-pool = { version = "4.0.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +sp-messenger = { version = "0.1.0", path = "../../domains/primitives/messenger" } +sp-runtime = { version = "24.0.0", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +sp-keystore = { version = "0.27.0", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +sp-keyring = { git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } +subspace-core-primitives = { version = "0.1.0", path = "../subspace-core-primitives" } +subspace-networking = { version = "0.1.0", path = "../subspace-networking" } +subspace-proof-of-space = { version = "0.1.0", path = "../subspace-proof-of-space" } +subspace-runtime = { version = "0.1.0", path = "../subspace-runtime" } +subspace-runtime-primitives = { version = "0.1.0", path = "../subspace-runtime-primitives" } +subspace-node = { version = "0.1.0", path = "../subspace-node" } +subspace-service = { version = "0.1.0", path = "../subspace-service" } +thiserror = "1.0.48" +tokio = "1.34.0" +rand = "0.8.5" +tracing = "0.1.37" + +[build-dependencies] +substrate-build-script-utils = { version = "3.0.0", git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c" } + +[features] +default = [] diff --git a/crates/subspace-malicious-operator/build.rs b/crates/subspace-malicious-operator/build.rs new file mode 100644 index 0000000000..e69d6d8b7a --- /dev/null +++ b/crates/subspace-malicious-operator/build.rs @@ -0,0 +1,23 @@ +// Copyright (C) 2023 Subspace Labs, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use substrate_build_script_utils::{generate_cargo_keys, rerun_if_git_head_changed}; + +fn main() { + generate_cargo_keys(); + + rerun_if_git_head_changed(); +} diff --git a/crates/subspace-malicious-operator/src/bin/subspace-malicious-operator.rs b/crates/subspace-malicious-operator/src/bin/subspace-malicious-operator.rs new file mode 100644 index 0000000000..fadadf5e32 --- /dev/null +++ b/crates/subspace-malicious-operator/src/bin/subspace-malicious-operator.rs @@ -0,0 +1,432 @@ +// Copyright (C) 2023 Subspace Labs, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Subspace malicious operator node. + +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + +use cross_domain_message_gossip::GossipWorkerBuilder; +use domain_client_operator::Bootstrapper; +use domain_runtime_primitives::opaque::Block as DomainBlock; +use log::warn; +use sc_cli::{ChainSpec, CliConfiguration, SubstrateCli}; +use sc_consensus_slots::SlotProportion; +use sc_service::Configuration; +use sc_storage_monitor::StorageMonitorService; +use sc_transaction_pool_api::OffchainTransactionPoolFactory; +use sc_utils::mpsc::tracing_unbounded; +use sp_core::crypto::Ss58AddressFormat; +use sp_core::traits::SpawnEssentialNamed; +use sp_messenger::messages::ChainId; +use subspace_malicious_operator::malicious_domain_instance_starter::DomainInstanceStarter; +use subspace_node::domain::DomainCli; +use subspace_node::{Cli, ExecutorDispatch}; +use subspace_proof_of_space::chia::ChiaTable; +use subspace_runtime::{Block, RuntimeApi}; +use subspace_service::{DsnConfig, SubspaceConfiguration, SubspaceNetworking}; + +type PosTable = ChiaTable; + +/// Subspace node error. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Subspace service error. + #[error(transparent)] + SubspaceService(#[from] subspace_service::Error), + + /// CLI error. + #[error(transparent)] + SubstrateCli(#[from] sc_cli::Error), + + /// Substrate service error. + #[error(transparent)] + SubstrateService(#[from] sc_service::Error), + + /// Other kind of error. + #[error("Other: {0}")] + Other(String), +} + +impl From for Error { + #[inline] + fn from(s: String) -> Self { + Self::Other(s) + } +} + +fn set_default_ss58_version>(chain_spec: C) { + let maybe_ss58_address_format = chain_spec + .as_ref() + .properties() + .get("ss58Format") + .map(|v| { + v.as_u64() + .expect("ss58Format must always be an unsigned number; qed") + }) + .map(|v| { + v.try_into() + .expect("ss58Format must always be within u16 range; qed") + }) + .map(Ss58AddressFormat::custom); + + if let Some(ss58_address_format) = maybe_ss58_address_format { + sp_core::crypto::set_default_ss58_version(ss58_address_format); + } +} + +fn pot_external_entropy( + consensus_chain_config: &Configuration, + cli: &Cli, +) -> Result, sc_service::Error> { + let maybe_chain_spec_pot_external_entropy = consensus_chain_config + .chain_spec + .properties() + .get("potExternalEntropy") + .map(|d| serde_json::from_value(d.clone())) + .transpose() + .map_err(|error| { + sc_service::Error::Other(format!("Failed to decode PoT initial key: {error:?}")) + })? + .flatten(); + if maybe_chain_spec_pot_external_entropy.is_some() + && cli.pot_external_entropy.is_some() + && maybe_chain_spec_pot_external_entropy != cli.pot_external_entropy + { + warn!( + "--pot-external-entropy CLI argument was ignored due to chain spec having a different \ + explicit value" + ); + } + Ok(maybe_chain_spec_pot_external_entropy + .or(cli.pot_external_entropy.clone()) + .unwrap_or_default()) +} + +fn main() -> Result<(), Error> { + let mut cli = Cli::from_args(); + // Force UTC logs for Subspace node + cli.run.shared_params.use_utc_log_time = true; + + let runner = cli.create_runner(&cli.run)?; + set_default_ss58_version(&runner.config().chain_spec); + runner.run_node_until_exit(|consensus_chain_config| async move { + let tokio_handle = consensus_chain_config.tokio_handle.clone(); + let database_source = consensus_chain_config.database.clone(); + + let domains_bootstrap_nodes: serde_json::map::Map = + consensus_chain_config + .chain_spec + .properties() + .get("domainsBootstrapNodes") + .map(|d| serde_json::from_value(d.clone())) + .transpose() + .map_err(|error| { + sc_service::Error::Other(format!( + "Failed to decode Domains bootstrap nodes: {error:?}" + )) + })? + .unwrap_or_default(); + + let consensus_state_pruning_mode = consensus_chain_config + .state_pruning + .clone() + .unwrap_or_default(); + let (consensus_chain_node, consensus_keystore) = { + let span = sc_tracing::tracing::info_span!( + sc_tracing::logging::PREFIX_LOG_SPAN, + name = "Consensus" + ); + let _enter = span.enter(); + + let pot_external_entropy = pot_external_entropy(&consensus_chain_config, &cli)?; + + let dsn_config = { + let network_keypair = consensus_chain_config + .network + .node_key + .clone() + .into_keypair() + .map_err(|error| { + sc_service::Error::Other(format!( + "Failed to convert network keypair: {error:?}" + )) + })?; + + let dsn_bootstrap_nodes = if cli.dsn_bootstrap_nodes.is_empty() { + consensus_chain_config + .chain_spec + .properties() + .get("dsnBootstrapNodes") + .map(|d| serde_json::from_value(d.clone())) + .transpose() + .map_err(|error| { + sc_service::Error::Other(format!( + "Failed to decode DSN bootstrap nodes: {error:?}" + )) + })? + .unwrap_or_default() + } else { + cli.dsn_bootstrap_nodes + }; + + // TODO: Libp2p versions for Substrate and Subspace diverged. + // We get type compatibility by encoding and decoding the original keypair. + let encoded_keypair = network_keypair + .to_protobuf_encoding() + .expect("Keypair-to-protobuf encoding should succeed."); + let keypair = + subspace_networking::libp2p::identity::Keypair::from_protobuf_encoding( + &encoded_keypair, + ) + .expect("Keypair-from-protobuf decoding should succeed."); + + DsnConfig { + keypair, + base_path: consensus_chain_config.base_path.path().into(), + listen_on: cli.dsn_listen_on, + bootstrap_nodes: dsn_bootstrap_nodes, + reserved_peers: cli.dsn_reserved_peers, + // Override enabling private IPs with --dev + allow_non_global_addresses_in_dht: cli.dsn_enable_private_ips + || cli.run.shared_params.dev, + max_in_connections: cli.dsn_in_connections, + max_out_connections: cli.dsn_out_connections, + max_pending_in_connections: cli.dsn_pending_in_connections, + max_pending_out_connections: cli.dsn_pending_out_connections, + external_addresses: cli.dsn_external_addresses, + // Override initial Kademlia bootstrapping with --dev + disable_bootstrap_on_start: cli.dsn_disable_bootstrap_on_start + || cli.run.shared_params.dev, + } + }; + + let consensus_chain_config = SubspaceConfiguration { + base: consensus_chain_config, + // Domain node needs slots notifications for bundle production. + force_new_slot_notifications: !cli.domain_args.is_empty(), + subspace_networking: SubspaceNetworking::Create { config: dsn_config }, + sync_from_dsn: cli.sync_from_dsn, + enable_subspace_block_relay: cli.enable_subspace_block_relay, + // Timekeeper is enabled if `--dev` is used + is_timekeeper: cli.timekeeper || cli.run.shared_params.dev, + timekeeper_cpu_cores: cli.timekeeper_cpu_cores, + }; + + let partial_components = subspace_service::new_partial::< + PosTable, + RuntimeApi, + ExecutorDispatch, + >( + &consensus_chain_config.base, &pot_external_entropy + ) + .map_err(|error| { + sc_service::Error::Other(format!("Failed to build a full subspace node: {error:?}")) + })?; + + let keystore = partial_components.keystore_container.keystore(); + + let consensus_chain_node = subspace_service::new_full::( + consensus_chain_config, + partial_components, + true, + SlotProportion::new(3f32 / 4f32), + ) + .await + .map_err(|error| { + sc_service::Error::Other(format!("Failed to build a full subspace node: {error:?}")) + })?; + + (consensus_chain_node, keystore) + }; + + StorageMonitorService::try_spawn( + cli.storage_monitor, + database_source, + &consensus_chain_node.task_manager.spawn_essential_handle(), + ) + .map_err(|error| { + sc_service::Error::Other(format!("Failed to start storage monitor: {error:?}")) + })?; + + // Run a domain node. + if !cli.domain_args.is_empty() { + let span = sc_tracing::tracing::info_span!( + sc_tracing::logging::PREFIX_LOG_SPAN, + name = "Domain" + ); + let _enter = span.enter(); + + let mut domain_cli = DomainCli::new( + cli.run + .base_path()? + .map(|base_path| base_path.path().to_path_buf()), + cli.domain_args.into_iter(), + ); + + let domain_id = domain_cli.domain_id; + + if domain_cli.run.network_params.bootnodes.is_empty() { + domain_cli.run.network_params.bootnodes = domains_bootstrap_nodes + .get(&format!("{}", domain_id)) + .map(|d| serde_json::from_value(d.clone())) + .transpose() + .map_err(|error| { + sc_service::Error::Other(format!( + "Failed to decode Domain: {} bootstrap nodes: {error:?}", + domain_id + )) + })? + .unwrap_or_default(); + } + + // start relayer for consensus chain + let mut xdm_gossip_worker_builder = GossipWorkerBuilder::new(); + { + let span = sc_tracing::tracing::info_span!( + sc_tracing::logging::PREFIX_LOG_SPAN, + name = "Consensus" + ); + let _enter = span.enter(); + + let relayer_worker = + domain_client_message_relayer::worker::relay_consensus_chain_messages( + consensus_chain_node.client.clone(), + consensus_state_pruning_mode, + consensus_chain_node.sync_service.clone(), + xdm_gossip_worker_builder.gossip_msg_sink(), + ); + + consensus_chain_node + .task_manager + .spawn_essential_handle() + .spawn_essential_blocking( + "consensus-chain-relayer", + None, + Box::pin(relayer_worker), + ); + + let (consensus_msg_sink, consensus_msg_receiver) = + tracing_unbounded("consensus_message_channel", 100); + + // Start cross domain message listener for Consensus chain to receive messages from domains in the network + let consensus_listener = + cross_domain_message_gossip::start_cross_chain_message_listener( + ChainId::Consensus, + consensus_chain_node.client.clone(), + consensus_chain_node.transaction_pool.clone(), + consensus_chain_node.network_service.clone(), + consensus_msg_receiver, + ); + + consensus_chain_node + .task_manager + .spawn_essential_handle() + .spawn_essential_blocking( + "consensus-message-listener", + None, + Box::pin(consensus_listener), + ); + + xdm_gossip_worker_builder + .push_chain_tx_pool_sink(ChainId::Consensus, consensus_msg_sink); + } + + let bootstrapper = + Bootstrapper::::new(consensus_chain_node.client.clone()); + + let (domain_message_sink, domain_message_receiver) = + tracing_unbounded("domain_message_channel", 100); + + xdm_gossip_worker_builder + .push_chain_tx_pool_sink(ChainId::Domain(domain_id), domain_message_sink); + + let domain_starter = DomainInstanceStarter { + domain_cli, + tokio_handle, + consensus_client: consensus_chain_node.client.clone(), + consensus_keystore, + consensus_offchain_tx_pool_factory: OffchainTransactionPoolFactory::new( + consensus_chain_node.transaction_pool.clone(), + ), + consensus_network: consensus_chain_node.network_service.clone(), + block_importing_notification_stream: consensus_chain_node + .block_importing_notification_stream + .clone(), + new_slot_notification_stream: consensus_chain_node + .new_slot_notification_stream + .clone(), + consensus_sync_service: consensus_chain_node.sync_service.clone(), + domain_message_receiver, + gossip_message_sink: xdm_gossip_worker_builder.gossip_msg_sink(), + }; + + consensus_chain_node + .task_manager + .spawn_essential_handle() + .spawn_essential_blocking( + "domain", + None, + Box::pin(async move { + let bootstrap_result = + match bootstrapper.fetch_domain_bootstrap_info(domain_id).await { + Err(err) => { + log::error!("Domain bootstrapper exited with an error {err:?}"); + return; + } + Ok(res) => res, + }; + if let Err(error) = domain_starter.start(bootstrap_result).await { + log::error!("Domain starter exited with an error {error:?}"); + } + }), + ); + + let cross_domain_message_gossip_worker = xdm_gossip_worker_builder + .build::( + consensus_chain_node.network_service.clone(), + consensus_chain_node.sync_service.clone(), + ); + + consensus_chain_node + .task_manager + .spawn_essential_handle() + .spawn_essential_blocking( + "cross-domain-gossip-message-worker", + None, + Box::pin(cross_domain_message_gossip_worker.run()), + ); + }; + + consensus_chain_node.network_starter.start_network(); + Ok::<_, Error>(consensus_chain_node.task_manager) + })?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use sc_cli::Database; + + #[test] + fn rocksdb_disabled_in_substrate() { + assert_eq!( + Database::variants(), + &["paritydb", "paritydb-experimental", "auto"], + ); + } +} diff --git a/crates/subspace-malicious-operator/src/lib.rs b/crates/subspace-malicious-operator/src/lib.rs new file mode 100644 index 0000000000..3a29bdbca6 --- /dev/null +++ b/crates/subspace-malicious-operator/src/lib.rs @@ -0,0 +1,19 @@ +// Copyright (C) 2023 Subspace Labs, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Subspace malicious operator library. + +pub mod malicious_domain_instance_starter; diff --git a/crates/subspace-malicious-operator/src/malicious_domain_instance_starter.rs b/crates/subspace-malicious-operator/src/malicious_domain_instance_starter.rs new file mode 100644 index 0000000000..c694747bfa --- /dev/null +++ b/crates/subspace-malicious-operator/src/malicious_domain_instance_starter.rs @@ -0,0 +1,180 @@ +use crate::malicious_bundle_producer::MaliciousBundleProducer; +use cross_domain_message_gossip::{ChainTxPoolMsg, Message}; +use domain_client_operator::{BootstrapResult, OperatorStreams}; +use domain_eth_service::provider::EthProvider; +use domain_eth_service::DefaultEthConfig; +use domain_runtime_primitives::opaque::Block as DomainBlock; +use domain_service::{FullBackend, FullClient}; +use futures::StreamExt; +use sc_cli::CliConfiguration; +use sc_consensus_subspace::block_import::BlockImportingNotification; +use sc_consensus_subspace::notification::SubspaceNotificationStream; +use sc_consensus_subspace::slot_worker::NewSlotNotification; +use sc_network::NetworkPeers; +use sc_service::BasePath; +use sc_transaction_pool_api::OffchainTransactionPoolFactory; +use sc_utils::mpsc::{TracingUnboundedReceiver, TracingUnboundedSender}; +use sp_core::traits::SpawnEssentialNamed; +use sp_domains::{DomainInstanceData, RuntimeType}; +use sp_keystore::KeystorePtr; +use std::sync::Arc; +use subspace_node::domain::{ + create_configuration, evm_chain_spec, AccountId20, DomainCli, EVMDomainExecutorDispatch, +}; +use subspace_node::ExecutorDispatch as CExecutorDispatch; +use subspace_runtime::RuntimeApi as CRuntimeApi; +use subspace_runtime_primitives::opaque::Block as CBlock; +use subspace_service::FullClient as CFullClient; + +/// `DomainInstanceStarter` used to start a domain instance node based on the given +/// bootstrap result +pub struct DomainInstanceStarter { + pub domain_cli: DomainCli, + pub tokio_handle: tokio::runtime::Handle, + pub consensus_client: Arc>, + pub consensus_keystore: KeystorePtr, + pub consensus_offchain_tx_pool_factory: OffchainTransactionPoolFactory, + pub block_importing_notification_stream: + SubspaceNotificationStream>, + pub new_slot_notification_stream: SubspaceNotificationStream, + pub consensus_sync_service: Arc>, + pub domain_message_receiver: TracingUnboundedReceiver, + pub gossip_message_sink: TracingUnboundedSender, + pub consensus_network: Arc, +} + +impl DomainInstanceStarter +where + CNetwork: NetworkPeers + Send + Sync + 'static, +{ + pub async fn start( + self, + bootstrap_result: BootstrapResult, + ) -> std::result::Result<(), Box> { + let BootstrapResult { + domain_instance_data, + domain_created_at, + imported_block_notification_stream, + } = bootstrap_result; + + let DomainInstanceData { + runtime_type, + raw_genesis, + } = domain_instance_data; + + let DomainInstanceStarter { + domain_cli, + tokio_handle, + consensus_client, + consensus_keystore, + consensus_offchain_tx_pool_factory, + block_importing_notification_stream, + new_slot_notification_stream, + consensus_sync_service, + domain_message_receiver, + gossip_message_sink, + consensus_network, + } = self; + + let domain_id = domain_cli.domain_id; + let domain_config = { + let chain_id = domain_cli.chain_id(domain_cli.is_dev()?)?; + + let domain_spec = evm_chain_spec::create_domain_spec(chain_id.as_str(), raw_genesis)?; + + create_configuration::<_, DomainCli, DomainCli>(&domain_cli, domain_spec, tokio_handle)? + }; + + let block_importing_notification_stream = || { + block_importing_notification_stream.subscribe().then( + |block_importing_notification| async move { + ( + block_importing_notification.block_number, + block_importing_notification.acknowledgement_sender, + ) + }, + ) + }; + + let new_slot_notification_stream = || { + new_slot_notification_stream + .subscribe() + .then(|slot_notification| async move { + ( + slot_notification.new_slot_info.slot, + slot_notification.new_slot_info.global_randomness, + ) + }) + }; + + let operator_streams = OperatorStreams { + // TODO: proper value + consensus_block_import_throttling_buffer_size: 10, + block_importing_notification_stream: block_importing_notification_stream(), + imported_block_notification_stream, + new_slot_notification_stream: new_slot_notification_stream(), + acknowledgement_sender_stream: futures::stream::empty(), + _phantom: Default::default(), + }; + + match runtime_type { + RuntimeType::Evm => { + let evm_base_path = BasePath::new( + domain_config + .base_path + .config_dir(domain_config.chain_spec.id()), + ); + + let eth_provider = + EthProvider::< + evm_domain_runtime::TransactionConverter, + DefaultEthConfig< + FullClient< + DomainBlock, + evm_domain_runtime::RuntimeApi, + EVMDomainExecutorDispatch, + >, + FullBackend, + >, + >::new(Some(evm_base_path), domain_cli.additional_args()); + + let domain_params = domain_service::DomainParams { + domain_id, + domain_config, + domain_created_at, + consensus_client: consensus_client.clone(), + consensus_offchain_tx_pool_factory: consensus_offchain_tx_pool_factory.clone(), + consensus_network, + consensus_network_sync_oracle: consensus_sync_service.clone(), + operator_streams, + gossip_message_sink, + domain_message_receiver, + provider: eth_provider, + skip_empty_bundle_production: true, + maybe_operator_id: domain_cli.operator_id, + }; + + let mut domain_node = domain_service::new_full::< + _, + _, + _, + _, + _, + _, + evm_domain_runtime::RuntimeApi, + EVMDomainExecutorDispatch, + AccountId20, + _, + _, + >(domain_params) + .await?; + + domain_node.network_starter.start_network(); + + domain_node.task_manager.future().await?; + + Ok(()) + } + } + } +} From a9c0b265d2ddb2fd1d6a54e74e7d0af6dd02778c Mon Sep 17 00:00:00 2001 From: linning Date: Fri, 8 Dec 2023 22:05:10 +0800 Subject: [PATCH 5/9] Introduce the MaliciousBundleTamper It is will be used within the malicious operator to randomly temper normal bundle to be invalid bundle and/or contains bad ER with probability Signed-off-by: linning --- crates/subspace-malicious-operator/src/lib.rs | 1 + .../src/malicious_bundle_tamper.rs | 307 ++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 crates/subspace-malicious-operator/src/malicious_bundle_tamper.rs diff --git a/crates/subspace-malicious-operator/src/lib.rs b/crates/subspace-malicious-operator/src/lib.rs index 3a29bdbca6..3f37407f8e 100644 --- a/crates/subspace-malicious-operator/src/lib.rs +++ b/crates/subspace-malicious-operator/src/lib.rs @@ -16,4 +16,5 @@ //! Subspace malicious operator library. +mod malicious_bundle_tamper; pub mod malicious_domain_instance_starter; diff --git a/crates/subspace-malicious-operator/src/malicious_bundle_tamper.rs b/crates/subspace-malicious-operator/src/malicious_bundle_tamper.rs new file mode 100644 index 0000000000..baed809796 --- /dev/null +++ b/crates/subspace-malicious-operator/src/malicious_bundle_tamper.rs @@ -0,0 +1,307 @@ +use domain_client_operator::{ExecutionReceiptFor, OpaqueBundleFor}; +use domain_runtime_primitives::DomainCoreApi; +use parity_scale_codec::{Decode, Encode}; +use sc_client_api::HeaderBackend; +use sp_api::{HashT, HeaderT, ProvideRuntimeApi}; +use sp_domain_digests::AsPredigest; +use sp_domains::merkle_tree::MerkleTree; +use sp_domains::{ + BundleValidity, HeaderHashingFor, InvalidBundleType, OperatorPublicKey, OperatorSignature, +}; +use sp_keystore::KeystorePtr; +use sp_runtime::traits::{Block as BlockT, NumberFor, One, Zero}; +use sp_runtime::{DigestItem, OpaqueExtrinsic, RuntimeAppPublic}; +use std::collections::{BTreeMap, HashMap}; +use std::error::Error; +use std::sync::Arc; + +const MAX_BAD_RECEIPT_CACHE: u32 = 128; + +#[allow(dead_code)] +#[derive(Debug)] +enum BadReceiptType { + TotalRewards, + ExecutionTrace, + ExtrinsicsRoot, + DomainBlockHash, + InboxedBundle, + ParentReceipt, +} + +struct Random; + +impl Random { + fn seed() -> u32 { + rand::random::() + } + + // Return `true` based on the given probability + fn probability(p: f64) -> bool { + assert!(p <= 1f64); + Self::seed() < ((u32::MAX as f64) * p) as u32 + } +} + +#[allow(clippy::type_complexity)] +pub struct MaliciousBundleTamper +where + Block: BlockT, + CBlock: BlockT, +{ + domain_client: Arc, + keystore: KeystorePtr, + // A cache for recently produced bad receipts + bad_receipts_cache: + BTreeMap, HashMap>>, +} + +impl MaliciousBundleTamper +where + Block: BlockT, + CBlock: BlockT, + CBlock::Hash: Decode, + Client: HeaderBackend + ProvideRuntimeApi + 'static, + Client::Api: DomainCoreApi, +{ + pub fn new(domain_client: Arc, keystore: KeystorePtr) -> Self { + MaliciousBundleTamper { + domain_client, + keystore, + bad_receipts_cache: BTreeMap::new(), + } + } + + pub fn maybe_tamper_bundle( + &mut self, + opaque_bundle: &mut OpaqueBundleFor, + operator_signing_key: &OperatorPublicKey, + ) -> Result<(), Box> { + if Random::probability(0.5) { + self.make_receipt_fraudulent(&mut opaque_bundle.sealed_header.header.receipt)?; + self.reseal_bundle(opaque_bundle, operator_signing_key)?; + } + if Random::probability(0.3) { + self.make_bundle_invalid(opaque_bundle)?; + self.reseal_bundle(opaque_bundle, operator_signing_key)?; + } + Ok(()) + } + + fn make_receipt_fraudulent( + &mut self, + receipt: &mut ExecutionReceiptFor, + ) -> Result<(), Box> { + // We can't make the genesis receipt into a bad ER + if receipt.domain_block_number.is_zero() { + return Ok(()); + } + // If a bad receipt is already made for the same domain block, reuse it + if let Some(bad_receipts_at) = self.bad_receipts_cache.get(&receipt.domain_block_number) { + if let Some(previous_bad_receipt) = bad_receipts_at.get(&receipt.consensus_block_hash) { + *receipt = previous_bad_receipt.clone(); + return Ok(()); + } + } + + let random_seed = Random::seed(); + let bad_receipt_type = match random_seed % 5 { + 0 => BadReceiptType::TotalRewards, + 1 => BadReceiptType::ExecutionTrace, + 2 => BadReceiptType::ExtrinsicsRoot, + 3 => BadReceiptType::DomainBlockHash, + 4 => BadReceiptType::ParentReceipt, + // TODO: enable once `https://github.com/subspace/subspace/issues/2287` is resolved + // 5 => BadReceiptType::InboxedBundle, + _ => return Ok(()), + }; + + tracing::info!( + ?bad_receipt_type, + "Generate bad ER of domain block {}#{}", + receipt.domain_block_number, + receipt.domain_block_hash, + ); + + match bad_receipt_type { + BadReceiptType::TotalRewards => { + receipt.total_rewards = random_seed.into(); + } + // TODO: modify the length of `execution_trace` once the honest operator can handle + BadReceiptType::ExecutionTrace => { + let mismatch_index = random_seed as usize % receipt.execution_trace.len(); + receipt.execution_trace[mismatch_index] = Default::default(); + receipt.execution_trace_root = { + let trace: Vec<_> = receipt + .execution_trace + .iter() + .map(|t| t.encode().try_into().unwrap()) + .collect(); + MerkleTree::from_leaves(trace.as_slice()) + .root() + .unwrap() + .into() + }; + } + BadReceiptType::ExtrinsicsRoot => { + receipt.domain_block_extrinsic_root = Default::default(); + } + BadReceiptType::DomainBlockHash => { + receipt.domain_block_hash = Default::default(); + } + BadReceiptType::ParentReceipt => { + let parent_domain_number = receipt.domain_block_number - One::one(); + let parent_block_consensus_hash: CBlock::Hash = { + let parent_domain_hash = *self + .domain_client + .header(receipt.domain_block_hash)? + .ok_or_else(|| { + sp_blockchain::Error::Backend(format!( + "Domain block header for #{:?} not found", + receipt.domain_block_hash + )) + })? + .parent_hash(); + let parent_domain_header = self + .domain_client + .header(parent_domain_hash)? + .ok_or_else(|| { + sp_blockchain::Error::Backend(format!( + "Domain block header for #{parent_domain_hash:?} not found", + )) + })?; + parent_domain_header + .digest() + .convert_first(DigestItem::as_consensus_block_info) + .expect("Domain block header must have the consensus block info digest") + }; + let maybe_parent_bad_receipt = self + .bad_receipts_cache + .get(&parent_domain_number) + .and_then(|bad_receipts_at| bad_receipts_at.get(&parent_block_consensus_hash)); + match maybe_parent_bad_receipt { + Some(parent_bad_receipt) => { + receipt.parent_domain_block_receipt_hash = + parent_bad_receipt.hash::>(); + } + // The parent receipt is not a bad receipt so even we modify this field to a random + // value, the receipt will be rejected by the consensus node directly thus just skip + None => return Ok(()), + } + } + // NOTE: Not need to modify the bundle `extrinsics_root` or the lenght of `inboxed_bundles` + // since the consensus runtime will perform the these checks and reject the bundle directly + BadReceiptType::InboxedBundle => { + let mismatch_index = random_seed as usize % receipt.inboxed_bundles.len(); + let reverse_type = random_seed % 2 == 0; + receipt.inboxed_bundles[mismatch_index].bundle = + match receipt.inboxed_bundles[mismatch_index].bundle { + BundleValidity::Valid(_) => { + if reverse_type { + BundleValidity::Invalid(InvalidBundleType::InherentExtrinsic( + mismatch_index as u32, + )) + } else { + BundleValidity::Valid(Default::default()) + } + } + BundleValidity::Invalid(_) => { + if reverse_type { + BundleValidity::Valid(Default::default()) + } else { + BundleValidity::Invalid(InvalidBundleType::InherentExtrinsic( + mismatch_index as u32, + )) + } + } + }; + } + } + + // Add the bad receipt to cache and remove the oldest receipt from cache + self.bad_receipts_cache + .entry(receipt.domain_block_number) + .or_default() + .insert(receipt.consensus_block_hash, receipt.clone()); + if self.bad_receipts_cache.len() as u32 > MAX_BAD_RECEIPT_CACHE { + self.bad_receipts_cache.pop_first(); + } + + Ok(()) + } + + #[allow(clippy::modulo_one)] + fn make_bundle_invalid( + &self, + opaque_bundle: &mut OpaqueBundleFor, + ) -> Result<(), Box> { + let random_seed = Random::seed(); + let invalid_bundle_type = match random_seed % 1 { + 0 => InvalidBundleType::InherentExtrinsic(0), + // TODO: enable `UndecodableTx` and `IllegalTx` once the fraud proof is implemented + // and support other invalid bundle types + // 1 => InvalidBundleType::UndecodableTx(0), + // 2 => InvalidBundleType::IllegalTx(0), + _ => return Ok(()), + }; + tracing::info!( + ?invalid_bundle_type, + "Generate invalid bundle, receipt domain block {}#{}", + opaque_bundle.receipt().domain_block_number, + opaque_bundle.receipt().domain_block_hash, + ); + + let invalid_tx = match invalid_bundle_type { + InvalidBundleType::UndecodableTx(_) => OpaqueExtrinsic::default(), + // The duplicated extrinsic will be illegal due to `Nonce` if it is a signed extrinsic + InvalidBundleType::IllegalTx(_) if !opaque_bundle.extrinsics.is_empty() => { + opaque_bundle.extrinsics[0].clone() + } + InvalidBundleType::InherentExtrinsic(_) => { + let inherent_tx = self + .domain_client + .runtime_api() + .construct_timestamp_extrinsic( + self.domain_client.info().best_hash, + Default::default(), + )?; + OpaqueExtrinsic::from_bytes(&inherent_tx.encode()) + .expect("We have just encoded a valid extrinsic; qed") + } + _ => return Ok(()), + }; + + opaque_bundle.sealed_header.header.bundle_size += invalid_tx.encoded_size() as u32; + opaque_bundle.extrinsics.push(invalid_tx); + opaque_bundle.sealed_header.header.bundle_extrinsics_root = + HeaderHashingFor::::ordered_trie_root( + opaque_bundle + .extrinsics + .iter() + .map(|xt| xt.encode()) + .collect(), + sp_core::storage::StateVersion::V1, + ); + Ok(()) + } + + fn reseal_bundle( + &self, + opaque_bundle: &mut OpaqueBundleFor, + operator_signing_key: &OperatorPublicKey, + ) -> Result<(), Box> { + let sealed_header = &mut opaque_bundle.sealed_header; + sealed_header.signature = { + let s = self + .keystore + .sr25519_sign( + OperatorPublicKey::ID, + operator_signing_key.as_ref(), + sealed_header.pre_hash().as_ref(), + )? + .expect("The malicious operator's key pair must exist"); + OperatorSignature::decode(&mut s.as_ref()) + .expect("Deconde as OperatorSignature must succeed") + }; + Ok(()) + } +} From 861cbe2ab93c2b95b210a213b7259811633def00 Mon Sep 17 00:00:00 2001 From: linning Date: Fri, 8 Dec 2023 22:11:59 +0800 Subject: [PATCH 6/9] Introduce and run MaliciousBundleProducer for the malicious operator The MaliciousBundleProducer act similar as the normal bundle producer except it may randomly temper the bundle into a invalid bundle and/or contains bad ER with probability before submitting it to the consensus chain Signed-off-by: linning --- crates/subspace-malicious-operator/src/lib.rs | 1 + .../src/malicious_bundle_producer.rs | 200 ++++++++++++++++++ .../src/malicious_domain_instance_starter.rs | 21 +- domains/service/src/domain.rs | 3 + 4 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 crates/subspace-malicious-operator/src/malicious_bundle_producer.rs diff --git a/crates/subspace-malicious-operator/src/lib.rs b/crates/subspace-malicious-operator/src/lib.rs index 3f37407f8e..64c32567c2 100644 --- a/crates/subspace-malicious-operator/src/lib.rs +++ b/crates/subspace-malicious-operator/src/lib.rs @@ -16,5 +16,6 @@ //! Subspace malicious operator library. +mod malicious_bundle_producer; mod malicious_bundle_tamper; pub mod malicious_domain_instance_starter; diff --git a/crates/subspace-malicious-operator/src/malicious_bundle_producer.rs b/crates/subspace-malicious-operator/src/malicious_bundle_producer.rs new file mode 100644 index 0000000000..66919a0a22 --- /dev/null +++ b/crates/subspace-malicious-operator/src/malicious_bundle_producer.rs @@ -0,0 +1,200 @@ +use crate::malicious_bundle_tamper::MaliciousBundleTamper; +use domain_client_operator::domain_bundle_producer::DomainBundleProducer; +use domain_client_operator::domain_bundle_proposer::DomainBundleProposer; +use domain_client_operator::{OpaqueBundleFor, OperatorSlotInfo}; +use domain_runtime_primitives::opaque::Block as DomainBlock; +use domain_runtime_primitives::DomainCoreApi; +use frame_system_rpc_runtime_api::AccountNonceApi; +use futures::{Stream, StreamExt, TryFutureExt}; +use pallet_domains::OperatorConfig; +use parity_scale_codec::Encode; +use sc_client_api::{AuxStore, BlockBackend, HeaderBackend}; +use sc_service::config::KeystoreConfig; +use sc_service::KeystoreContainer; +use sc_transaction_pool_api::OffchainTransactionPoolFactory; +use sc_utils::mpsc::tracing_unbounded; +use sp_api::ProvideRuntimeApi; +use sp_block_builder::BlockBuilder; +use sp_blockchain::Info; +use sp_consensus_slots::Slot; +use sp_consensus_subspace::FarmerPublicKey; +use sp_core::crypto::UncheckedFrom; +use sp_core::Get; +use sp_domains::{BundleProducerElectionApi, DomainId, DomainsApi, OperatorId, OperatorPublicKey}; +use sp_keyring::Sr25519Keyring; +use sp_keystore::KeystorePtr; +use sp_runtime::traits::Block as BlockT; +use sp_runtime::{generic, RuntimeAppPublic}; +use sp_transaction_pool::runtime_api::TaggedTransactionQueue; +use std::error::Error; +use std::sync::Arc; +use subspace_core_primitives::Randomness; +use subspace_runtime::{ + CheckStorageAccess, DisablePallets, Runtime, RuntimeCall, SignedExtra, UncheckedExtrinsic, +}; +use subspace_runtime_primitives::opaque::Block as CBlock; +use subspace_runtime_primitives::{AccountId, Balance, Nonce}; + +pub struct MaliciousBundleProducer { + domain_id: DomainId, + sudo_acccount: AccountId, + consensus_keystore: KeystorePtr, + operator_keystore: KeystorePtr, + consensus_client: Arc, + consensus_offchain_tx_pool_factory: OffchainTransactionPoolFactory, + bundle_producer: DomainBundleProducer, + malicious_bundle_makder: MaliciousBundleTamper, + malicious_operator_status: MaliciousOperatorStatus, +} + +impl MaliciousBundleProducer +where + Client: HeaderBackend + + BlockBackend + + AuxStore + + ProvideRuntimeApi + + 'static, + Client::Api: BlockBuilder + + DomainCoreApi + + TaggedTransactionQueue, + CClient: HeaderBackend + ProvideRuntimeApi + 'static, + CClient::Api: DomainsApi::Header> + + BundleProducerElectionApi + + AccountNonceApi, + TransactionPool: sc_transaction_pool_api::TransactionPool + 'static, +{ + pub fn new( + domain_id: DomainId, + domain_client: Arc, + consensus_client: Arc, + consensus_keystore: KeystorePtr, + consensus_offchain_tx_pool_factory: OffchainTransactionPoolFactory, + domain_transaction_pool: Arc, + ) -> Self { + let operator_keystore = KeystoreContainer::new(&KeystoreConfig::InMemory) + .expect("create in-memory keystore container must succeed") + .keystore(); + + let domain_bundle_proposer = DomainBundleProposer::new( + domain_id, + domain_client.clone(), + consensus_client.clone(), + domain_transaction_pool, + ); + + let (bundle_sender, _bundle_receiver) = tracing_unbounded("domain_bundle_stream", 100); + let bundle_producer = DomainBundleProducer::new( + domain_id, + consensus_client.clone(), + domain_client.clone(), + domain_bundle_proposer, + Arc::new(bundle_sender), + operator_keystore.clone(), + // The malicious operator doesn't skip empty bundle + false, + ); + + let malicious_bundle_makder = + MaliciousBundleTamper::new(domain_client, operator_keystore.clone()); + + let sudo_acccount = consensus_client + .runtime_api() + .sudo_account_id(consensus_client.info().best_hash) + .expect("Failed to get sudo account"); + + Self { + domain_id, + consensus_client, + consensus_keystore, + operator_keystore, + bundle_producer, + malicious_bundle_makder, + malicious_operator_status: MaliciousOperatorStatus::NoStatus, + sudo_acccount, + consensus_offchain_tx_pool_factory, + } + } + + async fn handle_new_slot( + &self, + operator_id: OperatorId, + new_slot_info: OperatorSlotInfo, + ) -> Option> { + let slot = new_slot_info.slot; + let consensus_block_info = { + let info = self.consensus_client.info(); + sp_blockchain::HashAndNumber { + number: info.best_number, + hash: info.best_hash, + } + }; + self.bundle_producer + .clone() + .produce_bundle(operator_id, consensus_block_info.clone(), new_slot_info) + .unwrap_or_else(move |error| { + tracing::error!( + ?consensus_block_info, + ?slot, + ?operator_id, + ?error, + "Error at malicious operator producing bundle" + ); + None + }) + .await + } + + pub async fn start + Send + 'static>( + mut self, + new_slot_notification_stream: NSNS, + ) { + let mut new_slot_notification_stream = Box::pin(new_slot_notification_stream); + while let Some((slot, global_randomness)) = new_slot_notification_stream.next().await { + if let Some((operator_id, signing_key)) = + self.malicious_operator_status.registered_operator() + { + let maybe_opaque_bundle = self + .handle_new_slot( + *operator_id, + OperatorSlotInfo { + slot, + global_randomness, + }, + ) + .await; + + if let Some(mut opaque_bundle) = maybe_opaque_bundle { + if let Err(err) = self + .malicious_bundle_makder + .maybe_tamper_bundle(&mut opaque_bundle, signing_key) + { + tracing::error!(?err, "Got error when try to tamper bundle"); + } + if let Err(err) = self.submit_bundle(opaque_bundle) { + tracing::info!(?err, "Malicious operator failed to submit bundle"); + } + } + } + } + } + + fn submit_bundle( + &self, + opaque_bundle: OpaqueBundleFor, + ) -> Result<(), Box> { + let call = UncheckedExtrinsic::new_unsigned( + pallet_domains::Call::submit_bundle { opaque_bundle }.into(), + ); + self.consensus_offchain_tx_pool_factory + .offchain_transaction_pool(self.consensus_client.info().best_hash) + .submit_transaction(etx.encode()) + .map_err(|err| { + sp_blockchain::Error::Application( + format!("Failed to submit consensus extrinsic, call {call:?}, err {err:?}") + .into(), + ) + })?; + + Ok(()) + } +} diff --git a/crates/subspace-malicious-operator/src/malicious_domain_instance_starter.rs b/crates/subspace-malicious-operator/src/malicious_domain_instance_starter.rs index c694747bfa..f5a363c936 100644 --- a/crates/subspace-malicious-operator/src/malicious_domain_instance_starter.rs +++ b/crates/subspace-malicious-operator/src/malicious_domain_instance_starter.rs @@ -151,7 +151,8 @@ where domain_message_receiver, provider: eth_provider, skip_empty_bundle_production: true, - maybe_operator_id: domain_cli.operator_id, + // Always set it to `None` to not running the normal bundle producer + maybe_operator_id: None, }; let mut domain_node = domain_service::new_full::< @@ -169,6 +170,24 @@ where >(domain_params) .await?; + let malicious_bundle_producer = MaliciousBundleProducer::new( + domain_id, + domain_node.client.clone(), + consensus_client, + consensus_keystore, + consensus_offchain_tx_pool_factory, + domain_node.transaction_pool.clone(), + ); + + domain_node + .task_manager + .spawn_essential_handle() + .spawn_essential_blocking( + "malicious-bundle-producer", + None, + Box::pin(malicious_bundle_producer.start(new_slot_notification_stream())), + ); + domain_node.network_starter.start_network(); domain_node.task_manager.future().await?; diff --git a/domains/service/src/domain.rs b/domains/service/src/domain.rs index 3632a62142..21e21fcebe 100644 --- a/domains/service/src/domain.rs +++ b/domains/service/src/domain.rs @@ -105,6 +105,8 @@ where pub network_starter: NetworkStarter, /// Operator. pub operator: DomainOperator, + /// Transaction pool + pub transaction_pool: Arc>, _phantom_data: PhantomData, } @@ -509,6 +511,7 @@ where rpc_handlers, network_starter, operator, + transaction_pool: params.transaction_pool, _phantom_data: Default::default(), }; From 97efc5dc370dec9096bf0b9942c4d6487911356f Mon Sep 17 00:00:00 2001 From: linning Date: Fri, 8 Dec 2023 22:16:24 +0800 Subject: [PATCH 7/9] Periodically check the malicious operator status and register a new operator when it is slashed This commit also enforce epoch transition immediately to accelerate the onboard of the new malicious operator, also it increase the endowed balance of the sudo account to ensure it always have enough balance to register operator Signed-off-by: linning --- Cargo.lock | 60 ++++ .../src/malicious_bundle_producer.rs | 272 +++++++++++++++++- crates/subspace-node/src/chain_spec.rs | 6 +- 3 files changed, 332 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4604afaa0e..33a452a6a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11755,6 +11755,66 @@ dependencies = [ "winapi", ] +[[package]] +name = "subspace-malicious-operator" +version = "0.1.0" +dependencies = [ + "cross-domain-message-gossip", + "domain-client-message-relayer", + "domain-client-operator", + "domain-eth-service", + "domain-runtime-primitives", + "domain-service", + "evm-domain-runtime", + "frame-system", + "frame-system-rpc-runtime-api", + "futures", + "log", + "mimalloc", + "pallet-domains", + "pallet-sudo", + "pallet-transaction-payment", + "parity-scale-codec", + "rand 0.8.5", + "sc-chain-spec", + "sc-cli", + "sc-client-api", + "sc-consensus-slots", + "sc-consensus-subspace", + "sc-network", + "sc-network-sync", + "sc-service", + "sc-storage-monitor", + "sc-tracing", + "sc-transaction-pool-api", + "sc-utils", + "serde_json", + "sp-api", + "sp-block-builder", + "sp-blockchain", + "sp-consensus-slots", + "sp-consensus-subspace", + "sp-core", + "sp-domain-digests", + "sp-domains", + "sp-keyring", + "sp-keystore", + "sp-messenger", + "sp-runtime", + "sp-transaction-pool", + "subspace-core-primitives", + "subspace-networking", + "subspace-node", + "subspace-proof-of-space", + "subspace-runtime", + "subspace-runtime-primitives", + "subspace-service", + "substrate-build-script-utils", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "subspace-metrics" version = "0.1.0" diff --git a/crates/subspace-malicious-operator/src/malicious_bundle_producer.rs b/crates/subspace-malicious-operator/src/malicious_bundle_producer.rs index 66919a0a22..a77a8b70e7 100644 --- a/crates/subspace-malicious-operator/src/malicious_bundle_producer.rs +++ b/crates/subspace-malicious-operator/src/malicious_bundle_producer.rs @@ -35,6 +35,51 @@ use subspace_runtime::{ use subspace_runtime_primitives::opaque::Block as CBlock; use subspace_runtime_primitives::{AccountId, Balance, Nonce}; +const MALICIOUS_OPR_STAKE_MULTIPLIER: Balance = 3; + +enum MaliciousOperatorStatus { + Registering(OperatorPublicKey), + Registered { + operator_id: OperatorId, + signing_key: OperatorPublicKey, + }, + NoStatus, +} + +impl MaliciousOperatorStatus { + fn registering(&mut self, signing_key: OperatorPublicKey) { + *self = MaliciousOperatorStatus::Registering(signing_key) + } + + fn registered(&mut self, operator_id: OperatorId, signing_key: OperatorPublicKey) { + *self = MaliciousOperatorStatus::Registered { + operator_id, + signing_key, + } + } + + fn no_status(&mut self) { + *self = MaliciousOperatorStatus::NoStatus + } + + fn registered_operator(&self) -> Option<(&OperatorId, &OperatorPublicKey)> { + match self { + MaliciousOperatorStatus::Registered { + operator_id, + signing_key, + } => Some((operator_id, signing_key)), + _ => None, + } + } + + fn registering_signing_key(&self) -> Option { + match self { + MaliciousOperatorStatus::Registering(key) => Some(key.clone()), + _ => None, + } + } +} + pub struct MaliciousBundleProducer { domain_id: DomainId, sudo_acccount: AccountId, @@ -175,16 +220,165 @@ where } } } + + // Periodically check the malicious operator status + if u64::from(slot) % 10 == 0 { + if let Err(err) = self.update_malicious_operator_status() { + tracing::error!(?err, "Failed to update malicious operator status"); + } + } + } + } + + fn update_malicious_operator_status(&mut self) -> Result<(), Box> { + let consensus_best_hash = self.consensus_client.info().best_hash; + let (mut current_operators, next_operators) = self + .consensus_client + .runtime_api() + .domain_operators(consensus_best_hash, self.domain_id)? + .ok_or_else(|| { + sp_blockchain::Error::Application( + format!("Operator set for domain {} not found", self.domain_id).into(), + ) + })?; + + if let Some((malicious_operator_id, _)) = + self.malicious_operator_status.registered_operator() + { + if next_operators.contains(malicious_operator_id) { + return Ok(()); + } else { + tracing::info!( + ?malicious_operator_id, + "Current malicious operator is missing from next operator set, probably got slashed" + ); + // Remove the current malicious operator to not account its stake toward + // `current_total_stake` otherwise the next malicious operator will stake + // more and more fund + current_operators.remove(malicious_operator_id); + self.malicious_operator_status.no_status(); + } + } + + let signing_key = match &self.malicious_operator_status.registering_signing_key() { + Some(k) => k.clone(), + None => { + let public_key: OperatorPublicKey = self + .operator_keystore + .sr25519_generate_new(OperatorPublicKey::ID, None)? + .into(); + + self.malicious_operator_status + .registering(public_key.clone()); + + tracing::info!(?public_key, "Start register new malicious operator"); + + public_key + } + }; + + let maybe_operator_id = self + .consensus_client + .runtime_api() + .operator_id_by_signing_key(consensus_best_hash, signing_key.clone())?; + + // The `signing_key` is linked to a operator means the previous registeration request is succeeded + // otherwise we need to retry + match maybe_operator_id { + None => { + let nonce = self.sudo_acccount_nonce()?; + let current_total_stake: Balance = current_operators.into_values().sum(); + self.submit_register_operator( + nonce, + signing_key, + // Ideally we should use the `next_total_stake` but it is tricky to get + MALICIOUS_OPR_STAKE_MULTIPLIER * current_total_stake, + )?; + self.submit_force_staking_epoch_transition(nonce + 1)?; + } + Some(operator_id) => { + if !next_operators.contains(&operator_id) { + // The operator id not present in `next_operators` means the operator is deregistered + // or slashed, which should not happen since we haven't use this operator to submit bad + // ER yet. But just set `malicious_operator_status` to `NoStatus` to register a new operator. + self.malicious_operator_status.no_status(); + } else if !current_operators.contains_key(&operator_id) { + self.submit_force_staking_epoch_transition(self.sudo_acccount_nonce()?)?; + } else { + tracing::info!( + ?operator_id, + ?signing_key, + "Registered a new malicious operator" + ); + self.malicious_operator_status + .registered(operator_id, signing_key); + } + } } + + Ok(()) + } + + fn sudo_acccount_nonce(&self) -> Result> { + Ok(self.consensus_client.runtime_api().account_nonce( + self.consensus_client.info().best_hash, + self.sudo_acccount.clone(), + )?) } fn submit_bundle( &self, opaque_bundle: OpaqueBundleFor, ) -> Result<(), Box> { - let call = UncheckedExtrinsic::new_unsigned( - pallet_domains::Call::submit_bundle { opaque_bundle }.into(), - ); + let call = pallet_domains::Call::submit_bundle { opaque_bundle }; + self.submit_consensus_extrinsic(None, call.into()) + } + + fn submit_register_operator( + &self, + nonce: Nonce, + signing_key: OperatorPublicKey, + staking_amount: Balance, + ) -> Result<(), Box> { + let call = pallet_domains::Call::register_operator { + domain_id: self.domain_id, + amount: staking_amount, + config: OperatorConfig { + signing_key, + minimum_nominator_stake: Balance::MAX, + nomination_tax: Default::default(), + }, + }; + self.submit_consensus_extrinsic(Some(nonce), call.into()) + } + + fn submit_force_staking_epoch_transition(&self, nonce: Nonce) -> Result<(), Box> { + let call = pallet_sudo::Call::sudo { + call: Box::new(RuntimeCall::Domains( + pallet_domains::Call::force_staking_epoch_transition { + domain_id: self.domain_id, + }, + )), + }; + self.submit_consensus_extrinsic(Some(nonce), call.into()) + } + + fn submit_consensus_extrinsic( + &self, + maybe_nonce: Option, + call: RuntimeCall, + ) -> Result<(), Box> { + let etx = match maybe_nonce { + Some(nonce) => construct_signed_extrinsic( + &self.consensus_keystore, + self.consensus_client.info(), + call.clone(), + self.sudo_acccount.clone(), + nonce, + )?, + None => UncheckedExtrinsic::new_unsigned(call.clone()), + }; + self.consensus_offchain_tx_pool_factory .offchain_transaction_pool(self.consensus_client.info().best_hash) .submit_transaction(etx.encode()) @@ -198,3 +392,75 @@ where Ok(()) } } + +pub fn construct_signed_extrinsic( + consensus_keystore: &KeystorePtr, + consensus_chain_info: Info, + call: RuntimeCall, + caller: AccountId, + nonce: Nonce, +) -> Result> { + let period = u64::from(<::BlockHashCount as Get< + u32, + >>::get()) + .checked_next_power_of_two() + .map(|c| c / 2) + .unwrap_or(2); + let extra: SignedExtra = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckMortality::::from(generic::Era::mortal( + period, + consensus_chain_info.best_number.into(), + )), + frame_system::CheckNonce::::from(nonce), + frame_system::CheckWeight::::new(), + pallet_transaction_payment::ChargeTransactionPayment::::from(0u128), + CheckStorageAccess, + DisablePallets, + ); + let raw_payload = generic::SignedPayload::::from_raw( + call.clone(), + extra.clone(), + ( + (), + subspace_runtime::VERSION.spec_version, + subspace_runtime::VERSION.transaction_version, + consensus_chain_info.genesis_hash, + consensus_chain_info.best_hash, + (), + (), + (), + (), + (), + ), + ); + + let signature = match Sr25519Keyring::from_account_id(&caller) { + Some(keyring) => raw_payload.using_encoded(|e| keyring.sign(e)), + None => { + let public_key = + sp_core::sr25519::Public::unchecked_from(>::into( + caller.clone(), + )); + raw_payload + .using_encoded(|e| { + consensus_keystore + .sr25519_sign(FarmerPublicKey::ID, &public_key, e) + })? + .ok_or(format!( + "Failed to sign extrinsic, sudo key pair missing from keystore?, public_key {:?}", + public_key + ))? + } + }; + + Ok(UncheckedExtrinsic::new_signed( + call, + sp_runtime::MultiAddress::Id(caller), + signature.into(), + extra, + )) +} diff --git a/crates/subspace-node/src/chain_spec.rs b/crates/subspace-node/src/chain_spec.rs index b23a790911..dd4a6bb0b0 100644 --- a/crates/subspace-node/src/chain_spec.rs +++ b/crates/subspace-node/src/chain_spec.rs @@ -225,7 +225,7 @@ pub fn devnet_config_compiled() -> Result Result, String> get_account_id_from_seed("Alice"), // Pre-funded accounts vec![ - (get_account_id_from_seed("Alice"), 1_000 * SSC), + (get_account_id_from_seed("Alice"), Balance::MAX / 2), (get_account_id_from_seed("Bob"), 1_000 * SSC), (get_account_id_from_seed("Alice//stash"), 1_000 * SSC), (get_account_id_from_seed("Bob//stash"), 1_000 * SSC), @@ -387,7 +387,7 @@ pub fn local_config() -> Result, String get_account_id_from_seed("Alice"), // Pre-funded accounts vec![ - (get_account_id_from_seed("Alice"), 1_000 * SSC), + (get_account_id_from_seed("Alice"), Balance::MAX / 2), (get_account_id_from_seed("Bob"), 1_000 * SSC), (get_account_id_from_seed("Charlie"), 1_000 * SSC), (get_account_id_from_seed("Dave"), 1_000 * SSC), From cfb246823b6b3d33f311ca5b9f96c98fcd63bb18 Mon Sep 17 00:00:00 2001 From: linning Date: Fri, 8 Dec 2023 22:17:18 +0800 Subject: [PATCH 8/9] Add the malicious operator README Signed-off-by: linning --- crates/subspace-malicious-operator/README.md | 33 ++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 crates/subspace-malicious-operator/README.md diff --git a/crates/subspace-malicious-operator/README.md b/crates/subspace-malicious-operator/README.md new file mode 100644 index 0000000000..cbef55c655 --- /dev/null +++ b/crates/subspace-malicious-operator/README.md @@ -0,0 +1,33 @@ +# The intentional malicious operator node + +NOTE: ****this is only use for testing purpose**** + +The malicious operator node act as a regular [domain operator](../../domains/README.md) but it will intentionally and continuously produce malicious content to test if the network can handle it properly. + +### How it works + +Most parts of the malicious operator act exactly the same as the regular domain operator except its bundle producer. When it produce a bundle, the bundle will be tampered with malicious content with probability before submitting to the consensus chain. + +Currently, it supports produce: +- Invalid bundle +- Fraudulent ER + +When the operator submit malicious content to the consensus chain, the honest operator in the network will detect and submit fraud proof that target these content, and cause the malicious operator being slashed and baned from submitting bundle. + +The malicious operator node will detect the slashing and register a new operator as the malicious operator, moreover, it will enforce the epoch transition to accelerate the onboard of the new malicious operator, and contiune producing malicious content. + +### Build from source + +```bash +cargo build -r subspace-malicious-operator +``` + +### Run + +The malicious operator node take the same args as the regular domain operator, please refer to [Domain operator](../../domains/README.md). + +A few notable differences: +- The malicious operator node will ignore the `--operator-id` arg if specified, instead it will register new operator internally and automatically and using their id to produce malicious content. +- The malicious operator node requires the consensus chain sudo key pair to run in the network. + - With `--chains local/dev`, Alice is the sudo account and its key pair is already exist in the node. + - With `--chain devnet`, the sudo key pair need to insert into the keystore with `subspace-node key insert --suri "" --key-type sub_ --scheme sr25519 --keystore-path `. From 44ee11f810a7022cf20dffa6aed48a5207e8d305 Mon Sep 17 00:00:00 2001 From: linning Date: Wed, 13 Dec 2023 00:02:53 +0800 Subject: [PATCH 9/9] Make domain args mandatory for the malicious operator and cleanup typo Signed-off-by: linning --- .../src/bin/subspace-malicious-operator.rs | 6 +++++- .../src/malicious_bundle_producer.rs | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/subspace-malicious-operator/src/bin/subspace-malicious-operator.rs b/crates/subspace-malicious-operator/src/bin/subspace-malicious-operator.rs index fadadf5e32..090dc56492 100644 --- a/crates/subspace-malicious-operator/src/bin/subspace-malicious-operator.rs +++ b/crates/subspace-malicious-operator/src/bin/subspace-malicious-operator.rs @@ -263,7 +263,11 @@ fn main() -> Result<(), Error> { })?; // Run a domain node. - if !cli.domain_args.is_empty() { + if cli.domain_args.is_empty() { + return Err(Error::Other( + "The domain args must be specified for the malicious operator".to_string(), + )); + } else { let span = sc_tracing::tracing::info_span!( sc_tracing::logging::PREFIX_LOG_SPAN, name = "Domain" diff --git a/crates/subspace-malicious-operator/src/malicious_bundle_producer.rs b/crates/subspace-malicious-operator/src/malicious_bundle_producer.rs index a77a8b70e7..177021ddd8 100644 --- a/crates/subspace-malicious-operator/src/malicious_bundle_producer.rs +++ b/crates/subspace-malicious-operator/src/malicious_bundle_producer.rs @@ -88,7 +88,7 @@ pub struct MaliciousBundleProducer { consensus_client: Arc, consensus_offchain_tx_pool_factory: OffchainTransactionPoolFactory, bundle_producer: DomainBundleProducer, - malicious_bundle_makder: MaliciousBundleTamper, + malicious_bundle_tamper: MaliciousBundleTamper, malicious_operator_status: MaliciousOperatorStatus, } @@ -139,7 +139,7 @@ where false, ); - let malicious_bundle_makder = + let malicious_bundle_tamper = MaliciousBundleTamper::new(domain_client, operator_keystore.clone()); let sudo_acccount = consensus_client @@ -153,7 +153,7 @@ where consensus_keystore, operator_keystore, bundle_producer, - malicious_bundle_makder, + malicious_bundle_tamper, malicious_operator_status: MaliciousOperatorStatus::NoStatus, sudo_acccount, consensus_offchain_tx_pool_factory, @@ -210,7 +210,7 @@ where if let Some(mut opaque_bundle) = maybe_opaque_bundle { if let Err(err) = self - .malicious_bundle_makder + .malicious_bundle_tamper .maybe_tamper_bundle(&mut opaque_bundle, signing_key) { tracing::error!(?err, "Got error when try to tamper bundle");