diff --git a/Cargo.lock b/Cargo.lock index 606b16057677f..c69776b788946 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8776,6 +8776,102 @@ dependencies = [ "thiserror", ] +[[package]] +name = "subspace-test-client" +version = "0.1.0" +dependencies = [ + "futures 0.3.19", + "rand 0.8.4", + "sc-chain-spec", + "sc-client-api", + "sc-consensus-subspace", + "sc-executor", + "sc-service", + "schnorrkel", + "sp-api", + "sp-consensus-subspace", + "sp-core", + "sp-runtime", + "subspace-archiving", + "subspace-core-primitives", + "subspace-runtime-primitives", + "subspace-service", + "subspace-solving", + "subspace-test-runtime", + "zeroize", +] + +[[package]] +name = "subspace-test-runtime" +version = "0.1.0" +dependencies = [ + "frame-executive", + "frame-support", + "frame-system", + "frame-system-rpc-runtime-api", + "hex-literal", + "orml-vesting", + "pallet-balances", + "pallet-executor", + "pallet-feeds", + "pallet-object-store", + "pallet-offences-subspace", + "pallet-rewards", + "pallet-subspace", + "pallet-sudo", + "pallet-timestamp", + "pallet-transaction-fees", + "pallet-transaction-payment", + "pallet-transaction-payment-rpc-runtime-api", + "pallet-utility", + "parity-scale-codec", + "scale-info", + "sp-api", + "sp-block-builder", + "sp-consensus-slots", + "sp-consensus-subspace", + "sp-core", + "sp-executor", + "sp-inherents", + "sp-offchain", + "sp-runtime", + "sp-session", + "sp-std", + "sp-transaction-pool", + "sp-version", + "subspace-core-primitives", + "subspace-runtime-primitives", + "substrate-wasm-builder", +] + +[[package]] +name = "subspace-test-service" +version = "0.1.0" +dependencies = [ + "frame-system", + "futures 0.3.19", + "pallet-balances", + "pallet-transaction-payment", + "polkadot-overseer", + "rand 0.8.4", + "sc-cli", + "sc-client-api", + "sc-network", + "sc-service", + "sc-tracing", + "sp-arithmetic", + "sp-blockchain", + "sp-keyring", + "sp-runtime", + "subspace-runtime-primitives", + "subspace-service", + "subspace-test-client", + "subspace-test-runtime", + "substrate-test-client", + "substrate-test-utils", + "tokio", +] + [[package]] name = "substrate-bip39" version = "0.4.4" @@ -8941,6 +9037,27 @@ dependencies = [ "thiserror", ] +[[package]] +name = "substrate-test-utils" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?rev=e6def65920d30029e42d498cb07cec5dd433b927#e6def65920d30029e42d498cb07cec5dd433b927" +dependencies = [ + "futures 0.3.19", + "substrate-test-utils-derive", + "tokio", +] + +[[package]] +name = "substrate-test-utils-derive" +version = "0.10.0-dev" +source = "git+https://github.com/paritytech/substrate?rev=e6def65920d30029e42d498cb07cec5dd433b927#e6def65920d30029e42d498cb07cec5dd433b927" +dependencies = [ + "proc-macro-crate 1.1.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "substrate-wasm-builder" version = "5.0.0-dev" @@ -9433,7 +9550,7 @@ checksum = "4ee73e6e4924fe940354b8d4d98cad5231175d615cd855b758adc658c0aac6a0" dependencies = [ "cfg-if 1.0.0", "digest 0.10.3", - "rand 0.4.6", + "rand 0.8.4", "static_assertions", ] diff --git a/Cargo.toml b/Cargo.toml index 66d50071cf224..2da3bbcaea1ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,9 @@ members = [ "polkadot/node/subsystem-types", "polkadot/node/subsystem-util", "substrate/*", + "test/subspace-test-client", + "test/subspace-test-runtime", + "test/subspace-test-service", ] # The list of dependencies below (which can be both direct and indirect dependencies) are crates diff --git a/test/subspace-test-client/Cargo.toml b/test/subspace-test-client/Cargo.toml new file mode 100644 index 0000000000000..ef5bf62c81611 --- /dev/null +++ b/test/subspace-test-client/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "subspace-test-client" +version = "0.1.0" +authors = ["Subspace Labs "] +edition = "2021" +license = "GPL-3.0-or-later" +homepage = "https://subspace.network" +repository = "https://github.com/subspace/subspace" +include = [ + "/src", + "/Cargo.toml", +] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +futures = "0.3.19" +rand = "0.8.3" +schnorrkel = "0.9.1" +sc-chain-spec = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sc-client-api = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sc-consensus-subspace = { version = "0.1.0", path = "../../crates/sc-consensus-subspace" } +sc-executor = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927", features = ["wasmtime"] } +sc-service = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927", features = ["wasmtime"] } +sp-api = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sp-consensus-subspace = { version = "0.1.0", path = "../../crates/sp-consensus-subspace" } +sp-core = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sp-runtime = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +subspace-archiving = { path = "../../crates/subspace-archiving" } +subspace-core-primitives = { path = "../../crates/subspace-core-primitives" } +subspace-runtime-primitives = { path = "../../crates/subspace-runtime-primitives" } +subspace-service = { path = "../../crates/subspace-service" } +subspace-solving = { path = "../../crates/subspace-solving" } +subspace-test-runtime = { version = "0.1.0", features = ["do-not-enforce-cost-of-storage"], path = "../subspace-test-runtime" } +zeroize = "1.4.3" diff --git a/test/subspace-test-client/src/chain_spec.rs b/test/subspace-test-client/src/chain_spec.rs new file mode 100644 index 0000000000000..e394d610943fb --- /dev/null +++ b/test/subspace-test-client/src/chain_spec.rs @@ -0,0 +1,88 @@ +//! Chain specification for the test runtime. + +use sc_chain_spec::ChainType; +use sp_core::{sr25519, Pair, Public}; +use sp_runtime::traits::{IdentifyAccount, Verify}; +use subspace_runtime_primitives::{AccountId, Balance, BlockNumber, Signature}; +use subspace_test_runtime::{ + BalancesConfig, GenesisConfig, SudoConfig, SystemConfig, VestingConfig, SSC, WASM_BINARY, +}; + +/// The `ChainSpec` parameterized for subspace test runtime. +pub type TestChainSpec = sc_service::GenericChainSpec; + +/// Generate a crypto pair from seed. +pub fn get_from_seed(seed: &str) -> ::Public { + TPublic::Pair::from_string(&format!("//{}", seed), None) + .expect("static values are valid; qed") + .public() +} + +type AccountPublic = ::Signer; + +/// Generate an account ID from seed. +pub fn get_account_id_from_seed(seed: &str) -> AccountId { + AccountPublic::from(get_from_seed::(seed)).into_account() +} + +/// Local testnet config (multivalidator Alice + Bob). +pub fn subspace_local_testnet_config() -> TestChainSpec { + let wasm_binary = WASM_BINARY.expect("Development wasm not available"); + TestChainSpec::from_genesis( + "Local Testnet", + "local_testnet", + ChainType::Local, + || { + create_genesis_config( + wasm_binary, + // Sudo account + get_account_id_from_seed("Alice"), + // Pre-funded accounts + vec![ + (get_account_id_from_seed("Alice"), 1_000 * SSC), + (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), + (get_account_id_from_seed("Eve"), 1_000 * SSC), + (get_account_id_from_seed("Ferdie"), 1_000 * SSC), + (get_account_id_from_seed("Alice//stash"), 1_000 * SSC), + (get_account_id_from_seed("Bob//stash"), 1_000 * SSC), + (get_account_id_from_seed("Charlie//stash"), 1_000 * SSC), + (get_account_id_from_seed("Dave//stash"), 1_000 * SSC), + (get_account_id_from_seed("Eve//stash"), 1_000 * SSC), + (get_account_id_from_seed("Ferdie//stash"), 1_000 * SSC), + ], + vec![], + ) + }, + vec![], + None, + Some("subspace-test"), + None, + None, + Default::default(), + ) +} + +/// Configure initial storage state for FRAME modules. +fn create_genesis_config( + wasm_binary: &[u8], + sudo_account: AccountId, + balances: Vec<(AccountId, Balance)>, + // who, start, period, period_count, per_period + vesting: Vec<(AccountId, BlockNumber, BlockNumber, u32, Balance)>, +) -> GenesisConfig { + GenesisConfig { + system: SystemConfig { + // Add Wasm runtime to storage. + code: wasm_binary.to_vec(), + }, + balances: BalancesConfig { balances }, + transaction_payment: Default::default(), + sudo: SudoConfig { + // Assign network admin rights. + key: Some(sudo_account), + }, + vesting: VestingConfig { vesting }, + } +} diff --git a/test/subspace-test-client/src/lib.rs b/test/subspace-test-client/src/lib.rs new file mode 100644 index 0000000000000..0f26e579bb80a --- /dev/null +++ b/test/subspace-test-client/src/lib.rs @@ -0,0 +1,188 @@ +// Copyright (C) 2021 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 test client only. + +#![warn(missing_docs, unused_crate_dependencies)] + +pub mod chain_spec; + +use futures::{SinkExt, StreamExt}; +use rand::prelude::*; +use sc_client_api::BlockBackend; +use sc_consensus_subspace::{ + notification::SubspaceNotificationStream, BlockSigningNotification, NewSlotNotification, +}; +use sp_api::ProvideRuntimeApi; +use sp_consensus_subspace::{FarmerPublicKey, FarmerSignature, SubspaceApi}; +use sp_core::crypto::UncheckedFrom; +use sp_core::{Decode, Encode}; +use std::sync::Arc; +use subspace_core_primitives::objects::BlockObjectMapping; +use subspace_core_primitives::{FlatPieces, Piece, Solution, Tag}; +use subspace_runtime_primitives::opaque::{Block, BlockId}; +use subspace_service::{FullClient, NewFull}; +use subspace_solving::{SubspaceCodec, SOLUTION_SIGNING_CONTEXT}; +use zeroize::Zeroizing; + +/// Subspace native executor instance. +pub struct TestExecutorDispatch; + +impl sc_executor::NativeExecutionDispatch for TestExecutorDispatch { + /// Otherwise we only use the default Substrate host functions. + type ExtendHostFunctions = (); + + fn dispatch(method: &str, data: &[u8]) -> Option> { + subspace_test_runtime::api::dispatch(method, data) + } + + fn native_version() -> sc_executor::NativeVersion { + subspace_test_runtime::native_version() + } +} + +/// The client type being used by the test service. +pub type Client = FullClient; + +/// Run a farmer. +pub fn start_farmer(new_full: &NewFull>) { + let client = new_full.client.clone(); + let new_slot_notification_stream = new_full.new_slot_notification_stream.clone(); + let block_signing_notification_stream = new_full.block_signing_notification_stream.clone(); + + let keypair = schnorrkel::Keypair::generate(); + let subspace_farming = start_farming(keypair.clone(), client, new_slot_notification_stream); + new_full + .task_manager + .spawn_essential_handle() + .spawn_blocking("subspace-farmer", Some("farming"), subspace_farming); + + new_full + .task_manager + .spawn_essential_handle() + .spawn_blocking("subspace-farmer", Some("block-signing"), async move { + const SUBSTRATE_SIGNING_CONTEXT: &[u8] = b"substrate"; + + let substrate_ctx = schnorrkel::context::signing_context(SUBSTRATE_SIGNING_CONTEXT); + let signing_pair: Zeroizing = Zeroizing::new(keypair); + + let mut block_signing_notification_stream = + block_signing_notification_stream.subscribe(); + + while let Some(BlockSigningNotification { + header_hash, + mut signature_sender, + }) = block_signing_notification_stream.next().await + { + let header_hash: [u8; 32] = header_hash.into(); + let block_signature: schnorrkel::Signature = + signing_pair.sign(substrate_ctx.bytes(&header_hash)); + let signature: subspace_core_primitives::Signature = + block_signature.to_bytes().into(); + signature_sender + .send( + FarmerSignature::decode(&mut signature.encode().as_ref()) + .expect("Failed to decode schnorrkel block signature"), + ) + .await + .unwrap(); + } + }); +} + +async fn start_farming( + keypair: schnorrkel::Keypair, + client: Arc, + new_slot_notification_stream: SubspaceNotificationStream, +) where + Client: ProvideRuntimeApi + BlockBackend + Send + Sync + 'static, + Client::Api: SubspaceApi, +{ + let (archived_pieces_sender, archived_pieces_receiver) = futures::channel::oneshot::channel(); + + std::thread::spawn({ + move || { + let archived_pieces = get_archived_pieces(&client); + archived_pieces_sender.send(archived_pieces).unwrap(); + } + }); + + let subspace_solving = SubspaceCodec::new(&keypair.public); + let ctx = schnorrkel::context::signing_context(SOLUTION_SIGNING_CONTEXT); + let (piece_index, mut encoding) = archived_pieces_receiver + .await + .unwrap() + .iter() + .flat_map(|flat_pieces| flat_pieces.as_pieces()) + .enumerate() + .choose(&mut rand::thread_rng()) + .map(|(piece_index, piece)| (piece_index as u64, Piece::try_from(piece).unwrap())) + .unwrap(); + subspace_solving.encode(&mut encoding, piece_index).unwrap(); + + let mut new_slot_notification_stream = new_slot_notification_stream.subscribe(); + + while let Some(NewSlotNotification { + new_slot_info, + mut solution_sender, + }) = new_slot_notification_stream.next().await + { + if Into::::into(new_slot_info.slot) % 2 == 0 { + let tag: Tag = subspace_solving::create_tag(&encoding, new_slot_info.salt); + + let _ = solution_sender + .send(Solution { + public_key: FarmerPublicKey::unchecked_from(keypair.public.to_bytes()), + piece_index, + encoding, + signature: keypair.sign(ctx.bytes(&tag)).to_bytes().into(), + local_challenge: keypair + .sign(ctx.bytes(&new_slot_info.global_challenge)) + .to_bytes() + .into(), + tag, + }) + .await; + } + } +} + +fn get_archived_pieces(client: &Arc) -> Vec +where + Client: ProvideRuntimeApi + BlockBackend, + Client::Api: SubspaceApi, +{ + let genesis_block_id = BlockId::Number(sp_runtime::traits::Zero::zero()); + let runtime_api = client.runtime_api(); + + let record_size = runtime_api.record_size(&genesis_block_id).unwrap(); + let recorded_history_segment_size = runtime_api + .recorded_history_segment_size(&genesis_block_id) + .unwrap(); + + let mut archiver = subspace_archiving::archiver::Archiver::new( + record_size as usize, + recorded_history_segment_size as usize, + ) + .expect("Incorrect parameters for archiver"); + + let genesis_block = client.block(&genesis_block_id).unwrap().unwrap(); + archiver + .add_block(genesis_block.encode(), BlockObjectMapping::default()) + .into_iter() + .map(|archived_segment| archived_segment.pieces) + .collect() +} diff --git a/test/subspace-test-runtime/Cargo.toml b/test/subspace-test-runtime/Cargo.toml new file mode 100644 index 0000000000000..b98a87df5b799 --- /dev/null +++ b/test/subspace-test-runtime/Cargo.toml @@ -0,0 +1,100 @@ +[package] +name = "subspace-test-runtime" +version = "0.1.0" +authors = ["Subspace Labs "] +edition = "2021" +license = "GPL-3.0-or-later" +homepage = "https://subspace.network" +repository = "https://github.com/subspace/subspace" +include = [ + "/src", + "/build.rs", + "/Cargo.toml", +] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.3.0", default-features = false, features = ["derive"] } +frame-executive = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +frame-support = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +frame-system = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +hex-literal = { version = "0.3.3", optional = true } +orml-vesting = { version = "0.4.1-dev", default-features = false, git = "https://github.com/subspace/open-runtime-module-library", rev = "dc76f70657d17f7a7c3a6124772824bf6ddc8ade" } +pallet-balances = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +pallet-executor = { version = "0.1.0", default-features = false, path = "../../crates/pallet-executor" } +pallet-feeds = { version = "0.1.0", default-features = false, path = "../../crates/pallet-feeds" } +pallet-object-store = { version = "0.1.0", default-features = false, path = "../../crates/pallet-object-store" } +pallet-offences-subspace = { version = "0.1.0", default-features = false, path = "../../crates/pallet-offences-subspace" } +pallet-rewards = { version = "0.1.0", default-features = false, path = "../../crates/pallet-rewards" } +pallet-subspace = { version = "0.1.0", default-features = false, features = ["no-early-solution-range-updates"], path = "../../crates/pallet-subspace" } +pallet-sudo = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +pallet-timestamp = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +pallet-transaction-fees = { version = "0.1.0", default-features = false, path = "../../crates/pallet-transaction-fees" } +pallet-transaction-payment = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +pallet-utility = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } +sp-api = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sp-block-builder = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927", default-features = false, version = "4.0.0-dev"} +sp-consensus-subspace = { version = "0.1.0", default-features = false, path = "../../crates/sp-consensus-subspace" } +sp-consensus-slots = { version = "0.10.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sp-core = { version = "5.0.0", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sp-executor = { version = "0.1.0", default-features = false, path = "../../crates/sp-executor" } +sp-inherents = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927", default-features = false, version = "4.0.0-dev"} +sp-offchain = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sp-runtime = { version = "5.0.0", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sp-session = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sp-std = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sp-transaction-pool = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sp-version = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +subspace-core-primitives = { version = "0.1.0", default-features = false, path = "../../crates/subspace-core-primitives" } +subspace-runtime-primitives = { version = "0.1.0", default-features = false, path = "../../crates/subspace-runtime-primitives" } + +# Used for the node template's RPCs +frame-system-rpc-runtime-api = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +pallet-transaction-payment-rpc-runtime-api = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } + +[build-dependencies] +substrate-wasm-builder = { version = "5.0.0-dev", git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-executive/std", + "frame-support/std", + "frame-system/std", + "frame-system-rpc-runtime-api/std", + "orml-vesting/std", + "pallet-balances/std", + "pallet-executor/std", + "pallet-feeds/std", + "pallet-object-store/std", + "pallet-offences-subspace/std", + "pallet-rewards/std", + "pallet-subspace/std", + "pallet-sudo/std", + "pallet-timestamp/std", + "pallet-transaction-fees/std", + "pallet-transaction-payment-rpc-runtime-api/std", + "pallet-transaction-payment/std", + "pallet-utility/std", + "scale-info/std", + "sp-api/std", + "sp-block-builder/std", + "sp-consensus-subspace/std", + "sp-consensus-slots/std", + "sp-core/std", + "sp-executor/std", + "sp-inherents/std", + "sp-offchain/std", + "sp-runtime/std", + "sp-session/std", + "sp-std/std", + "sp-transaction-pool/std", + "sp-version/std", + "subspace-core-primitives/std", + "subspace-runtime-primitives/std", +] +do-not-enforce-cost-of-storage = [] diff --git a/test/subspace-test-runtime/build.rs b/test/subspace-test-runtime/build.rs new file mode 100644 index 0000000000000..3225bf48085ab --- /dev/null +++ b/test/subspace-test-runtime/build.rs @@ -0,0 +1,25 @@ +// Copyright (C) 2021 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_wasm_builder::WasmBuilder; + +fn main() { + WasmBuilder::new() + .with_current_project() + .export_heap_base() + .import_memory() + .build() +} diff --git a/test/subspace-test-runtime/src/lib.rs b/test/subspace-test-runtime/src/lib.rs new file mode 100644 index 0000000000000..303cd5518da72 --- /dev/null +++ b/test/subspace-test-runtime/src/lib.rs @@ -0,0 +1,924 @@ +// Copyright (C) 2021 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 . + +#![cfg_attr(not(feature = "std"), no_std)] +#![feature(const_option)] +// `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256. +#![recursion_limit = "256"] + +// Make the WASM binary available. +#[cfg(feature = "std")] +include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); + +use codec::{Compact, CompactLen, Decode, Encode}; +use core::time::Duration; +use frame_support::traits::{ + ConstU128, ConstU16, ConstU32, ConstU64, ConstU8, Currency, ExistenceRequirement, Get, + Imbalance, WithdrawReasons, +}; +use frame_support::weights::{ + constants::{RocksDbWeight, WEIGHT_PER_SECOND}, + IdentityFee, +}; +use frame_support::{construct_runtime, parameter_types}; +use frame_system::limits::{BlockLength, BlockWeights}; +use frame_system::EnsureNever; +use pallet_balances::NegativeImbalance; +use sp_api::{impl_runtime_apis, BlockT, HashT, HeaderT}; +use sp_consensus_subspace::digests::CompatibleDigestItem; +use sp_consensus_subspace::{ + EquivocationProof, FarmerPublicKey, GlobalRandomnesses, Salts, SolutionRanges, +}; +use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; +use sp_executor::{FraudProof, OpaqueBundle}; +use sp_runtime::traits::{AccountIdLookup, BlakeTwo256, DispatchInfoOf, PostDispatchInfoOf, Zero}; +use sp_runtime::transaction_validity::{ + InvalidTransaction, TransactionSource, TransactionValidity, TransactionValidityError, +}; +use sp_runtime::OpaqueExtrinsic; +use sp_runtime::{create_runtime_str, generic, ApplyExtrinsicResult, Perbill}; +use sp_std::prelude::*; +#[cfg(feature = "std")] +use sp_version::NativeVersion; +use sp_version::RuntimeVersion; +use subspace_core_primitives::objects::{BlockObject, BlockObjectMapping}; +use subspace_core_primitives::{Randomness, RootBlock, Sha256Hash, PIECE_SIZE}; +use subspace_runtime_primitives::{ + opaque, AccountId, Balance, BlockNumber, Hash, Index, Moment, Signature, CONFIRMATION_DEPTH_K, + MIN_REPLICATION_FACTOR, RECORDED_HISTORY_SEGMENT_SIZE, RECORD_SIZE, + STORAGE_FEES_ESCROW_BLOCK_REWARD, STORAGE_FEES_ESCROW_BLOCK_TAX, +}; + +sp_runtime::impl_opaque_keys! { + pub struct SessionKeys { + pub subspace: Subspace, + } +} + +// To learn more about runtime versioning and what each of the following value means: +// https://substrate.dev/docs/en/knowledgebase/runtime/upgrades#runtime-versioning +#[sp_version::runtime_version] +pub const VERSION: RuntimeVersion = RuntimeVersion { + spec_name: create_runtime_str!("subspace"), + impl_name: create_runtime_str!("subspace"), + authoring_version: 1, + // The version of the runtime specification. A full node will not attempt to use its native + // runtime in substitute for the on-chain Wasm runtime unless all of `spec_name`, + // `spec_version`, and `authoring_version` are the same between Wasm and native. + // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use + // the compatible custom types. + spec_version: 100, + impl_version: 1, + apis: RUNTIME_API_VERSIONS, + transaction_version: 1, + state_version: 1, +}; + +/// The version information used to identify this runtime when compiled natively. +#[cfg(feature = "std")] +pub fn native_version() -> NativeVersion { + NativeVersion { + runtime_version: VERSION, + can_author_with: Default::default(), + } +} + +/// The smallest unit of the token is called Shannon. +pub const SHANNON: Balance = 1; +/// Subspace Credits have 18 decimal places. +pub const DECIMAL_PLACES: u8 = 18; +/// One Subspace Credit. +pub const SSC: Balance = (10 * SHANNON).pow(DECIMAL_PLACES as u32); + +// TODO: Many of below constants should probably be updatable but currently they are not + +/// Since Subspace is probabilistic this is the average expected block time that +/// we are targeting. Blocks will be produced at a minimum duration defined +/// by `SLOT_DURATION`, but some slots will not be allocated to any +/// farmer and hence no block will be produced. We expect to have this +/// block time on average following the defined slot duration and the value +/// of `c` configured for Subspace (where `1 - c` represents the probability of +/// a slot being empty). +/// This value is only used indirectly to define the unit constants below +/// that are expressed in blocks. The rest of the code should use +/// `SLOT_DURATION` instead (like the Timestamp pallet for calculating the +/// minimum period). +/// +/// Based on: +/// +pub const MILLISECS_PER_BLOCK: u64 = 6000; + +// NOTE: Currently it is not possible to change the slot duration after the chain has started. +// Attempting to do so will brick block production. +const SLOT_DURATION: u64 = 1000; + +/// 1 in 6 slots (on average, not counting collisions) will have a block. +/// Must match ratio between block and slot duration in constants above. +const SLOT_PROBABILITY: (u64, u64) = (1, 6); + +/// The amount of time, in blocks, between updates of global randomness. +const GLOBAL_RANDOMNESS_UPDATE_INTERVAL: BlockNumber = 100; + +/// Era duration in blocks. +const ERA_DURATION_IN_BLOCKS: BlockNumber = 2016; + +const EQUIVOCATION_REPORT_LONGEVITY: BlockNumber = 256; + +/// Eon duration is 7 days +const EON_DURATION_IN_SLOTS: u64 = 3600 * 24 * 7; +/// Reveal next eon salt 1 day before eon end +const EON_NEXT_SALT_REVEAL: u64 = EON_DURATION_IN_SLOTS + .checked_sub(3600 * 24) + .expect("Offset is smaller than eon duration; qed"); + +/// Any solution range is valid in the test environment. +const INITIAL_SOLUTION_RANGE: u64 = u64::MAX; + +/// A ratio of `Normal` dispatch class within block, for `BlockWeight` and `BlockLength`. +const NORMAL_DISPATCH_RATIO: Perbill = Perbill::from_percent(75); + +/// Maximum block length for non-`Normal` extrinsic is 5 MiB. +const MAX_BLOCK_LENGTH: u32 = 5 * 1024 * 1024; + +const MAX_OBJECT_MAPPING_RECURSION_DEPTH: u16 = 5; + +parameter_types! { + pub const Version: RuntimeVersion = VERSION; + pub const BlockHashCount: BlockNumber = 2400; + /// We allow for 2 seconds of compute with a 6 second average block time. + pub SubspaceBlockWeights: BlockWeights = BlockWeights::with_sensible_defaults(2 * WEIGHT_PER_SECOND, NORMAL_DISPATCH_RATIO); + /// We allow for 3.75 MiB for `Normal` extrinsic with 5 MiB maximum block length. + pub SubspaceBlockLength: BlockLength = BlockLength::max_with_normal_ratio(MAX_BLOCK_LENGTH, NORMAL_DISPATCH_RATIO); +} + +pub type SS58Prefix = ConstU16<2254>; + +// Configure FRAME pallets to include in runtime. + +impl frame_system::Config for Runtime { + /// The basic call filter to use in dispatchable. + type BaseCallFilter = frame_support::traits::Everything; + /// Block & extrinsics weights: base values and limits. + type BlockWeights = SubspaceBlockWeights; + /// The maximum length of a block (in bytes). + type BlockLength = SubspaceBlockLength; + /// The identifier used to distinguish between accounts. + type AccountId = AccountId; + /// The aggregated dispatch type that is available for extrinsics. + type Call = Call; + /// The lookup mechanism to get account ID from whatever is passed in dispatchers. + type Lookup = AccountIdLookup; + /// The index type for storing how many extrinsics an account has signed. + type Index = Index; + /// The index type for blocks. + type BlockNumber = BlockNumber; + /// The type for hashing blocks and tries. + type Hash = Hash; + /// The hashing algorithm used. + type Hashing = BlakeTwo256; + /// The header type. + type Header = Header; + /// The ubiquitous event type. + type Event = Event; + /// The ubiquitous origin type. + type Origin = Origin; + /// Maximum number of block number to block hash mappings to keep (oldest pruned first). + type BlockHashCount = ConstU32<250>; + /// The weight of database operations that the runtime can invoke. + type DbWeight = RocksDbWeight; + /// Version of the runtime. + type Version = Version; + /// Converts a module to the index of the module in `construct_runtime!`. + /// + /// This type is being generated by `construct_runtime!`. + type PalletInfo = PalletInfo; + /// What to do if a new account is created. + type OnNewAccount = (); + /// What to do if an account is fully reaped from the system. + type OnKilledAccount = (); + /// The data to be stored in an account. + type AccountData = pallet_balances::AccountData; + /// Weight information for the extrinsics of this pallet. + type SystemWeightInfo = (); + /// This is used as an identifier of the chain. + type SS58Prefix = SS58Prefix; + /// The set code logic, just the default since we're not a parachain. + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +parameter_types! { + pub const SlotProbability: (u64, u64) = SLOT_PROBABILITY; + pub const ExpectedBlockTime: Moment = MILLISECS_PER_BLOCK; +} + +impl pallet_subspace::Config for Runtime { + type Event = Event; + type GlobalRandomnessUpdateInterval = ConstU32; + type EraDuration = ConstU32; + type EonDuration = ConstU64; + type EonNextSaltReveal = ConstU64; + type InitialSolutionRange = ConstU64; + type SlotProbability = SlotProbability; + type ExpectedBlockTime = ExpectedBlockTime; + type ConfirmationDepthK = ConstU32; + type RecordSize = ConstU32; + type RecordedHistorySegmentSize = ConstU32; + type GlobalRandomnessIntervalTrigger = pallet_subspace::NormalGlobalRandomnessInterval; + type EraChangeTrigger = pallet_subspace::NormalEraChange; + type EonChangeTrigger = pallet_subspace::NormalEonChange; + + type HandleEquivocation = pallet_subspace::equivocation::EquivocationHandler< + OffencesSubspace, + ConstU64<{ EQUIVOCATION_REPORT_LONGEVITY as u64 }>, + >; + + type WeightInfo = (); +} + +impl pallet_timestamp::Config for Runtime { + /// A timestamp: milliseconds since the unix epoch. + type Moment = Moment; + type OnTimestampSet = Subspace; + type MinimumPeriod = ConstU64<{ SLOT_DURATION / 2 }>; + type WeightInfo = (); +} + +impl pallet_balances::Config for Runtime { + type MaxLocks = ConstU32<50>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + /// The type for recording an account's balance. + type Balance = Balance; + /// The ubiquitous event type. + type Event = Event; + type DustRemoval = (); + // TODO: Correct value + type ExistentialDeposit = ConstU128<{ 500 * SHANNON }>; + type AccountStore = System; + type WeightInfo = pallet_balances::weights::SubstrateWeight; +} + +parameter_types! { + pub const StorageFeesEscrowBlockReward: (u64, u64) = STORAGE_FEES_ESCROW_BLOCK_REWARD; + pub const StorageFeesEscrowBlockTax: (u64, u64) = STORAGE_FEES_ESCROW_BLOCK_TAX; +} + +pub struct CreditSupply; + +impl Get for CreditSupply { + fn get() -> Balance { + Balances::total_issuance() + } +} + +pub struct TotalSpacePledged; + +impl Get for TotalSpacePledged { + fn get() -> u64 { + let piece_size = u64::try_from(PIECE_SIZE) + .expect("Piece size is definitely small enough to fit into u64; qed"); + // Operations reordered to avoid u64 overflow, but essentially are: + // u64::MAX * SlotProbability / (solution_range / PIECE_SIZE) + u64::MAX / Subspace::solution_ranges().current * piece_size * SlotProbability::get().0 + / SlotProbability::get().1 + } +} + +pub struct BlockchainHistorySize; + +impl Get for BlockchainHistorySize { + fn get() -> u64 { + Subspace::archived_history_size() + } +} + +impl pallet_transaction_fees::Config for Runtime { + type Event = Event; + type MinReplicationFactor = ConstU16; + type StorageFeesEscrowBlockReward = StorageFeesEscrowBlockReward; + type StorageFeesEscrowBlockTax = StorageFeesEscrowBlockTax; + type CreditSupply = CreditSupply; + type TotalSpacePledged = TotalSpacePledged; + type BlockchainHistorySize = BlockchainHistorySize; + type Currency = Balances; + type FindAuthor = Subspace; + type WeightInfo = (); +} + +pub struct TransactionByteFee; + +impl Get for TransactionByteFee { + fn get() -> Balance { + if cfg!(feature = "do-not-enforce-cost-of-storage") { + 1 + } else { + TransactionFees::transaction_byte_fee() + } + } +} + +pub struct LiquidityInfo { + storage_fee: Balance, + imbalance: NegativeImbalance, +} + +/// Implementation of [`pallet_transaction_payment::OnChargeTransaction`] that charges transaction +/// fees and distributes storage/compute fees and tip separately. +pub struct OnChargeTransaction; + +impl pallet_transaction_payment::OnChargeTransaction for OnChargeTransaction { + type LiquidityInfo = Option; + type Balance = Balance; + + fn withdraw_fee( + who: &AccountId, + call: &Call, + _info: &DispatchInfoOf, + fee: Self::Balance, + tip: Self::Balance, + ) -> Result { + if fee.is_zero() { + return Ok(None); + } + + let withdraw_reason = if tip.is_zero() { + WithdrawReasons::TRANSACTION_PAYMENT + } else { + WithdrawReasons::TRANSACTION_PAYMENT | WithdrawReasons::TIP + }; + + let withdraw_result = >::withdraw( + who, + fee, + withdraw_reason, + ExistenceRequirement::KeepAlive, + ); + let imbalance = withdraw_result.map_err(|_error| InvalidTransaction::Payment)?; + + // Separate storage fee while we have access to the call data structure to calculate it. + let storage_fee = TransactionByteFee::get() + * Balance::try_from(call.encoded_size()) + .expect("Size of the call never exceeds balance units; qed"); + + Ok(Some(LiquidityInfo { + storage_fee, + imbalance, + })) + } + + fn correct_and_deposit_fee( + who: &AccountId, + _dispatch_info: &DispatchInfoOf, + _post_info: &PostDispatchInfoOf, + corrected_fee: Self::Balance, + tip: Self::Balance, + liquidity_info: Self::LiquidityInfo, + ) -> Result<(), TransactionValidityError> { + if let Some(LiquidityInfo { + storage_fee, + imbalance, + }) = liquidity_info + { + // Calculate how much refund we should return + let refund_amount = imbalance.peek().saturating_sub(corrected_fee); + // Refund to the the account that paid the fees. If this fails, the account might have + // dropped below the existential balance. In that case we don't refund anything. + let refund_imbalance = Balances::deposit_into_existing(who, refund_amount) + .unwrap_or_else(|_| >::PositiveImbalance::zero()); + // Merge the imbalance caused by paying the fees and refunding parts of it again. + let adjusted_paid = imbalance + .offset(refund_imbalance) + .same() + .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Payment))?; + + // Split the tip from the total fee that ended up being paid. + let (tip, fee) = adjusted_paid.split(tip); + // Split paid storage and compute fees so that they can be distributed separately. + let (paid_storage_fee, paid_compute_fee) = fee.split(storage_fee); + + TransactionFees::note_transaction_fees( + paid_storage_fee.peek(), + paid_compute_fee.peek(), + tip.peek(), + ); + } + Ok(()) + } +} + +impl pallet_transaction_payment::Config for Runtime { + type OnChargeTransaction = OnChargeTransaction; + type TransactionByteFee = TransactionByteFee; + type OperationalFeeMultiplier = ConstU8<5>; + type WeightToFee = IdentityFee; + type FeeMultiplierUpdate = (); +} + +impl pallet_utility::Config for Runtime { + type Event = Event; + type Call = Call; + type PalletsOrigin = OriginCaller; + type WeightInfo = pallet_utility::weights::SubstrateWeight; +} + +impl pallet_sudo::Config for Runtime { + type Event = Event; + type Call = Call; +} + +impl frame_system::offchain::SendTransactionTypes for Runtime +where + Call: From, +{ + type Extrinsic = UncheckedExtrinsic; + type OverarchingCall = Call; +} + +impl pallet_offences_subspace::Config for Runtime { + type Event = Event; + type OnOffenceHandler = Subspace; +} + +impl pallet_executor::Config for Runtime { + type Event = Event; +} + +parameter_types! { + pub const BlockReward: Balance = SSC; +} + +impl pallet_rewards::Config for Runtime { + type Event = Event; + type Currency = Balances; + type BlockReward = BlockReward; + type FindAuthor = Subspace; + type WeightInfo = (); +} + +impl pallet_feeds::Config for Runtime { + type Event = Event; +} + +impl pallet_object_store::Config for Runtime { + type Event = Event; +} + +parameter_types! { + // This value doesn't matter, we don't use it (`VestedTransferOrigin = EnsureNever` below). + pub const MinVestedTransfer: Balance = 0; +} + +impl orml_vesting::Config for Runtime { + type Event = Event; + type Currency = Balances; + type MinVestedTransfer = MinVestedTransfer; + type VestedTransferOrigin = EnsureNever; + type WeightInfo = (); + type MaxVestingSchedules = ConstU32<2>; + type BlockNumberProvider = System; +} + +construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = opaque::Block, + UncheckedExtrinsic = UncheckedExtrinsic + { + System: frame_system = 0, + Timestamp: pallet_timestamp = 1, + + Subspace: pallet_subspace = 2, + OffencesSubspace: pallet_offences_subspace = 3, + Rewards: pallet_rewards = 9, + + Balances: pallet_balances = 4, + TransactionFees: pallet_transaction_fees = 12, + TransactionPayment: pallet_transaction_payment = 5, + Utility: pallet_utility = 8, + + Feeds: pallet_feeds = 6, + ObjectStore: pallet_object_store = 10, + Executor: pallet_executor = 11, + + Vesting: orml_vesting = 7, + + // Reserve some room for other pallets as we'll remove sudo pallet eventually. + Sudo: pallet_sudo = 100, + } +); + +/// The address format for describing accounts. +pub type Address = sp_runtime::MultiAddress; +/// Block header type as expected by this runtime. +pub type Header = generic::Header; +/// Block type as expected by this runtime. +pub type Block = generic::Block; +/// The SignedExtension to the basic transaction logic. +pub type SignedExtra = ( + frame_system::CheckNonZeroSender, + frame_system::CheckSpecVersion, + frame_system::CheckTxVersion, + frame_system::CheckGenesis, + frame_system::CheckEra, + frame_system::CheckNonce, + frame_system::CheckWeight, + pallet_transaction_payment::ChargeTransactionPayment, +); +/// Unchecked extrinsic type as expected by this runtime. +pub type UncheckedExtrinsic = generic::UncheckedExtrinsic; +/// Executive: handles dispatch to the various modules. +pub type Executive = frame_executive::Executive< + Runtime, + Block, + frame_system::ChainContext, + Runtime, + AllPalletsWithSystem, +>; +/// The payload being signed in transactions. +pub type SignedPayload = generic::SignedPayload; + +fn extract_root_blocks(ext: &UncheckedExtrinsic) -> Option> { + match &ext.function { + Call::Subspace(pallet_subspace::Call::store_root_blocks { root_blocks }) => { + Some(root_blocks.clone()) + } + _ => None, + } +} + +fn extract_feeds_block_object_mapping( + base_offset: u32, + objects: &mut Vec, + call: &pallet_feeds::Call, +) { + if let Some(call_object) = call.extract_call_object() { + objects.push(BlockObject::V0 { + hash: call_object.hash, + offset: base_offset + call_object.offset, + }); + } +} + +fn extract_object_store_block_object_mapping( + base_offset: u32, + objects: &mut Vec, + call: &pallet_object_store::Call, +) { + if let Some(call_object) = call.extract_call_object() { + objects.push(BlockObject::V0 { + hash: call_object.hash, + offset: base_offset + call_object.offset, + }); + } +} + +fn extract_utility_block_object_mapping( + mut base_offset: u32, + objects: &mut Vec, + call: &pallet_utility::Call, + mut recursion_depth_left: u16, +) { + if recursion_depth_left == 0 { + return; + } + + recursion_depth_left -= 1; + + // Add enum variant to the base offset. + base_offset += 1; + + match call { + pallet_utility::Call::batch { calls } | pallet_utility::Call::batch_all { calls } => { + base_offset += Compact::compact_len(&(calls.len() as u32)) as u32; + + for call in calls { + extract_call_block_object_mapping(base_offset, objects, call, recursion_depth_left); + + base_offset += call.encoded_size() as u32; + } + } + pallet_utility::Call::as_derivative { index, call } => { + base_offset += index.encoded_size() as u32; + + extract_call_block_object_mapping( + base_offset, + objects, + call.as_ref(), + recursion_depth_left, + ); + } + pallet_utility::Call::dispatch_as { as_origin, call } => { + base_offset += as_origin.encoded_size() as u32; + + extract_call_block_object_mapping( + base_offset, + objects, + call.as_ref(), + recursion_depth_left, + ); + } + pallet_utility::Call::__Ignore(_, _) => { + // Ignore. + } + } +} + +fn extract_call_block_object_mapping( + mut base_offset: u32, + objects: &mut Vec, + call: &Call, + recursion_depth_left: u16, +) { + // Add enum variant to the base offset. + base_offset += 1; + + match call { + Call::Feeds(call) => { + extract_feeds_block_object_mapping(base_offset, objects, call); + } + Call::ObjectStore(call) => { + extract_object_store_block_object_mapping(base_offset, objects, call); + } + Call::Utility(call) => { + extract_utility_block_object_mapping(base_offset, objects, call, recursion_depth_left); + } + _ => {} + } +} + +fn extract_block_object_mapping(block: Block) -> BlockObjectMapping { + let mut block_object_mapping = BlockObjectMapping::default(); + let mut base_offset = + block.header.encoded_size() + Compact::compact_len(&(block.extrinsics.len() as u32)); + for extrinsic in block.extrinsics { + let signature_size = extrinsic + .signature + .as_ref() + .map(|s| s.encoded_size()) + .unwrap_or_default(); + // Extrinsic starts with vector length and version byte, followed by optional signature and + // `function` encoding. + let base_extrinsic_offset = base_offset + + Compact::compact_len( + &((1 + signature_size + extrinsic.function.encoded_size()) as u32), + ) + + 1 + + signature_size; + + extract_call_block_object_mapping( + base_extrinsic_offset as u32, + &mut block_object_mapping.objects, + &extrinsic.function, + MAX_OBJECT_MAPPING_RECURSION_DEPTH, + ); + + base_offset += extrinsic.encoded_size(); + } + + block_object_mapping +} + +fn extract_bundles(extrinsics: Vec) -> Vec { + extrinsics + .into_iter() + .filter_map(|opaque_extrinsic| { + match ::decode(&mut opaque_extrinsic.encode().as_slice()) { + Ok(uxt) => { + if let Call::Executor(pallet_executor::Call::submit_transaction_bundle { + opaque_bundle, + }) = uxt.function + { + Some(opaque_bundle) + } else { + None + } + } + Err(_) => None, + } + }) + .collect() +} + +fn extrinsics_shuffling_seed(header: Block::Header) -> Randomness { + if header.number().is_zero() { + Randomness::default() + } else { + let mut pre_digest: Option<_> = None; + for log in header.digest().logs() { + match ( + log.as_subspace_pre_digest::(), + pre_digest.is_some(), + ) { + (Some(_), true) => panic!("Multiple Subspace pre-runtime digests in a header"), + (None, _) => {} + (s, false) => pre_digest = s, + } + } + + let pre_digest = pre_digest.expect("Header must contain one pre-runtime digest; qed"); + + BlakeTwo256::hash_of(&pre_digest.solution.signature).into() + } +} + +impl_runtime_apis! { + impl sp_api::Core for Runtime { + fn version() -> RuntimeVersion { + VERSION + } + + fn execute_block(block: Block) { + Executive::execute_block(block); + } + + fn initialize_block(header: &::Header) { + Executive::initialize_block(header) + } + } + + impl sp_api::Metadata for Runtime { + fn metadata() -> OpaqueMetadata { + OpaqueMetadata::new(Runtime::metadata().into()) + } + } + + impl sp_block_builder::BlockBuilder for Runtime { + fn apply_extrinsic(extrinsic: ::Extrinsic) -> ApplyExtrinsicResult { + Executive::apply_extrinsic(extrinsic) + } + + fn finalize_block() -> ::Header { + Executive::finalize_block() + } + + fn inherent_extrinsics(data: sp_inherents::InherentData) -> Vec<::Extrinsic> { + data.create_extrinsics() + } + + fn check_inherents( + block: Block, + data: sp_inherents::InherentData, + ) -> sp_inherents::CheckInherentsResult { + data.check_extrinsics(&block) + } + } + + impl sp_transaction_pool::runtime_api::TaggedTransactionQueue for Runtime { + fn validate_transaction( + source: TransactionSource, + tx: ::Extrinsic, + block_hash: ::Hash, + ) -> TransactionValidity { + Executive::validate_transaction(source, tx, block_hash) + } + } + + impl sp_offchain::OffchainWorkerApi for Runtime { + fn offchain_worker(header: &::Header) { + Executive::offchain_worker(header) + } + } + + impl sp_consensus_subspace::SubspaceApi for Runtime { + fn confirmation_depth_k() -> <::Header as HeaderT>::Number { + ::ConfirmationDepthK::get() + } + + fn record_size() -> u32 { + ::RecordSize::get() + } + + fn recorded_history_segment_size() -> u32 { + ::RecordedHistorySegmentSize::get() + } + + fn slot_duration() -> Duration { + Duration::from_millis(Subspace::slot_duration()) + } + + fn global_randomnesses() -> GlobalRandomnesses { + Subspace::global_randomnesses() + } + + fn solution_ranges() -> SolutionRanges { + Subspace::solution_ranges() + } + + fn salts() -> Salts { + Subspace::salts() + } + + fn submit_report_equivocation_extrinsic( + equivocation_proof: EquivocationProof<::Header>, + ) -> Option<()> { + Subspace::submit_equivocation_report(equivocation_proof) + } + + fn is_in_block_list(farmer_public_key: &FarmerPublicKey) -> bool { + // TODO: Either check tx pool too for pending equivocations or replace equivocation + // mechanism with an alternative one, so that blocking happens faster + Subspace::is_in_block_list(farmer_public_key) + } + + fn records_root(segment_index: u64) -> Option { + Subspace::records_root(segment_index) + } + + fn extract_root_blocks(ext: &::Extrinsic) -> Option> { + extract_root_blocks(ext) + } + + fn extract_block_object_mapping(block: Block) -> BlockObjectMapping { + extract_block_object_mapping(block) + } + } + + impl sp_executor::ExecutorApi for Runtime { + fn submit_execution_receipt_unsigned( + opaque_execution_receipt: sp_executor::OpaqueExecutionReceipt, + ) -> Option<()> { + ::Hash>>::decode( + &mut opaque_execution_receipt.encode().as_slice(), + ) + .ok() + .and_then(|execution_receipt| { + Executor::submit_execution_receipt_unsigned(execution_receipt).ok() + }) + } + + fn submit_transaction_bundle_unsigned(opaque_bundle: OpaqueBundle) -> Option<()> { + Executor::submit_transaction_bundle_unsigned(opaque_bundle).ok() + } + + fn submit_fraud_proof_unsigned(fraud_proof: FraudProof) -> Option<()> { + Executor::submit_fraud_proof_unsigned(fraud_proof).ok() + } + + fn submit_bundle_equivocation_proof_unsigned( + bundle_equivocation_proof: sp_executor::BundleEquivocationProof, + ) -> Option<()> { + Executor::submit_bundle_equivocation_proof_unsigned(bundle_equivocation_proof).ok() + } + + fn submit_invalid_transaction_proof_unsigned( + invalid_transaction_proof: sp_executor::InvalidTransactionProof, + ) -> Option<()> { + Executor::submit_invalid_transaction_proof_unsigned(invalid_transaction_proof).ok() + } + + fn extract_bundles(extrinsics: Vec) -> Vec { + extract_bundles(extrinsics) + } + + fn extrinsics_shuffling_seed(header: ::Header) -> Randomness { + extrinsics_shuffling_seed::(header) + } + } + + impl sp_session::SessionKeys for Runtime { + fn generate_session_keys(seed: Option>) -> Vec { + SessionKeys::generate(seed) + } + + fn decode_session_keys( + encoded: Vec, + ) -> Option, KeyTypeId)>> { + SessionKeys::decode_into_raw_public_keys(&encoded) + } + } + + impl frame_system_rpc_runtime_api::AccountNonceApi for Runtime { + fn account_nonce(account: AccountId) -> Index { + System::account_nonce(account) + } + } + + impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentApi for Runtime { + fn query_info( + uxt: ::Extrinsic, + len: u32, + ) -> pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo { + TransactionPayment::query_info(uxt, len) + } + fn query_fee_details( + uxt: ::Extrinsic, + len: u32, + ) -> pallet_transaction_payment::FeeDetails { + TransactionPayment::query_fee_details(uxt, len) + } + } +} diff --git a/test/subspace-test-service/Cargo.toml b/test/subspace-test-service/Cargo.toml new file mode 100644 index 0000000000000..1e5b1b7e3674c --- /dev/null +++ b/test/subspace-test-service/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "subspace-test-service" +version = "0.1.0" +authors = ["Subspace Labs "] +edition = "2021" +license = "GPL-3.0-or-later" +homepage = "https://subspace.network" +repository = "https://github.com/subspace/subspace" +include = [ + "/src", + "/Cargo.toml", +] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +frame-system = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +futures = "0.3.19" +rand = "0.8.3" +pallet-balances = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +pallet-transaction-payment = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +polkadot-overseer = { path = "../../polkadot/node/overseer" } +sc-client-api = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sc-network = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sc-service = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927", features = ["wasmtime"] } +sc-tracing = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sp-arithmetic = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sp-blockchain = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sp-keyring = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sp-runtime = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +subspace-runtime-primitives = { path = "../../crates/subspace-runtime-primitives" } +subspace-service = { path = "../../crates/subspace-service" } +subspace-test-client = { path = "../subspace-test-client" } +subspace-test-runtime = { version = "0.1.0", features = ["do-not-enforce-cost-of-storage"], path = "../subspace-test-runtime" } +substrate-test-client = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +tokio = "1.14.0" + +[dev-dependencies] +sc-cli = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +sp-keyring = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } +substrate-test-utils = { git = "https://github.com/paritytech/substrate", rev = "e6def65920d30029e42d498cb07cec5dd433b927" } diff --git a/test/subspace-test-service/src/lib.rs b/test/subspace-test-service/src/lib.rs new file mode 100644 index 0000000000000..bd2acf973c16d --- /dev/null +++ b/test/subspace-test-service/src/lib.rs @@ -0,0 +1,309 @@ +// Copyright (C) 2021 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 test service only. + +#![warn(missing_docs, unused_crate_dependencies)] + +use futures::future::Future; +use polkadot_overseer::Handle; +use sc_client_api::execution_extensions::ExecutionStrategies; +use sc_network::{ + config::{NetworkConfiguration, TransportConfig}, + multiaddr, +}; +use sc_service::{ + config::{DatabaseSource, KeystoreConfig, MultiaddrWithPeerId, WasmExecutionMethod}, + BasePath, Configuration, KeepBlocks, Role, RpcHandlers, TaskManager, TransactionStorageMode, +}; +use sp_arithmetic::traits::SaturatedConversion; +use sp_blockchain::HeaderBackend; +use sp_keyring::Sr25519Keyring; +use sp_runtime::{codec::Encode, generic, traits::IdentifyAccount, MultiSigner}; +use std::sync::Arc; +use subspace_runtime_primitives::Balance; +use subspace_service::NewFull; +use subspace_test_client::{chain_spec, start_farmer, Client, TestExecutorDispatch}; +use subspace_test_runtime::{ + BlockHashCount, Runtime, SignedExtra, SignedPayload, UncheckedExtrinsic, VERSION, +}; +use substrate_test_client::{ + BlockchainEventsExt, RpcHandlersExt, RpcTransactionError, RpcTransactionOutput, +}; + +/// Create a new full node. +#[sc_tracing::logging::prefix_logs_with(config.network.node_name.as_str())] +pub fn new_full( + config: Configuration, + enable_rpc_extensions: bool, + run_farmer: bool, +) -> NewFull> { + let new_full = futures::executor::block_on(subspace_service::new_full::< + subspace_test_runtime::RuntimeApi, + TestExecutorDispatch, + >(config, enable_rpc_extensions)) + .expect("Failed to create Subspace full client"); + if run_farmer { + start_farmer(&new_full); + } + new_full +} + +/// Create a Subspace `Configuration`. +/// +/// By default an in-memory socket will be used, therefore you need to provide boot +/// nodes if you want the future node to be connected to other nodes. +pub fn node_config( + tokio_handle: tokio::runtime::Handle, + key: Sr25519Keyring, + boot_nodes: Vec, + is_validator: bool, +) -> Configuration { + let base_path = BasePath::new_temp_dir().expect("Could not create temporary directory"); + let root = base_path.path(); + let role = if is_validator { + Role::Authority + } else { + Role::Full + }; + let key_seed = key.to_seed(); + let spec = chain_spec::subspace_local_testnet_config(); + + let mut network_config = NetworkConfiguration::new( + key_seed.to_string(), + "network/test/0.1", + Default::default(), + None, + ); + + network_config.boot_nodes = boot_nodes; + + network_config.allow_non_globals_in_dht = true; + + let addr: multiaddr::Multiaddr = multiaddr::Protocol::Memory(rand::random()).into(); + network_config.listen_addresses.push(addr.clone()); + + network_config.public_addresses.push(addr); + + network_config.transport = TransportConfig::MemoryOnly; + + Configuration { + impl_name: "subspace-test-node".to_string(), + impl_version: "0.1".to_string(), + role, + tokio_handle, + transaction_pool: Default::default(), + network: network_config, + keystore: KeystoreConfig::InMemory, + keystore_remote: Default::default(), + database: DatabaseSource::RocksDb { + path: root.join("db"), + cache_size: 128, + }, + state_cache_size: 16777216, + state_cache_child_ratio: None, + state_pruning: Default::default(), + keep_blocks: KeepBlocks::All, + transaction_storage: TransactionStorageMode::BlockBody, + chain_spec: Box::new(spec), + wasm_method: WasmExecutionMethod::Interpreted, + wasm_runtime_overrides: Default::default(), + // NOTE: we enforce the use of the native runtime to make the errors more debuggable + execution_strategies: ExecutionStrategies { + syncing: sc_client_api::ExecutionStrategy::NativeWhenPossible, + importing: sc_client_api::ExecutionStrategy::NativeWhenPossible, + block_construction: sc_client_api::ExecutionStrategy::NativeWhenPossible, + offchain_worker: sc_client_api::ExecutionStrategy::NativeWhenPossible, + other: sc_client_api::ExecutionStrategy::NativeWhenPossible, + }, + rpc_http: None, + rpc_ws: None, + rpc_ipc: None, + rpc_max_payload: None, + rpc_ws_max_connections: None, + rpc_cors: None, + rpc_methods: Default::default(), + ws_max_out_buffer_capacity: None, + prometheus_config: None, + telemetry_endpoints: None, + default_heap_pages: None, + offchain_worker: Default::default(), + force_authoring: false, + disable_grandpa: false, + dev_key_seed: Some(key_seed), + tracing_targets: None, + tracing_receiver: Default::default(), + max_runtime_instances: 8, + announce_block: true, + base_path: Some(base_path), + informant_output_format: Default::default(), + runtime_cache_size: 2, + } +} + +/// Run a test validator node that uses the test runtime. +/// +/// The node will be using an in-memory socket, therefore you need to provide boot nodes if you +/// want it to be connected to other nodes. +pub fn run_validator_node( + tokio_handle: tokio::runtime::Handle, + key: Sr25519Keyring, + boot_nodes: Vec, + is_validator: bool, +) -> SubspaceTestNode { + let config = node_config(tokio_handle, key, boot_nodes, is_validator); + let multiaddr = config.network.listen_addresses[0].clone(); + let NewFull { + task_manager, + client, + network, + rpc_handlers, + overseer_handle, + .. + } = new_full(config, is_validator, true); + + let peer_id = *network.local_peer_id(); + let addr = MultiaddrWithPeerId { multiaddr, peer_id }; + + SubspaceTestNode { + task_manager, + client, + overseer_handle, + addr, + rpc_handlers, + } +} + +/// A Subspace test node instance used for testing. +pub struct SubspaceTestNode { + /// `TaskManager`'s instance. + pub task_manager: TaskManager, + /// Client's instance. + pub client: Arc, + /// A handle to Overseer. + pub overseer_handle: Option, + /// The `MultiaddrWithPeerId` to this node. This is useful if you want to pass it as "boot node" to other nodes. + pub addr: MultiaddrWithPeerId, + /// `RPCHandlers` to make RPC queries. + pub rpc_handlers: RpcHandlers, +} + +impl SubspaceTestNode { + /// Send an extrinsic to this node. + pub async fn send_extrinsic( + &self, + function: impl Into, + caller: Sr25519Keyring, + ) -> Result { + let extrinsic = construct_extrinsic(&*self.client, function, caller, 0); + self.rpc_handlers.send_transaction(extrinsic.into()).await + } + + /// Wait for `count` blocks to be imported in the node and then exit. This function will not return if no blocks + /// are ever created, thus you should restrict the maximum amount of time of the test execution. + pub fn wait_for_blocks(&self, count: usize) -> impl Future { + self.client.wait_for_blocks(count) + } +} + +/// Construct an extrinsic that can be applied to the test runtime. +pub fn construct_extrinsic( + client: &Client, + function: impl Into, + caller: Sr25519Keyring, + nonce: u32, +) -> UncheckedExtrinsic { + let function = function.into(); + let current_block_hash = client.info().best_hash; + let current_block = client.info().best_number.saturated_into(); + let genesis_block = client.hash(0).unwrap().unwrap(); + let block_hash_count: u32 = BlockHashCount::get(); + let period = block_hash_count + .checked_next_power_of_two() + .map(|c| c / 2) + .unwrap_or(2) as u64; + let tip = 0; + let extra: SignedExtra = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckEra::::from(generic::Era::mortal(period, current_block)), + frame_system::CheckNonce::::from(nonce), + frame_system::CheckWeight::::new(), + pallet_transaction_payment::ChargeTransactionPayment::::from(tip), + ); + let raw_payload = SignedPayload::from_raw( + function.clone(), + extra.clone(), + ( + (), + VERSION.spec_version, + VERSION.transaction_version, + genesis_block, + current_block_hash, + (), + (), + (), + ), + ); + let signature = raw_payload.using_encoded(|e| caller.sign(e)); + UncheckedExtrinsic::new_signed( + function, + subspace_test_runtime::Address::Id(caller.public().into()), + sp_runtime::MultiSignature::Sr25519(signature), + extra, + ) +} + +/// Construct a transfer extrinsic. +pub fn construct_transfer_extrinsic( + client: &Client, + origin: sp_keyring::AccountKeyring, + dest: sp_keyring::AccountKeyring, + value: Balance, +) -> UncheckedExtrinsic { + let function = subspace_test_runtime::Call::Balances(pallet_balances::Call::transfer { + dest: MultiSigner::from(dest.public()).into_account().into(), + value, + }); + + construct_extrinsic(client, function, origin, 0) +} + +#[cfg(test)] +mod tests { + use super::run_validator_node; + use sp_keyring::Sr25519Keyring::{Alice, Bob}; + + // TODO: always enable the test to catch any potential regressions. + #[substrate_test_utils::test] + #[ignore] + async fn test_primary_node_catching_up() { + let mut builder = sc_cli::LoggerBuilder::new(""); + builder.with_colors(false); + let _ = builder.init(); + + let tokio_handle = tokio::runtime::Handle::current(); + + // start alice + let alice = run_validator_node(tokio_handle.clone(), Alice, vec![], true); + + let bob = run_validator_node(tokio_handle.clone(), Bob, vec![alice.addr], false); + + bob.wait_for_blocks(10).await; + } +}