diff --git a/Cargo.lock b/Cargo.lock index 5d7c81d82d..2e0a72be32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11756,6 +11756,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/pallet-domains/src/lib.rs b/crates/pallet-domains/src/lib.rs index 6427baaeea..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; @@ -358,10 +359,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>; @@ -1388,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/sp-domains/src/lib.rs b/crates/sp-domains/src/lib.rs index 340bb1ac90..819f12ffe9 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-malicious-operator/Cargo.toml b/crates/subspace-malicious-operator/Cargo.toml new file mode 100644 index 0000000000..eca0edeb8e --- /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 = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +frame-system-rpc-runtime-api = { version = "4.0.0-dev", default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +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 = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +parity-scale-codec = "3.6.5" +pallet-sudo = { version = "4.0.0-dev", default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +sc-chain-spec = { version = "4.0.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +sc-cli = { version = "0.10.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5", default-features = false } +sc-client-api = { version = "4.0.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +sc-consensus-slots = { version = "0.10.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +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 = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +sc-service = { version = "0.10.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5", default-features = false } +sc-storage-monitor = { version = "0.1.0", git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5", default-features = false } +sc-tracing = { version = "4.0.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +sc-transaction-pool-api = { version = "4.0.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +sc-network-sync = { version = "0.10.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +sc-utils = { version = "4.0.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +serde_json = "1.0.106" +sp-api = { version = "4.0.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +sp-blockchain = { version = "4.0.0-dev", git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +sp-block-builder = { git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5", 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 = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +sp-core = { version = "21.0.0", git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +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 = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +sp-messenger = { version = "0.1.0", path = "../../domains/primitives/messenger" } +sp-runtime = { version = "24.0.0", git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +sp-keystore = { version = "0.27.0", git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +sp-keyring = { git = "https://github.com/subspace/polkadot-sdk", rev = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } +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 = "0831dfc3c54b10ab46e82acf98603b4af1a47bd5" } + +[features] +default = [] 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 `. 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..090dc56492 --- /dev/null +++ b/crates/subspace-malicious-operator/src/bin/subspace-malicious-operator.rs @@ -0,0 +1,436 @@ +// 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() { + 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" + ); + 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..64c32567c2 --- /dev/null +++ b/crates/subspace-malicious-operator/src/lib.rs @@ -0,0 +1,21 @@ +// 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. + +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..177021ddd8 --- /dev/null +++ b/crates/subspace-malicious-operator/src/malicious_bundle_producer.rs @@ -0,0 +1,466 @@ +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}; + +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, + consensus_keystore: KeystorePtr, + operator_keystore: KeystorePtr, + consensus_client: Arc, + consensus_offchain_tx_pool_factory: OffchainTransactionPoolFactory, + bundle_producer: DomainBundleProducer, + malicious_bundle_tamper: 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_tamper = + 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_tamper, + 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_tamper + .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"); + } + } + } + + // 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 = 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()) + .map_err(|err| { + sp_blockchain::Error::Application( + format!("Failed to submit consensus extrinsic, call {call:?}, err {err:?}") + .into(), + ) + })?; + + 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-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(()) + } +} 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..f5a363c936 --- /dev/null +++ b/crates/subspace-malicious-operator/src/malicious_domain_instance_starter.rs @@ -0,0 +1,199 @@ +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, + // Always set it to `None` to not running the normal bundle producer + maybe_operator_id: None, + }; + + let mut domain_node = domain_service::new_full::< + _, + _, + _, + _, + _, + _, + evm_domain_runtime::RuntimeApi, + EVMDomainExecutorDispatch, + AccountId20, + _, + _, + >(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?; + + Ok(()) + } + } + } +} diff --git a/crates/subspace-node/src/chain_spec.rs b/crates/subspace-node/src/chain_spec.rs index c3c28893ca..b466b9925f 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), 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 db506cc7b6..f851bd7dda 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; @@ -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")] @@ -1057,6 +1058,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/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_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 ff6b0ee3a5..6e19101d0e 100644 --- a/domains/client/domain-operator/src/domain_bundle_producer.rs +++ b/domains/client/domain-operator/src/domain_bundle_producer.rs @@ -28,13 +28,12 @@ type OpaqueBundle = sp_domains::OpaqueBundle< Balance, >; -pub(super) struct DomainBundleProducer +pub struct DomainBundleProducer where Block: BlockT, 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(), @@ -79,9 +77,8 @@ where TransactionPool: sc_transaction_pool_api::TransactionPool, { #[allow(clippy::too_many_arguments)] - pub(super) fn new( + pub 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, @@ -112,8 +108,9 @@ where } } - pub(super) async fn produce_bundle( + pub 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_bundle_proposer.rs b/domains/client/domain-operator/src/domain_bundle_proposer.rs index 2de5f4f094..ff78f4c467 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/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/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/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, 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)] 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(), }; diff --git a/test/subspace-test-runtime/src/lib.rs b/test/subspace-test-runtime/src/lib.rs index 3e3c0e19fb..bdfc5ddb77 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::*; @@ -1250,6 +1251,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 {