diff --git a/Cargo.lock b/Cargo.lock index df4a1fbe34..368d370314 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7168,6 +7168,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-consensus-slots", + "sp-consensus-subspace", "sp-core", "sp-domains", "sp-domains-fraud-proof", diff --git a/crates/pallet-domains/Cargo.toml b/crates/pallet-domains/Cargo.toml index 47d27c2354..4c9a00c409 100644 --- a/crates/pallet-domains/Cargo.toml +++ b/crates/pallet-domains/Cargo.toml @@ -20,6 +20,7 @@ frame-system = { version = "4.0.0-dev", default-features = false, git = "https:/ log = { version = "0.4.20", default-features = false } scale-info = { version = "2.7.0", default-features = false, features = ["derive"] } sp-consensus-slots = { version = "0.10.0-dev", default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "d6b500960579d73c43fc4ef550b703acfa61c4c8" } +sp-consensus-subspace = { version = "0.1.0", default-features = false, path = "../sp-consensus-subspace" } sp-core = { version = "21.0.0", default-features = false, git = "https://github.com/subspace/polkadot-sdk", rev = "d6b500960579d73c43fc4ef550b703acfa61c4c8" } sp-domains = { version = "0.1.0", default-features = false, path = "../sp-domains" } sp-domains-fraud-proof = { version = "0.1.0", default-features = false, path = "../sp-domains-fraud-proof" } @@ -50,6 +51,7 @@ std = [ "log/std", "scale-info/std", "sp-consensus-slots/std", + "sp-consensus-subspace/std", "sp-core/std", "sp-domains/std", "sp-domains-fraud-proof/std", diff --git a/crates/pallet-domains/src/lib.rs b/crates/pallet-domains/src/lib.rs index ae6463d733..25b8159dc3 100644 --- a/crates/pallet-domains/src/lib.rs +++ b/crates/pallet-domains/src/lib.rs @@ -45,6 +45,8 @@ use frame_system::offchain::SubmitTransaction; use frame_system::pallet_prelude::*; pub use pallet::*; use scale_info::TypeInfo; +use sp_consensus_subspace::consensus::is_proof_of_time_valid; +use sp_consensus_subspace::WrappedPotOutput; use sp_core::H256; use sp_domains::bundle_producer_election::BundleProducerElectionParams; use sp_domains::{ @@ -66,7 +68,7 @@ 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_core_primitives::{BlockHash, SlotNumber, U256}; use subspace_runtime_primitives::Balance; pub(crate) type BalanceOf = @@ -83,6 +85,11 @@ pub trait HoldIdentifier { fn storage_fund_withdrawal(operator_id: OperatorId) -> FungibleHoldId; } +pub trait BlockSlot { + fn current_slot() -> sp_consensus_slots::Slot; + fn future_slot() -> sp_consensus_slots::Slot; +} + pub type ExecutionReceiptOf = ExecutionReceipt< BlockNumberFor, ::Hash, @@ -143,8 +150,8 @@ mod pallet { use crate::staking_epoch::{do_finalize_domain_current_epoch, Error as StakingEpochError}; use crate::weights::WeightInfo; use crate::{ - BalanceOf, DomainBlockNumberFor, ElectionVerificationParams, HoldIdentifier, NominatorId, - OpaqueBundleOf, ReceiptHashFor, STORAGE_VERSION, + BalanceOf, BlockSlot, DomainBlockNumberFor, ElectionVerificationParams, HoldIdentifier, + NominatorId, OpaqueBundleOf, ReceiptHashFor, STORAGE_VERSION, }; use alloc::string::String; use codec::FullCodec; @@ -311,6 +318,9 @@ mod pallet { /// Storage fee interface used to deal with bundle storage fee type StorageFee: StorageFee>; + + /// The block slot + type BlockSlot: BlockSlot; } #[pallet::pallet] @@ -585,10 +595,16 @@ mod pallet { Receipt(BlockTreeError), /// Bundle size exceed the max bundle size limit in the domain config BundleTooLarge, - // Bundle with an invalid extrinsic root + /// Bundle with an invalid extrinsic root InvalidExtrinsicRoot, /// This bundle duplicated with an already submitted bundle DuplicatedBundle, + /// Invalid proof of time in the proof of election + InvalidProofOfTime, + /// The bundle is built on a slot in the future + SlotInTheFuture, + /// The bundle is built on a slot in the past + SlotInThePast, } #[derive(TypeInfo, Encode, Decode, PalletError, Debug, PartialEq)] @@ -1380,7 +1396,7 @@ mod pallet { type Call = Call; fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> { match call { - Call::submit_bundle { opaque_bundle } => Self::validate_bundle(opaque_bundle) + Call::submit_bundle { opaque_bundle } => Self::validate_bundle(opaque_bundle, true) .map_err(|_| InvalidTransaction::Call.into()) .and_then(|_| { charge_bundle_storage_fee::( @@ -1398,7 +1414,7 @@ mod pallet { fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { match call { Call::submit_bundle { opaque_bundle } => { - if let Err(e) = Self::validate_bundle(opaque_bundle) { + if let Err(e) = Self::validate_bundle(opaque_bundle, false) { match e { // These errors are common due to networking delay or chain re-org, // using a lower log level to avoid the noise. @@ -1406,7 +1422,9 @@ mod pallet { | BundleError::Receipt(BlockTreeError::StaleReceipt) | BundleError::Receipt(BlockTreeError::NewBranchReceipt) | BundleError::Receipt(BlockTreeError::UnavailableConsensusBlockHash) - | BundleError::Receipt(BlockTreeError::BuiltOnUnknownConsensusBlock) => { + | BundleError::Receipt(BlockTreeError::BuiltOnUnknownConsensusBlock) + | BundleError::SlotInThePast + | BundleError::SlotInTheFuture => { log::debug!( target: "runtime::domains", "Bad bundle {:?}, error: {e:?}", opaque_bundle.domain_id(), @@ -1571,7 +1589,10 @@ impl Pallet { Ok(()) } - fn validate_bundle(opaque_bundle: &OpaqueBundleOf) -> Result<(), BundleError> { + fn validate_bundle( + opaque_bundle: &OpaqueBundleOf, + pre_dispatch: bool, + ) -> Result<(), BundleError> { let domain_id = opaque_bundle.domain_id(); let operator_id = opaque_bundle.operator_id(); let sealed_header = &opaque_bundle.sealed_header; @@ -1606,9 +1627,37 @@ impl Pallet { Self::check_extrinsics_root(opaque_bundle)?; let proof_of_election = &sealed_header.header.proof_of_election; + let slot_number = proof_of_election.slot_number; let (operator_stake, total_domain_stake) = Self::fetch_operator_stake_info(domain_id, &operator_id)?; + // Check if the bundle is built with slot and valid proof-of-time that produced between the parent block + // and the current block + // + // NOTE: during `validate_unsigned` `current_slot` is query from the parent block and during `pre_dispatch` + // the `future_slot` is query from the current block. + if pre_dispatch { + ensure!( + slot_number <= *T::BlockSlot::future_slot(), + BundleError::SlotInTheFuture + ) + } else { + ensure!( + slot_number > *T::BlockSlot::current_slot(), + BundleError::SlotInThePast + ); + } + if !is_proof_of_time_valid( + BlockHash::try_from(frame_system::Pallet::::parent_hash().as_ref()) + .expect("Must be able to convert to block hash type"), + SlotNumber::from(slot_number), + WrappedPotOutput::from(proof_of_election.proof_of_time), + // Quick verification when entering transaction pool, but not when constructing the block + !pre_dispatch, + ) { + return Err(BundleError::InvalidProofOfTime); + } + sp_domains::bundle_producer_election::check_proof_of_election( &operator.signing_key, domain_config.bundle_slot_probability, diff --git a/crates/pallet-domains/src/tests.rs b/crates/pallet-domains/src/tests.rs index 8bd5b18ef1..a723bf6c2d 100644 --- a/crates/pallet-domains/src/tests.rs +++ b/crates/pallet-domains/src/tests.rs @@ -2,7 +2,7 @@ use crate::block_tree::BlockTreeNode; use crate::domain_registry::{DomainConfig, DomainObject}; use crate::staking::Operator; use crate::{ - self as pallet_domains, BalanceOf, BlockTree, BlockTreeNodes, BundleError, Config, + self as pallet_domains, BalanceOf, BlockSlot, BlockTree, BlockTreeNodes, BundleError, Config, ConsensusBlockHash, DomainBlockNumberFor, DomainHashingFor, DomainRegistry, ExecutionInbox, ExecutionReceiptOf, FraudProofError, FungibleHoldId, HeadReceiptNumber, NextDomainId, OperatorStatus, Operators, ReceiptHashFor, @@ -48,7 +48,6 @@ use sp_std::sync::Arc; use sp_trie::trie_types::TrieDBMutBuilderV1; use sp_trie::{LayoutV1, PrefixedMemoryDB, StorageProof, TrieMut}; use sp_version::RuntimeVersion; -use std::sync::atomic::{AtomicU64, Ordering}; use subspace_core_primitives::{Randomness, U256 as P256}; use subspace_runtime_primitives::{Moment, StorageFee, SSC}; @@ -116,23 +115,10 @@ parameter_types! { pub const BlockTreePruningDepth: u32 = 16; } -static CONFIRMATION_DEPTH_K: AtomicU64 = AtomicU64::new(10); - pub struct ConfirmationDepthK; - -impl ConfirmationDepthK { - fn set(new: BlockNumber) { - CONFIRMATION_DEPTH_K.store(new, Ordering::SeqCst); - } - - fn get() -> BlockNumber { - CONFIRMATION_DEPTH_K.load(Ordering::SeqCst) - } -} - impl Get for ConfirmationDepthK { fn get() -> BlockNumber { - Self::get() + 10 } } @@ -223,6 +209,17 @@ impl StorageFee for DummyStorageFee { fn note_storage_fees(_fee: Balance) {} } +pub struct DummyBlockSlot; +impl BlockSlot for DummyBlockSlot { + fn current_slot() -> sp_consensus_slots::Slot { + 0u64.into() + } + + fn future_slot() -> sp_consensus_slots::Slot { + 0u64.into() + } +} + impl pallet_domains::Config for Test { type RuntimeEvent = RuntimeEvent; type DomainHash = sp_core::H256; @@ -252,6 +249,7 @@ impl pallet_domains::Config for Test { type SudoId = (); type PalletId = DomainsPalletId; type StorageFee = DummyStorageFee; + type BlockSlot = DummyBlockSlot; } pub struct ExtrinsicStorageFees; @@ -646,110 +644,6 @@ pub(crate) fn get_block_tree_node_at( BlockTree::::get(domain_id, block_number).and_then(BlockTreeNodes::::get) } -// TODO: Unblock once bundle producer election v2 is finished. -#[test] -#[ignore] -fn test_stale_bundle_should_be_rejected() { - // Small macro in order to be more readable. - // - // We only care about whether the error type is `StaleBundle`. - macro_rules! assert_stale { - ($validate_bundle_result:expr) => { - assert_eq!( - $validate_bundle_result, - Err(pallet_domains::BundleError::StaleBundle) - ) - }; - } - - macro_rules! assert_not_stale { - ($validate_bundle_result:expr) => { - assert_ne!( - $validate_bundle_result, - Err(pallet_domains::BundleError::StaleBundle) - ) - }; - } - - ConfirmationDepthK::set(1); - new_test_ext().execute_with(|| { - // Create a bundle at genesis block -> #1 - let bundle0 = create_dummy_bundle(DOMAIN_ID, 0, System::parent_hash()); - System::initialize(&1, &System::parent_hash(), &Default::default()); - >::on_initialize(1); - assert_not_stale!(pallet_domains::Pallet::::validate_bundle(&bundle0)); - - // Create a bundle at block #1 -> #2 - let block_hash1 = Hash::random(); - let bundle1 = create_dummy_bundle(DOMAIN_ID, 1, block_hash1); - System::initialize(&2, &block_hash1, &Default::default()); - >::on_initialize(2); - assert_stale!(pallet_domains::Pallet::::validate_bundle(&bundle0)); - assert_not_stale!(pallet_domains::Pallet::::validate_bundle(&bundle1)); - }); - - ConfirmationDepthK::set(2); - new_test_ext().execute_with(|| { - // Create a bundle at genesis block -> #1 - let bundle0 = create_dummy_bundle(DOMAIN_ID, 0, System::parent_hash()); - System::initialize(&1, &System::parent_hash(), &Default::default()); - >::on_initialize(1); - assert_not_stale!(pallet_domains::Pallet::::validate_bundle(&bundle0)); - - // Create a bundle at block #1 -> #2 - let block_hash1 = Hash::random(); - let bundle1 = create_dummy_bundle(DOMAIN_ID, 1, block_hash1); - System::initialize(&2, &block_hash1, &Default::default()); - >::on_initialize(2); - assert_stale!(pallet_domains::Pallet::::validate_bundle(&bundle0)); - assert_not_stale!(pallet_domains::Pallet::::validate_bundle(&bundle1)); - - // Create a bundle at block #2 -> #3 - let block_hash2 = Hash::random(); - let bundle2 = create_dummy_bundle(DOMAIN_ID, 2, block_hash2); - System::initialize(&3, &block_hash2, &Default::default()); - >::on_initialize(3); - assert_stale!(pallet_domains::Pallet::::validate_bundle(&bundle0)); - assert_stale!(pallet_domains::Pallet::::validate_bundle(&bundle1)); - assert_not_stale!(pallet_domains::Pallet::::validate_bundle(&bundle2)); - }); - - ConfirmationDepthK::set(10); - let confirmation_depth_k = ConfirmationDepthK::get(); - let (dummy_bundles, block_hashes): (Vec<_>, Vec<_>) = (1..=confirmation_depth_k + 2) - .map(|n| { - let consensus_block_hash = Hash::random(); - ( - create_dummy_bundle(DOMAIN_ID, n, consensus_block_hash), - consensus_block_hash, - ) - }) - .unzip(); - - let run_to_block = |n: BlockNumber, block_hashes: Vec| { - System::initialize(&1, &System::parent_hash(), &Default::default()); - >::on_initialize(1); - System::finalize(); - - for b in 2..=n { - System::set_block_number(b); - System::initialize(&b, &block_hashes[b as usize - 2], &Default::default()); - >::on_initialize(b); - System::finalize(); - } - }; - - new_test_ext().execute_with(|| { - run_to_block(confirmation_depth_k + 2, block_hashes); - for bundle in dummy_bundles.iter().take(2) { - assert_stale!(pallet_domains::Pallet::::validate_bundle(bundle)); - } - for bundle in dummy_bundles.iter().skip(2) { - assert_not_stale!(pallet_domains::Pallet::::validate_bundle(bundle)); - } - }); -} - #[test] fn test_calculate_tx_range() { let cur_tx_range = P256::from(400_u64); diff --git a/crates/sc-consensus-subspace-rpc/src/lib.rs b/crates/sc-consensus-subspace-rpc/src/lib.rs index 80007d60b2..a0d79fe6f2 100644 --- a/crates/sc-consensus-subspace-rpc/src/lib.rs +++ b/crates/sc-consensus-subspace-rpc/src/lib.rs @@ -421,7 +421,8 @@ where } let global_challenge = new_slot_info - .global_randomness + .proof_of_time + .derive_global_randomness() .derive_global_challenge(slot_number); // This will be sent to the farmer diff --git a/crates/sc-consensus-subspace/src/slot_worker.rs b/crates/sc-consensus-subspace/src/slot_worker.rs index 2b091ea93d..d225566e25 100644 --- a/crates/sc-consensus-subspace/src/slot_worker.rs +++ b/crates/sc-consensus-subspace/src/slot_worker.rs @@ -69,8 +69,8 @@ use std::pin::Pin; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use subspace_core_primitives::{ - BlockNumber, PotCheckpoints, PotOutput, PublicKey, Randomness, RewardSignature, SectorId, - Solution, SolutionRange, REWARD_SIGNING_CONTEXT, + BlockNumber, PotCheckpoints, PotOutput, PublicKey, RewardSignature, SectorId, Solution, + SolutionRange, REWARD_SIGNING_CONTEXT, }; use subspace_proof_of_space::Table; use subspace_verification::{ @@ -136,8 +136,8 @@ where pub struct NewSlotInfo { /// Slot pub slot: Slot, - /// Global randomness - pub global_randomness: Randomness, + /// The PoT output for `slot` + pub proof_of_time: PotOutput, /// Acceptable solution range for block authoring pub solution_range: SolutionRange, /// Acceptable solution range for voting @@ -272,7 +272,6 @@ where } let proof_of_time = checkpoints.output(); - let global_randomness = proof_of_time.derive_global_randomness(); // NOTE: Best hash is not necessarily going to be the parent of corresponding block, but // solution range shouldn't be too far off @@ -293,7 +292,7 @@ where let new_slot_info = NewSlotInfo { slot, - global_randomness, + proof_of_time, solution_range, voting_solution_range, }; diff --git a/crates/sp-domains/src/lib.rs b/crates/sp-domains/src/lib.rs index 4b9d4cbed7..2bf5415bea 100644 --- a/crates/sp-domains/src/lib.rs +++ b/crates/sp-domains/src/lib.rs @@ -60,7 +60,7 @@ use sp_trie::TrieLayout; use sp_version::RuntimeVersion; use sp_weights::Weight; use subspace_core_primitives::crypto::blake3_hash; -use subspace_core_primitives::{bidirectional_distance, Blake3Hash, Randomness, U256}; +use subspace_core_primitives::{bidirectional_distance, Blake3Hash, PotOutput, Randomness, U256}; use subspace_runtime_primitives::{Balance, Moment}; /// Key type for Operator. @@ -611,8 +611,8 @@ pub struct ProofOfElection { pub domain_id: DomainId, /// The slot number. pub slot_number: u64, - /// Global randomness. - pub global_randomness: Randomness, + /// The PoT output for `slot_number`. + pub proof_of_time: PotOutput, /// VRF signature. pub vrf_signature: VrfSignature, /// Operator index in the OperatorRegistry. @@ -627,7 +627,8 @@ impl ProofOfElection { operator_signing_key: &OperatorPublicKey, ) -> Result<(), ProofOfElectionError> { let global_challenge = self - .global_randomness + .proof_of_time + .derive_global_randomness() .derive_global_challenge(self.slot_number); bundle_producer_election::verify_vrf_signature( self.domain_id, @@ -657,7 +658,7 @@ impl ProofOfElection { Self { domain_id, slot_number: 0u64, - global_randomness: Randomness::default(), + proof_of_time: PotOutput::default(), vrf_signature, operator_id, consensus_block_hash: Default::default(), diff --git a/crates/subspace-malicious-operator/src/malicious_bundle_producer.rs b/crates/subspace-malicious-operator/src/malicious_bundle_producer.rs index b76142fb1a..bc197707a2 100644 --- a/crates/subspace-malicious-operator/src/malicious_bundle_producer.rs +++ b/crates/subspace-malicious-operator/src/malicious_bundle_producer.rs @@ -28,7 +28,7 @@ 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_core_primitives::PotOutput; use subspace_runtime::{ CheckStorageAccess, DisablePallets, Runtime, RuntimeCall, SignedExtra, UncheckedExtrinsic, }; @@ -183,12 +183,12 @@ where .await } - pub async fn start + Send + 'static>( + 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 { + while let Some((slot, proof_of_time)) = new_slot_notification_stream.next().await { if let Some((operator_id, signing_key)) = self.malicious_operator_status.registered_operator() { @@ -197,7 +197,7 @@ where operator_id, OperatorSlotInfo { slot, - global_randomness, + proof_of_time, }, ) .await; diff --git a/crates/subspace-malicious-operator/src/malicious_domain_instance_starter.rs b/crates/subspace-malicious-operator/src/malicious_domain_instance_starter.rs index c18b7f1a25..af1bc237d3 100644 --- a/crates/subspace-malicious-operator/src/malicious_domain_instance_starter.rs +++ b/crates/subspace-malicious-operator/src/malicious_domain_instance_starter.rs @@ -105,7 +105,7 @@ where .then(|slot_notification| async move { ( slot_notification.new_slot_info.slot, - slot_notification.new_slot_info.global_randomness, + slot_notification.new_slot_info.proof_of_time, ) }) }; diff --git a/crates/subspace-node/src/commands/run/domain.rs b/crates/subspace-node/src/commands/run/domain.rs index cea0b5a6a7..bdf8389838 100644 --- a/crates/subspace-node/src/commands/run/domain.rs +++ b/crates/subspace-node/src/commands/run/domain.rs @@ -443,7 +443,7 @@ where .then(|slot_notification| async move { ( slot_notification.new_slot_info.slot, - slot_notification.new_slot_info.global_randomness, + slot_notification.new_slot_info.proof_of_time, ) }); diff --git a/crates/subspace-runtime/src/lib.rs b/crates/subspace-runtime/src/lib.rs index d65a8f5974..553ace5c15 100644 --- a/crates/subspace-runtime/src/lib.rs +++ b/crates/subspace-runtime/src/lib.rs @@ -593,6 +593,17 @@ const_assert!(MinOperatorStake::get() >= MinNominatorStake::get()); // Stake Withdrawal locking period must be >= Block tree pruning depth const_assert!(StakeWithdrawalLockingPeriod::get() >= BlockTreePruningDepth::get()); +pub struct BlockSlot; +impl pallet_domains::BlockSlot for BlockSlot { + fn current_slot() -> sp_consensus_slots::Slot { + Subspace::current_slot() + } + + fn future_slot() -> sp_consensus_slots::Slot { + Subspace::current_slot() + Slot::from(BlockAuthoringDelay::get()) + } +} + impl pallet_domains::Config for Runtime { type RuntimeEvent = RuntimeEvent; type DomainHash = DomainHash; @@ -622,6 +633,7 @@ impl pallet_domains::Config for Runtime { type SudoId = SudoId; type PalletId = DomainsPalletId; type StorageFee = TransactionFees; + type BlockSlot = BlockSlot; } parameter_types! { 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 dc74cde161..5a5338014c 100644 --- a/domains/client/domain-operator/src/bundle_producer_election_solver.rs +++ b/domains/client/domain-operator/src/bundle_producer_election_solver.rs @@ -13,7 +13,7 @@ use sp_runtime::traits::Block as BlockT; use sp_runtime::RuntimeAppPublic; use std::marker::PhantomData; use std::sync::Arc; -use subspace_core_primitives::Randomness; +use subspace_core_primitives::PotOutput; use subspace_runtime_primitives::Balance; use tracing::log; @@ -54,7 +54,7 @@ where consensus_block_hash: CBlock::Hash, domain_id: DomainId, operator_id: OperatorId, - global_randomness: Randomness, + proof_of_time: PotOutput, ) -> sp_blockchain::Result, OperatorPublicKey)>> { let BundleProducerElectionParams { total_domain_stake, @@ -69,7 +69,9 @@ where None => return Ok(None), }; - let global_challenge = global_randomness.derive_global_challenge(slot.into()); + let global_challenge = proof_of_time + .derive_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 @@ -97,7 +99,7 @@ where let proof_of_election = ProofOfElection { domain_id, slot_number: slot.into(), - global_randomness, + proof_of_time, vrf_signature, operator_id, consensus_block_hash, diff --git a/domains/client/domain-operator/src/domain_bundle_producer.rs b/domains/client/domain-operator/src/domain_bundle_producer.rs index 20b9b8b59a..81334b6f32 100644 --- a/domains/client/domain-operator/src/domain_bundle_producer.rs +++ b/domains/client/domain-operator/src/domain_bundle_producer.rs @@ -116,7 +116,7 @@ where ) -> sp_blockchain::Result>> { let OperatorSlotInfo { slot, - global_randomness, + proof_of_time, } = slot_info; let domain_best_number = self.client.info().best_number; @@ -148,7 +148,7 @@ where consensus_chain_best_hash, self.domain_id, operator_id, - global_randomness, + proof_of_time, )? { tracing::info!("📦 Claimed bundle at slot {slot}"); diff --git a/domains/client/domain-operator/src/domain_worker.rs b/domains/client/domain-operator/src/domain_worker.rs index d1678348fe..d18942c988 100644 --- a/domains/client/domain-operator/src/domain_worker.rs +++ b/domains/client/domain-operator/src/domain_worker.rs @@ -132,13 +132,13 @@ pub(super) async fn start_worker< // NOTE: this is only necessary for the test. biased; - Some((slot, global_randomness)) = new_slot_notification_stream.next() => { + Some((slot, proof_of_time)) = new_slot_notification_stream.next() => { let res = bundle_producer .produce_bundle( operator_id, OperatorSlotInfo { slot, - global_randomness, + proof_of_time, }, ) .instrument(span.clone()) diff --git a/domains/client/domain-operator/src/lib.rs b/domains/client/domain-operator/src/lib.rs index 9c7bf9fdce..13e9e9b106 100644 --- a/domains/client/domain-operator/src/lib.rs +++ b/domains/client/domain-operator/src/lib.rs @@ -96,7 +96,7 @@ use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor}; use sp_runtime::DigestItem; use std::marker::PhantomData; use std::sync::Arc; -use subspace_core_primitives::Randomness; +use subspace_core_primitives::PotOutput; use subspace_runtime_primitives::Balance; pub type ExecutionReceiptFor = ExecutionReceipt< @@ -138,7 +138,7 @@ pub struct OperatorStreams { pub _phantom: PhantomData, } -type NewSlotNotification = (Slot, Randomness); +type NewSlotNotification = (Slot, PotOutput); pub struct OperatorParams< Block, diff --git a/domains/client/domain-operator/src/tests.rs b/domains/client/domain-operator/src/tests.rs index f74730d91f..8699bcc0f0 100644 --- a/domains/client/domain-operator/src/tests.rs +++ b/domains/client/domain-operator/src/tests.rs @@ -3,7 +3,7 @@ use crate::domain_bundle_producer::DomainBundleProducer; use crate::domain_bundle_proposer::DomainBundleProposer; use crate::fraud_proof::{FraudProofGenerator, TraceDiffType}; use crate::tests::TxPoolError::InvalidTransaction as TxPoolInvalidTransaction; -use crate::utils::OperatorSlotInfo; +use crate::OperatorSlotInfo; use codec::{Decode, Encode}; use domain_runtime_primitives::{DomainCoreApi, Hash}; use domain_test_primitives::{OnchainStateApi, TimestampApi}; @@ -39,7 +39,7 @@ use sp_runtime::transaction_validity::InvalidTransaction; use sp_runtime::OpaqueExtrinsic; use sp_state_machine::backend::AsTrieBackend; use std::sync::Arc; -use subspace_core_primitives::Randomness; +use subspace_core_primitives::PotOutput; use subspace_runtime_primitives::opaque::Block as CBlock; use subspace_runtime_primitives::Balance; use subspace_test_service::{ @@ -329,7 +329,11 @@ async fn test_processing_empty_consensus_block() { let domain_genesis_hash = alice.client.info().best_hash; for _ in 0..10 { // Produce consensus block with no tx thus no bundle - ferdie.produce_block_with_extrinsics(vec![]).await.unwrap(); + let slot = ferdie.produce_slot(); + ferdie + .produce_block_with_slot_at(slot, ferdie.client.info().best_hash, Some(vec![])) + .await + .unwrap(); let consensus_best_hash = ferdie.client.info().best_hash; let consensus_best_number = ferdie.client.info().best_number; @@ -1175,7 +1179,7 @@ async fn test_invalid_state_transition_proof_creation_and_verification( .prune_tx_from_pool(&original_submit_bundle_tx) .await .unwrap(); - assert!(ferdie.get_bundle_from_tx_pool(slot.into()).is_none()); + assert!(ferdie.get_bundle_from_tx_pool(slot).is_none()); ferdie .submit_transaction(bad_submit_bundle_tx) @@ -1337,7 +1341,7 @@ async fn test_true_invalid_bundles_inherent_extrinsic_proof_creation_and_verific .prune_tx_from_pool(&original_submit_bundle_tx) .await .unwrap(); - assert!(ferdie.get_bundle_from_tx_pool(slot.into()).is_none()); + assert!(ferdie.get_bundle_from_tx_pool(slot).is_none()); ferdie .submit_transaction(bad_submit_bundle_tx) @@ -1374,7 +1378,7 @@ async fn test_true_invalid_bundles_inherent_extrinsic_proof_creation_and_verific .prune_tx_from_pool(&original_submit_bundle_tx) .await .unwrap(); - assert!(ferdie.get_bundle_from_tx_pool(slot.into()).is_none()); + assert!(ferdie.get_bundle_from_tx_pool(slot).is_none()); ferdie .submit_transaction(bad_submit_bundle_tx) @@ -1495,7 +1499,7 @@ async fn test_false_invalid_bundles_inherent_extrinsic_proof_creation_and_verifi .prune_tx_from_pool(&original_submit_bundle_tx) .await .unwrap(); - assert!(ferdie.get_bundle_from_tx_pool(slot.into()).is_none()); + assert!(ferdie.get_bundle_from_tx_pool(slot).is_none()); ferdie .submit_transaction(bad_submit_bundle_tx) @@ -1635,7 +1639,7 @@ async fn test_true_invalid_bundles_illegal_extrinsic_proof_creation_and_verifica .prune_tx_from_pool(&original_submit_bundle_tx) .await .unwrap(); - assert!(ferdie.get_bundle_from_tx_pool(slot.into()).is_none()); + assert!(ferdie.get_bundle_from_tx_pool(slot).is_none()); ferdie .submit_transaction(bad_submit_bundle_tx) @@ -1672,7 +1676,7 @@ async fn test_true_invalid_bundles_illegal_extrinsic_proof_creation_and_verifica .prune_tx_from_pool(&original_submit_bundle_tx) .await .unwrap(); - assert!(ferdie.get_bundle_from_tx_pool(slot.into()).is_none()); + assert!(ferdie.get_bundle_from_tx_pool(slot).is_none()); ferdie .submit_transaction(bad_submit_bundle_tx) @@ -1813,7 +1817,7 @@ async fn test_false_invalid_bundles_illegal_extrinsic_proof_creation_and_verific .prune_tx_from_pool(&original_submit_bundle_tx) .await .unwrap(); - assert!(ferdie.get_bundle_from_tx_pool(slot.into()).is_none()); + assert!(ferdie.get_bundle_from_tx_pool(slot).is_none()); ferdie .submit_transaction(bad_submit_bundle_tx) @@ -1920,7 +1924,7 @@ async fn test_invalid_block_fees_proof_creation() { .prune_tx_from_pool(&original_submit_bundle_tx) .await .unwrap(); - assert!(ferdie.get_bundle_from_tx_pool(slot.into()).is_none()); + assert!(ferdie.get_bundle_from_tx_pool(slot).is_none()); ferdie .submit_transaction(bad_submit_bundle_tx) @@ -2025,7 +2029,7 @@ async fn test_invalid_domain_block_hash_proof_creation() { .prune_tx_from_pool(&original_submit_bundle_tx) .await .unwrap(); - assert!(ferdie.get_bundle_from_tx_pool(slot.into()).is_none()); + assert!(ferdie.get_bundle_from_tx_pool(slot).is_none()); ferdie .submit_transaction(bad_submit_bundle_tx) @@ -2130,7 +2134,7 @@ async fn test_invalid_domain_extrinsics_root_proof_creation() { .prune_tx_from_pool(&original_submit_bundle_tx) .await .unwrap(); - assert!(ferdie.get_bundle_from_tx_pool(slot.into()).is_none()); + assert!(ferdie.get_bundle_from_tx_pool(slot).is_none()); ferdie .submit_transaction(bad_submit_bundle_tx) @@ -2213,7 +2217,7 @@ async fn test_bundle_equivocation_fraud_proof() { .prune_tx_from_pool(&original_submit_bundle_tx) .await .unwrap(); - assert!(ferdie.get_bundle_from_tx_pool(slot.into()).is_none()); + assert!(ferdie.get_bundle_from_tx_pool(slot).is_none()); ferdie .submit_transaction(original_submit_bundle_tx) @@ -2542,7 +2546,7 @@ async fn test_valid_bundle_proof_generation_and_verification() { .prune_tx_from_pool(&original_submit_bundle_tx) .await .unwrap(); - assert!(ferdie.get_bundle_from_tx_pool(slot.into()).is_none()); + assert!(ferdie.get_bundle_from_tx_pool(slot).is_none()); ferdie .submit_transaction(submit_bundle_tx_with_bad_receipt) .await @@ -2785,7 +2789,7 @@ async fn pallet_domains_unsigned_extrinsics_should_work() { } #[tokio::test(flavor = "multi_thread")] -async fn duplicated_and_stale_bundle_should_be_rejected() { +async fn duplicated_bundle_should_be_rejected() { let directory = TempDir::new().expect("Must be able to create temporary directory"); let mut builder = sc_cli::LoggerBuilder::new(""); @@ -2838,21 +2842,152 @@ async fn duplicated_and_stale_bundle_should_be_rejected() { } e => panic!("Unexpected error: {e}"), } +} - // Wait for `BlockTreePruningDepth + 1` blocks which is 16 + 1 in test - produce_blocks!(ferdie, alice, 17).await.unwrap(); +#[tokio::test(flavor = "multi_thread")] +async fn stale_and_in_future_bundle_should_be_rejected() { + let directory = TempDir::new().expect("Must be able to create temporary directory"); + + let mut builder = sc_cli::LoggerBuilder::new(""); + builder.with_colors(false); + let _ = builder.init(); + + let tokio_handle = tokio::runtime::Handle::current(); + + // Start Ferdie + let mut ferdie = MockConsensusNode::run( + tokio_handle.clone(), + Ferdie, + BasePath::new(directory.path().join("ferdie")), + ); + + // Run Alice (a evm domain authority node) + let alice = domain_test_service::DomainNodeBuilder::new( + tokio_handle.clone(), + Alice, + BasePath::new(directory.path().join("alice")), + ) + .build_evm_node(Role::Authority, GENESIS_DOMAIN_ID, &mut ferdie) + .await; - // Bundle is now rejected because its receipt is pruned. + produce_blocks!(ferdie, alice, 1).await.unwrap(); + let bundle_to_tx = |opaque_bundle| { + subspace_test_runtime::UncheckedExtrinsic::new_unsigned( + pallet_domains::Call::submit_bundle { opaque_bundle }.into(), + ) + .into() + }; + let slot_info = |slot, proof_of_time| OperatorSlotInfo { + slot, + proof_of_time, + }; + let operator_id = 0; + let mut bundle_producer = { + let domain_bundle_proposer = DomainBundleProposer::new( + GENESIS_DOMAIN_ID, + alice.client.clone(), + ferdie.client.clone(), + alice.operator.transaction_pool.clone(), + ); + let (bundle_sender, _bundle_receiver) = + sc_utils::mpsc::tracing_unbounded("domain_bundle_stream", 100); + DomainBundleProducer::new( + GENESIS_DOMAIN_ID, + ferdie.client.clone(), + alice.client.clone(), + domain_bundle_proposer, + Arc::new(bundle_sender), + alice.operator.keystore.clone(), + false, + ) + }; + + let (_, bundle1) = ferdie.produce_slot_and_wait_for_bundle_submission().await; + let (_, bundle2) = ferdie.produce_slot_and_wait_for_bundle_submission().await; + ferdie.clear_tx_pool().await.unwrap(); + + // Produce one block that only included `bundle1` + produce_block_with!( + ferdie.produce_block_with_extrinsics(vec![bundle_to_tx(bundle1.unwrap())]), + alice + ) + .await + .unwrap(); + + // `bundle2` will be rejected because its PoT is stale match ferdie - .submit_transaction(submit_bundle_tx) + .submit_transaction(bundle_to_tx(bundle2.unwrap())) .await .unwrap_err() { sc_transaction_pool::error::Error::Pool(TxPoolError::InvalidTransaction(invalid_tx)) => { - assert_eq!(invalid_tx, InvalidTransactionCode::ExecutionReceipt.into()) + assert_eq!(invalid_tx, InvalidTransactionCode::Bundle.into()) } e => panic!("Unexpected error: {e}"), } + + // Bundle with unknow PoT and in future slot will be rejected before entring the tx pool + let (valid_slot, valid_pot) = ferdie.produce_slot(); + let slot_in_future = u64::MAX.into(); + let unknow_pot = PotOutput::from( + <&[u8] as TryInto<[u8; 16]>>::try_into( + &subspace_runtime_primitives::Hash::random().to_fixed_bytes()[..16], + ) + .expect("slice with length of 16 must able convert into [u8; 16]; qed"), + ); + + let valid_bundle = bundle_producer + .produce_bundle(operator_id, slot_info(valid_slot, valid_pot)) + .await + .unwrap() + .unwrap(); + let bundle_with_unknow_pot = bundle_producer + .produce_bundle(operator_id, slot_info(valid_slot, unknow_pot)) + .await + .unwrap() + .unwrap(); + let bundle_with_slot_in_future = bundle_producer + .produce_bundle(operator_id, slot_info(slot_in_future, valid_pot)) + .await + .unwrap() + .unwrap(); + for bundle in [ + bundle_with_unknow_pot.clone(), + bundle_with_slot_in_future.clone(), + ] { + match ferdie + .submit_transaction(bundle_to_tx(bundle)) + .await + .unwrap_err() + { + sc_transaction_pool::error::Error::Pool(TxPoolError::InvalidTransaction( + invalid_tx, + )) => { + assert_eq!(invalid_tx, InvalidTransactionCode::Bundle.into()) + } + e => panic!("Unexpected error: {e}"), + } + } + ferdie + .submit_transaction(bundle_to_tx(valid_bundle)) + .await + .unwrap(); + produce_blocks!(ferdie, alice, 1).await.unwrap(); + + // Even try building consensus block with these invalid bundles, they will fail to pass + // the `pre_dispatch` check, and won't included in the consensus block + let pre_ferdie_best_number = ferdie.client.info().best_number; + let pre_alice_best_number = alice.client.info().best_number; + + ferdie + .produce_block_with_extrinsics(vec![ + bundle_to_tx(bundle_with_unknow_pot), + bundle_to_tx(bundle_with_slot_in_future), + ]) + .await + .unwrap(); + assert_eq!(ferdie.client.info().best_number, pre_ferdie_best_number + 1); + assert_eq!(alice.client.info().best_number, pre_alice_best_number); } #[tokio::test(flavor = "multi_thread")] @@ -2883,6 +3018,7 @@ async fn existing_bundle_can_be_resubmitted_to_new_fork() { produce_blocks!(ferdie, alice, 3).await.unwrap(); + let pre_alice_best_number = alice.client.info().best_number; let mut parent_hash = ferdie.client.info().best_hash; let (slot, bundle) = ferdie.produce_slot_and_wait_for_bundle_submission().await; @@ -2899,23 +3035,22 @@ async fn existing_bundle_can_be_resubmitted_to_new_fork() { .await .unwrap(); - // Create fork and build one more blocks on it to make it the new best fork + // Create fork that also contains the bundle and build one more blocks on it to make + // it the new best fork parent_hash = ferdie - .produce_block_with_slot_at(slot, parent_hash, Some(vec![])) + .produce_block_with_slot_at(slot, parent_hash, Some(vec![submit_bundle_tx])) .await .unwrap(); + let slot = ferdie.produce_slot(); ferdie - .produce_block_with_slot_at(slot + 1, parent_hash, Some(vec![])) + .produce_block_with_slot_at(slot, parent_hash, Some(vec![])) .await .unwrap(); - // Bundle can be successfully submitted to the new fork, or it is also possible - // that the `submit_bundle_tx` in the retracted block has been resubmitted to the - // tx pool in the background by the `txpool-notifications` worker. - match ferdie.submit_transaction(submit_bundle_tx).await { - Ok(_) | Err(sc_transaction_pool::error::Error::Pool(TxPoolError::AlreadyImported(_))) => {} - Err(err) => panic!("Unexpected error: {err}"), - } + assert_eq!(alice.client.info().best_number, pre_alice_best_number + 1); + + produce_blocks!(ferdie, alice, 1).await.unwrap(); + assert_eq!(alice.client.info().best_number, pre_alice_best_number + 2); } // TODO: Unlock test when multiple domains are supported in DecEx v2. @@ -3712,7 +3847,7 @@ async fn test_bad_receipt_chain() { .prune_tx_from_pool(&original_submit_bundle_tx) .await .unwrap(); - assert!(ferdie.get_bundle_from_tx_pool(slot.into()).is_none()); + assert!(ferdie.get_bundle_from_tx_pool(slot).is_none()); ferdie .submit_transaction(bad_submit_bundle_tx) .await @@ -3731,19 +3866,17 @@ async fn test_bad_receipt_chain() { // Produce a bundle with another bad ER that use previous bad ER as parent let parent_bad_receipt_hash = bad_receipt_hash; let slot = ferdie.produce_slot(); - let bundle = { - bundle_producer - .produce_bundle( - 0, - OperatorSlotInfo { - slot, - global_randomness: Randomness::from(Hash::random().to_fixed_bytes()), - }, - ) - .await - .expect("produce bundle must success") - .expect("must win the challenge") - }; + let bundle = bundle_producer + .produce_bundle( + 0, + OperatorSlotInfo { + slot: slot.0, + proof_of_time: slot.1, + }, + ) + .await + .expect("produce bundle must success") + .expect("must win the challenge"); let (bad_receipt_hash, bad_submit_bundle_tx) = { let mut opaque_bundle = bundle; let receipt = &mut opaque_bundle.sealed_header.header.receipt; diff --git a/domains/client/domain-operator/src/utils.rs b/domains/client/domain-operator/src/utils.rs index ab94c1a8bf..5293b79d4a 100644 --- a/domains/client/domain-operator/src/utils.rs +++ b/domains/client/domain-operator/src/utils.rs @@ -3,15 +3,15 @@ use sc_utils::mpsc::{TracingUnboundedReceiver, TracingUnboundedSender}; use sp_consensus_slots::Slot; use sp_runtime::traits::{Block as BlockT, NumberFor}; use std::sync::Arc; -use subspace_core_primitives::Randomness; +use subspace_core_primitives::PotOutput; /// Data required to produce bundles on executor node. #[derive(PartialEq, Clone, Debug)] pub struct OperatorSlotInfo { /// Slot pub slot: Slot, - /// Global randomness - pub global_randomness: Randomness, + /// The PoT output for `slot` + pub proof_of_time: PotOutput, } #[derive(Debug, Clone)] diff --git a/domains/service/src/domain.rs b/domains/service/src/domain.rs index 8970f5b7a5..53bacd5390 100644 --- a/domains/service/src/domain.rs +++ b/domains/service/src/domain.rs @@ -41,7 +41,7 @@ use std::fmt::{Debug, Display}; use std::marker::PhantomData; use std::str::FromStr; use std::sync::Arc; -use subspace_core_primitives::Randomness; +use subspace_core_primitives::PotOutput; use subspace_runtime_primitives::Nonce; use substrate_frame_rpc_system::AccountNonceApi; @@ -270,7 +270,7 @@ where + FraudProofApi, IBNS: Stream, mpsc::Sender<()>)> + Send + 'static, CIBNS: Stream> + Send + 'static, - NSNS: Stream + Send + 'static, + NSNS: Stream + Send + 'static, ASS: Stream> + Send + 'static, RuntimeApi: ConstructRuntimeApi> + Send + Sync + 'static, RuntimeApi::RuntimeApi: ApiExt diff --git a/test/subspace-test-client/src/lib.rs b/test/subspace-test-client/src/lib.rs index dcfcf773a4..b6e32a7b7e 100644 --- a/test/subspace-test-client/src/lib.rs +++ b/test/subspace-test-client/src/lib.rs @@ -159,7 +159,8 @@ async fn start_farming( { if u64::from(new_slot_info.slot) % 2 == 0 { let global_challenge = new_slot_info - .global_randomness + .proof_of_time + .derive_global_randomness() .derive_global_challenge(new_slot_info.slot.into()); let audit_result = audit_sector_sync( &public_key, diff --git a/test/subspace-test-runtime/src/lib.rs b/test/subspace-test-runtime/src/lib.rs index d8708b9719..bbc1181515 100644 --- a/test/subspace-test-runtime/src/lib.rs +++ b/test/subspace-test-runtime/src/lib.rs @@ -628,6 +628,17 @@ parameter_types! { // Minimum operator stake must be >= minimum nominator stake since operator is also a nominator. const_assert!(MinOperatorStake::get() >= MinNominatorStake::get()); +pub struct BlockSlot; +impl pallet_domains::BlockSlot for BlockSlot { + fn current_slot() -> sp_consensus_slots::Slot { + Subspace::current_slot() + } + + fn future_slot() -> sp_consensus_slots::Slot { + Subspace::current_slot() + Slot::from(BlockAuthoringDelay::get()) + } +} + impl pallet_domains::Config for Runtime { type RuntimeEvent = RuntimeEvent; type DomainHash = DomainHash; @@ -657,6 +668,7 @@ impl pallet_domains::Config for Runtime { type MinNominatorStake = MinNominatorStake; type PalletId = DomainsPalletId; type StorageFee = TransactionFees; + type BlockSlot = BlockSlot; } parameter_types! { diff --git a/test/subspace-test-service/src/lib.rs b/test/subspace-test-service/src/lib.rs index 73e5d0446a..e67f4160ca 100644 --- a/test/subspace-test-service/src/lib.rs +++ b/test/subspace-test-service/src/lib.rs @@ -51,8 +51,10 @@ use sp_application_crypto::UncheckedFrom; use sp_blockchain::HeaderBackend; use sp_consensus::{BlockOrigin, Error as ConsensusError}; use sp_consensus_slots::Slot; -use sp_consensus_subspace::digests::{CompatibleDigestItem, PreDigest, PreDigestPotInfo}; -use sp_consensus_subspace::FarmerPublicKey; +use sp_consensus_subspace::digests::{ + extract_pre_digest, CompatibleDigestItem, PreDigest, PreDigestPotInfo, +}; +use sp_consensus_subspace::{FarmerPublicKey, PotExtension}; use sp_core::traits::{CodeExecutor, SpawnEssentialNamed}; use sp_core::H256; use sp_domains::{BundleProducerElectionApi, DomainsApi, OpaqueBundle}; @@ -68,13 +70,14 @@ use sp_runtime::traits::{ use sp_runtime::{DigestItem, OpaqueExtrinsic}; use sp_subspace_mmr::host_functions::{SubspaceMmrExtension, SubspaceMmrHostFunctionsImpl}; use sp_timestamp::Timestamp; +use std::collections::HashMap; use std::error::Error; use std::marker::PhantomData; use std::pin::Pin; use std::sync::atomic::AtomicU32; use std::sync::Arc; use std::time; -use subspace_core_primitives::{Randomness, Solution}; +use subspace_core_primitives::{PotOutput, Solution}; use subspace_runtime_primitives::opaque::Block; use subspace_runtime_primitives::{AccountId, Balance, Hash}; use subspace_service::transaction_pool::FullPool; @@ -178,19 +181,38 @@ type StorageChanges = sp_api::StorageChanges; struct MockExtensionsFactory { consensus_client: Arc, executor: Arc, + mock_pot_verifier: Arc, _phantom: PhantomData, } impl MockExtensionsFactory { - fn new(consensus_client: Arc, executor: Arc) -> Self { + fn new( + consensus_client: Arc, + executor: Arc, + mock_pot_verifier: Arc, + ) -> Self { Self { consensus_client, executor, + mock_pot_verifier, _phantom: Default::default(), } } } +#[derive(Default)] +struct MockPotVerfier(Mutex>); + +impl MockPotVerfier { + fn is_valid(&self, slot: u64, pot: PotOutput) -> bool { + self.0.lock().get(&slot).map(|p| *p == pot).unwrap_or(false) + } + + fn inject_pot(&self, slot: u64, pot: PotOutput) { + self.0.lock().insert(slot, pot); + } +} + impl ExtensionsFactory for MockExtensionsFactory where @@ -217,9 +239,41 @@ where exts.register(SubspaceMmrExtension::new(Arc::new( SubspaceMmrHostFunctionsImpl::::new(self.consensus_client.clone()), ))); + exts.register(PotExtension::new({ + let client = Arc::clone(&self.consensus_client); + let mock_pot_verifier = Arc::clone(&self.mock_pot_verifier); + Box::new( + move |parent_hash, slot, proof_of_time, _quick_verification| { + let parent_hash = { + let mut converted_parent_hash = Block::Hash::default(); + converted_parent_hash.as_mut().copy_from_slice(&parent_hash); + converted_parent_hash + }; + + let parent_header = match client.header(parent_hash) { + Ok(Some(parent_header)) => parent_header, + _ => return false, + }; + let parent_pre_digest = match extract_pre_digest(&parent_header) { + Ok(parent_pre_digest) => parent_pre_digest, + _ => return false, + }; + + let parent_slot = parent_pre_digest.slot(); + if slot <= *parent_slot { + return false; + } + + mock_pot_verifier.is_valid(slot, proof_of_time) + }, + ) + })); exts } } + +type NewSlot = (Slot, PotOutput); + /// A mock Subspace consensus node instance used for testing. pub struct MockConsensusNode { /// `TaskManager`'s instance. @@ -246,14 +300,13 @@ pub struct MockConsensusNode { pub network_starter: Option, /// The next slot number next_slot: u64, + /// The mock pot verifier + mock_pot_verifier: Arc, /// The slot notification subscribers - #[allow(clippy::type_complexity)] - new_slot_notification_subscribers: Vec>, + new_slot_notification_subscribers: Vec>, /// The acknowledgement sender subscribers - #[allow(clippy::type_complexity)] acknowledgement_sender_subscribers: Vec>>, /// Block import pipeline - #[allow(clippy::type_complexity)] block_import: MockBlockImport, xdm_gossip_worker_builder: Option, /// Mock subspace solution used to mock the subspace `PreDigest` @@ -290,11 +343,13 @@ impl MockConsensusNode { .expect("Fail to new full parts"); let client = Arc::new(client); + let mock_pot_verifier = Arc::new(MockPotVerfier::default()); client .execution_extensions() .set_extensions_factory(MockExtensionsFactory::<_, DomainBlock, _>::new( client.clone(), Arc::new(executor.clone()), + Arc::clone(&mock_pot_verifier), )); let select_chain = sc_consensus::LongestChain::new(backend.clone()); @@ -368,6 +423,7 @@ impl MockConsensusNode { rpc_handlers, network_starter: Some(network_starter), next_slot: 1, + mock_pot_verifier, new_slot_notification_subscribers: Vec::new(), acknowledgement_sender_subscribers: Vec::new(), block_import, @@ -425,30 +481,35 @@ impl MockConsensusNode { } /// Produce a slot only, without waiting for the potential slot handlers. - pub fn produce_slot(&mut self) -> Slot { + pub fn produce_slot(&mut self) -> NewSlot { let slot = Slot::from(self.next_slot); + let proof_of_time = PotOutput::from( + <&[u8] as TryInto<[u8; 16]>>::try_into(&Hash::random().to_fixed_bytes()[..16]) + .expect("slice with length of 16 must able convert into [u8; 16]; qed"), + ); + self.mock_pot_verifier.inject_pot(*slot, proof_of_time); self.next_slot += 1; - slot + + (slot, proof_of_time) } /// Notify the executor about the new slot and wait for the bundle produced at this slot. pub async fn notify_new_slot_and_wait_for_bundle( &mut self, - slot: Slot, + new_slot: NewSlot, ) -> Option, Hash, DomainHeader, Balance>> { - let value = (slot, Randomness::from(Hash::random().to_fixed_bytes())); self.new_slot_notification_subscribers - .retain(|subscriber| subscriber.unbounded_send(value).is_ok()); + .retain(|subscriber| subscriber.unbounded_send(new_slot).is_ok()); self.confirm_acknowledgement().await; - self.get_bundle_from_tx_pool(slot.into()) + self.get_bundle_from_tx_pool(new_slot) } /// Produce a new slot and wait for a bundle produced at this slot. pub async fn produce_slot_and_wait_for_bundle_submission( &mut self, ) -> ( - Slot, + NewSlot, Option, Hash, DomainHeader, Balance>>, ) { let slot = self.produce_slot(); @@ -459,7 +520,7 @@ impl MockConsensusNode { } /// Subscribe the new slot notification - pub fn new_slot_notification_stream(&mut self) -> mpsc::UnboundedReceiver<(Slot, Randomness)> { + pub fn new_slot_notification_stream(&mut self) -> mpsc::UnboundedReceiver<(Slot, PotOutput)> { let (tx, rx) = mpsc::unbounded(); self.new_slot_notification_subscribers.push(tx); rx @@ -532,7 +593,7 @@ impl MockConsensusNode { /// Get the bundle that created at `slot` from the transaction pool pub fn get_bundle_from_tx_pool( &self, - slot: u64, + new_slot: NewSlot, ) -> Option, Hash, DomainHeader, Balance>> { for ready_tx in self.transaction_pool.ready() { let ext = UncheckedExtrinsic::decode(&mut ready_tx.data.encode().as_slice()) @@ -540,7 +601,7 @@ impl MockConsensusNode { if let RuntimeCall::Domains(pallet_domains::Call::submit_bundle { opaque_bundle }) = ext.function { - if opaque_bundle.sealed_header.slot_number() == slot { + if opaque_bundle.sealed_header.slot_number() == *new_slot.0 { return Some(opaque_bundle); } } @@ -698,7 +759,9 @@ impl MockConsensusNode { let inherent_txns = block_builder.create_inherents(inherent_data)?; for tx in inherent_txns.into_iter().chain(extrinsics) { - sc_block_builder::BlockBuilder::push(&mut block_builder, tx)?; + if let Err(err) = sc_block_builder::BlockBuilder::push(&mut block_builder, tx) { + tracing::error!("Invalid transaction while building block: {}", err); + } } let (block, storage_changes, _) = block_builder.build()?.into_inner(); @@ -740,7 +803,7 @@ impl MockConsensusNode { #[sc_tracing::logging::prefix_logs_with(self.log_prefix)] pub async fn produce_block_with_slot_at( &mut self, - slot: Slot, + new_slot: NewSlot, parent_hash: ::Hash, maybe_extrinsics: Option::Extrinsic>>, ) -> Result<::Hash, Box> { @@ -762,7 +825,9 @@ impl MockConsensusNode { .map(|t| self.transaction_pool.hash_of(t)) .collect(); - let (block, storage_changes) = self.build_block(slot, parent_hash, extrinsics).await?; + let (block, storage_changes) = self + .build_block(new_slot.0, parent_hash, extrinsics) + .await?; log_new_block(&block, block_timer.elapsed().as_millis()); @@ -782,7 +847,7 @@ impl MockConsensusNode { /// Produce a new block on top of the current best block, with the extrinsics collected from /// the transaction pool. #[sc_tracing::logging::prefix_logs_with(self.log_prefix)] - pub async fn produce_block_with_slot(&mut self, slot: Slot) -> Result<(), Box> { + pub async fn produce_block_with_slot(&mut self, slot: NewSlot) -> Result<(), Box> { self.produce_block_with_slot_at(slot, self.client.info().best_hash, None) .await?; Ok(())