diff --git a/Cargo.lock b/Cargo.lock index f1829f2946..fd18891966 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10811,6 +10811,7 @@ dependencies = [ "sp-std", "sp-transaction-pool", "sp-version", + "static_assertions", "subspace-core-primitives", "subspace-runtime-primitives", "subspace-verification", @@ -11015,12 +11016,14 @@ dependencies = [ "sp-consensus-subspace", "sp-core", "sp-domains", + "sp-externalities", "sp-inherents", "sp-keyring", "sp-runtime", "sp-timestamp", "subspace-core-primitives", "subspace-fraud-proof", + "subspace-node", "subspace-runtime-primitives", "subspace-service", "subspace-test-client", diff --git a/crates/pallet-domains/src/benchmarking.rs b/crates/pallet-domains/src/benchmarking.rs index 4c875a1260..8ed971bc45 100644 --- a/crates/pallet-domains/src/benchmarking.rs +++ b/crates/pallet-domains/src/benchmarking.rs @@ -22,11 +22,11 @@ mod benchmarks { /// - The receipts will prune a expired receipt #[benchmark] fn submit_system_bundle() { - let receipts_pruning_depth = T::ReceiptsPruningDepth::get().saturated_into::(); + let block_tree_pruning_depth = T::BlockTreePruningDepth::get().saturated_into::(); - // Import `ReceiptsPruningDepth` number of receipts which will be pruned later - run_to_block::(1, receipts_pruning_depth); - for i in 0..receipts_pruning_depth { + // Import `BlockTreePruningDepth` number of receipts which will be pruned later + run_to_block::(1, block_tree_pruning_depth); + for i in 0..block_tree_pruning_depth { let receipt = ExecutionReceipt::dummy(i.into(), block_hash_n::(i)); let bundle = create_dummy_bundle_with_receipts_generic( DOMAIN_ID, @@ -38,18 +38,18 @@ mod benchmarks { } assert_eq!( Domains::::head_receipt_number(), - (receipts_pruning_depth - 1).into() + (block_tree_pruning_depth - 1).into() ); // Construct a bundle that contains a new receipt - run_to_block::(receipts_pruning_depth + 1, receipts_pruning_depth + 2); + run_to_block::(block_tree_pruning_depth + 1, block_tree_pruning_depth + 2); let receipt = ExecutionReceipt::dummy( - receipts_pruning_depth.into(), - block_hash_n::(receipts_pruning_depth), + block_tree_pruning_depth.into(), + block_hash_n::(block_tree_pruning_depth), ); let bundle = create_dummy_bundle_with_receipts_generic( DOMAIN_ID, - (receipts_pruning_depth + 1).into(), + (block_tree_pruning_depth + 1).into(), Default::default(), receipt, ); @@ -59,7 +59,7 @@ mod benchmarks { assert_eq!( Domains::::head_receipt_number(), - receipts_pruning_depth.into() + block_tree_pruning_depth.into() ); assert_eq!(Domains::::oldest_receipt_number(), 1u32.into()); } @@ -82,11 +82,11 @@ mod benchmarks { /// - The fraud proof will revert the maximal possible number of receipts #[benchmark] fn submit_system_domain_invalid_state_transition_proof() { - let receipts_pruning_depth = T::ReceiptsPruningDepth::get().saturated_into::(); + let block_tree_pruning_depth = T::BlockTreePruningDepth::get().saturated_into::(); - // Import `ReceiptsPruningDepth` number of receipts which will be revert later - run_to_block::(1, receipts_pruning_depth); - for i in 0..receipts_pruning_depth { + // Import `BlockTreePruningDepth` number of receipts which will be revert later + run_to_block::(1, block_tree_pruning_depth); + for i in 0..block_tree_pruning_depth { let receipt = ExecutionReceipt::dummy(i.into(), block_hash_n::(i)); let bundle = create_dummy_bundle_with_receipts_generic( DOMAIN_ID, @@ -98,10 +98,10 @@ mod benchmarks { } assert_eq!( Domains::::head_receipt_number(), - (receipts_pruning_depth - 1).into() + (block_tree_pruning_depth - 1).into() ); - // Construct a fraud proof that will revert `ReceiptsPruningDepth` number of receipts + // Construct a fraud proof that will revert `BlockTreePruningDepth` number of receipts let proof: FraudProof = FraudProof::InvalidStateTransition(dummy_invalid_state_transition_proof(DOMAIN_ID, 0)); diff --git a/crates/pallet-domains/src/block_tree.rs b/crates/pallet-domains/src/block_tree.rs new file mode 100644 index 0000000000..39d8af3971 --- /dev/null +++ b/crates/pallet-domains/src/block_tree.rs @@ -0,0 +1,729 @@ +//! Domain block tree + +use crate::{ + BlockTree, Config, DomainBlocks, ExecutionInbox, ExecutionReceiptOf, HeadReceiptNumber, +}; +use codec::{Decode, Encode}; +use frame_support::{ensure, PalletError}; +use scale_info::TypeInfo; +use sp_core::Get; +use sp_domains::v2::ExecutionReceipt; +use sp_domains::{DomainId, OperatorId}; +use sp_runtime::traits::{CheckedSub, One, Saturating, Zero}; +use sp_std::cmp::Ordering; +use sp_std::vec::Vec; + +/// Block tree specific errors +#[derive(TypeInfo, Encode, Decode, PalletError, Debug, PartialEq)] +pub enum Error { + InvalidExtrinsicsRoots, + UnknownParentBlockReceipt, + BuiltOnUnknownConsensusBlock, + InFutureReceipt, + PrunedReceipt, + BadGenesisReceipt, + UnexpectedReceiptType, + MaxHeadDomainNumber, +} + +#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)] +pub struct DomainBlock { + /// The full ER for this block. + pub execution_receipt: ExecutionReceipt, + /// A set of all operators who have committed to this ER within a bundle. Used to determine who to + /// slash if a fraudulent branch of the `block_tree` is pruned. + /// + /// NOTE: there may be duplicated operator id as an operator can submit multiple bundles with the + /// same head receipt to a consensus block. + pub operator_ids: Vec, +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum AcceptedReceiptType { + // New head receipt that extend the longest branch + NewHead, + // Receipt that creates a new branch of the block tree + NewBranch, + // Receipt that comfirms the current head receipt + CurrentHead, +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum RejectedReceiptType { + // Receipt that is newer than the head receipt but does not extend the head receipt + InFuture, + // Receipt that already been pruned + Pruned, +} + +impl From for Error { + fn from(rejected_receipt: RejectedReceiptType) -> Error { + match rejected_receipt { + RejectedReceiptType::InFuture => Error::InFutureReceipt, + RejectedReceiptType::Pruned => Error::PrunedReceipt, + } + } +} + +/// The type of receipt regarding to its freshness +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum ReceiptType { + Accepted(AcceptedReceiptType), + Rejected(RejectedReceiptType), + // Receipt that comfirm a non-head receipt + Stale, +} + +/// Get the receipt type of the given receipt based on the current block tree state +pub(crate) fn execution_receipt_type( + domain_id: DomainId, + execution_receipt: &ExecutionReceiptOf, +) -> ReceiptType { + let receipt_number = execution_receipt.domain_block_number; + let head_receipt_number = HeadReceiptNumber::::get(domain_id); + + match receipt_number.cmp(&head_receipt_number.saturating_add(One::one())) { + Ordering::Greater => ReceiptType::Rejected(RejectedReceiptType::InFuture), + Ordering::Equal => ReceiptType::Accepted(AcceptedReceiptType::NewHead), + Ordering::Less => { + let oldest_receipt_number = + head_receipt_number.saturating_sub(T::BlockTreePruningDepth::get()); + let already_exist = + BlockTree::::get(domain_id, receipt_number).contains(&execution_receipt.hash()); + + if receipt_number < oldest_receipt_number { + // Receipt already pruned + ReceiptType::Rejected(RejectedReceiptType::Pruned) + } else if !already_exist { + // Create new branch + ReceiptType::Accepted(AcceptedReceiptType::NewBranch) + } else if receipt_number == head_receipt_number { + // Add comfirm to the current head receipt + ReceiptType::Accepted(AcceptedReceiptType::CurrentHead) + } else { + // Add comfirm to a non-head receipt + ReceiptType::Stale + } + } + } +} + +/// Verify the execution receipt +pub(crate) fn verify_execution_receipt( + domain_id: DomainId, + execution_receipt: &ExecutionReceiptOf, +) -> Result<(), Error> { + let ExecutionReceipt { + consensus_block_number, + consensus_block_hash, + domain_block_number, + block_extrinsics_roots, + parent_domain_block_receipt_hash, + .. + } = execution_receipt; + + if domain_block_number.is_zero() { + // The genesis receipt is generated and added to the block tree by the runtime upon domain + // instantiation, thus it is unchallengeable and must always be the same. + ensure!( + BlockTree::::get(domain_id, domain_block_number).contains(&execution_receipt.hash()), + Error::BadGenesisReceipt + ); + } else { + let execution_inbox = + ExecutionInbox::::get((domain_id, domain_block_number, consensus_block_number)); + ensure!( + !block_extrinsics_roots.is_empty() && *block_extrinsics_roots == execution_inbox, + Error::InvalidExtrinsicsRoots + ); + } + + let excepted_consensus_block_hash = + frame_system::Pallet::::block_hash(consensus_block_number); + ensure!( + *consensus_block_hash == excepted_consensus_block_hash, + Error::BuiltOnUnknownConsensusBlock + ); + + if let Some(parent_block_number) = domain_block_number.checked_sub(&One::one()) { + let parent_block_exist = BlockTree::::get(domain_id, parent_block_number) + .contains(parent_domain_block_receipt_hash); + ensure!(parent_block_exist, Error::UnknownParentBlockReceipt); + } + + match execution_receipt_type::(domain_id, execution_receipt) { + ReceiptType::Rejected(RejectedReceiptType::InFuture) => { + log::error!( + target: "runtime::domains", + "Unexpected in future receipt {execution_receipt:?}, which should result in \ + `UnknownParentBlockReceipt` error as it parent receipt is missing" + ); + Err(Error::InFutureReceipt) + } + ReceiptType::Rejected(RejectedReceiptType::Pruned) => { + log::error!( + target: "runtime::domains", + "Unexpected pruned receipt {execution_receipt:?}, which should result in \ + `InvalidExtrinsicsRoots` error as its `ExecutionInbox` is pruned at the same time" + ); + Err(Error::PrunedReceipt) + } + ReceiptType::Accepted(_) | ReceiptType::Stale => Ok(()), + } +} + +/// Process the execution receipt to add it to the block tree +pub(crate) fn process_execution_receipt( + domain_id: DomainId, + submitter: OperatorId, + execution_receipt: ExecutionReceiptOf, + receipt_type: AcceptedReceiptType, +) -> Result<(), Error> { + match receipt_type { + AcceptedReceiptType::NewBranch => { + add_new_receipt_to_block_tree::(domain_id, submitter, execution_receipt); + } + AcceptedReceiptType::NewHead => { + let domain_block_number = execution_receipt.domain_block_number; + + add_new_receipt_to_block_tree::(domain_id, submitter, execution_receipt); + + // Update the head receipt number + HeadReceiptNumber::::insert(domain_id, domain_block_number); + + // Prune expired domain block + if let Some(to_prune) = + domain_block_number.checked_sub(&T::BlockTreePruningDepth::get()) + { + for block in BlockTree::::take(domain_id, to_prune) { + DomainBlocks::::remove(block); + } + // Remove the block's `ExecutionInbox` as the block is pruned and does not need + // to verify its receipt's `extrinsics_root` anymore + let _ = ExecutionInbox::::clear_prefix((domain_id, to_prune), u32::MAX, None); + } + } + AcceptedReceiptType::CurrentHead => { + // Add confirmation to the current head receipt + let er_hash = execution_receipt.hash(); + DomainBlocks::::mutate(er_hash, |maybe_domain_block| { + let domain_block = maybe_domain_block.as_mut().expect( + "The domain block of `CurrentHead` receipt is checked to be exist in `execution_receipt_type`; qed" + ); + domain_block.operator_ids.push(submitter); + }); + } + } + Ok(()) +} + +fn add_new_receipt_to_block_tree( + domain_id: DomainId, + submitter: OperatorId, + execution_receipt: ExecutionReceiptOf, +) { + // Construct and add a new domain block to the block tree + let er_hash = execution_receipt.hash(); + let domain_block_number = execution_receipt.domain_block_number; + let domain_block = DomainBlock { + execution_receipt, + operator_ids: sp_std::vec![submitter], + }; + BlockTree::::mutate(domain_id, domain_block_number, |er_hashes| { + er_hashes.insert(er_hash); + }); + DomainBlocks::::insert(er_hash, domain_block); +} + +/// Import the genesis receipt to the block tree +pub(crate) fn import_genesis_receipt( + domain_id: DomainId, + genesis_receipt: ExecutionReceiptOf, +) { + let er_hash = genesis_receipt.hash(); + let domain_block_number = genesis_receipt.domain_block_number; + let domain_block = DomainBlock { + execution_receipt: genesis_receipt, + operator_ids: sp_std::vec![], + }; + // NOTE: no need to upate the head receipt number as we are using `ValueQuery` + BlockTree::::mutate(domain_id, domain_block_number, |er_hashes| { + er_hashes.insert(er_hash); + }); + DomainBlocks::::insert(er_hash, domain_block); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::domain_registry::DomainConfig; + use crate::tests::{ + create_dummy_bundle_with_receipts, create_dummy_receipt, GenesisStateRootGenerater, + ReadRuntimeVersion, Test, + }; + use crate::{BalanceOf, NextDomainId}; + use frame_support::dispatch::RawOrigin; + use frame_support::traits::{Currency, Hooks}; + use frame_support::weights::Weight; + use frame_support::{assert_err, assert_ok}; + use frame_system::Pallet as System; + use sp_core::H256; + use sp_domains::{GenesisReceiptExtension, RuntimeType}; + use sp_runtime::traits::BlockNumberProvider; + use sp_version::RuntimeVersion; + use std::sync::Arc; + + fn run_to_block(block_number: T::BlockNumber, parent_hash: T::Hash) { + System::::set_block_number(block_number); + System::::initialize(&block_number, &parent_hash, &Default::default()); + as Hooks>::on_initialize(block_number); + System::::finalize(); + } + + fn register_genesis_domain(creator: u64) -> DomainId { + assert_ok!(crate::Pallet::::register_domain_runtime( + RawOrigin::Root.into(), + b"evm".to_vec(), + RuntimeType::Evm, + vec![1, 2, 3, 4], + )); + + let domain_id = NextDomainId::::get(); + ::Currency::make_free_balance_be( + &creator, + ::DomainInstantiationDeposit::get() + + ::ExistentialDeposit::get(), + ); + crate::Pallet::::instantiate_domain( + RawOrigin::Signed(creator).into(), + DomainConfig { + domain_name: b"evm-domain".to_vec(), + runtime_id: 0, + max_block_size: 1u32, + max_block_weight: Weight::from_parts(1, 0), + bundle_slot_probability: (1, 1), + target_bundles_per_block: 1, + }, + ) + .unwrap(); + + domain_id + } + + // Submit new head receipt to extend the block tree + fn extend_block_tree(domain_id: DomainId, operator_id: u64, to: u64) { + let head_receipt_number = HeadReceiptNumber::::get(domain_id); + assert!(head_receipt_number < to); + + let head_node = get_block_tree_node_at::(domain_id, head_receipt_number).unwrap(); + let mut receipt = head_node.execution_receipt; + for block_number in (head_receipt_number + 1)..=to { + // Run to `block_number` + run_to_block::( + block_number, + frame_system::Pallet::::block_hash(block_number - 1), + ); + + // Submit a bundle with the receipt of the last block + let bundle_extrinsics_root = H256::random(); + let bundle = create_dummy_bundle_with_receipts( + domain_id, + block_number, + operator_id, + bundle_extrinsics_root, + receipt, + ); + assert_ok!(crate::Pallet::::submit_bundle_v2( + RawOrigin::None.into(), + bundle, + )); + + // Construct a `NewHead` receipt of the just submitted bundle, which will be included in the next bundle + let head_receipt_number = HeadReceiptNumber::::get(domain_id); + let parent_block_tree_node = + get_block_tree_node_at::(domain_id, head_receipt_number).unwrap(); + receipt = create_dummy_receipt( + block_number, + frame_system::Pallet::::block_hash(block_number), + parent_block_tree_node.execution_receipt.hash(), + vec![bundle_extrinsics_root], + ); + } + } + + #[allow(clippy::type_complexity)] + fn get_block_tree_node_at( + domain_id: DomainId, + block_number: T::DomainNumber, + ) -> Option>> + { + BlockTree::::get(domain_id, block_number) + .first() + .and_then(DomainBlocks::::get) + } + + fn new_test_ext() -> sp_io::TestExternalities { + let version = RuntimeVersion { + spec_name: "test".into(), + impl_name: Default::default(), + authoring_version: 0, + spec_version: 1, + impl_version: 1, + apis: Default::default(), + transaction_version: 1, + state_version: 0, + }; + + let mut ext = crate::tests::new_test_ext(); + ext.register_extension(sp_core::traits::ReadRuntimeVersionExt::new( + ReadRuntimeVersion(version.encode()), + )); + ext.register_extension(GenesisReceiptExtension::new(Arc::new( + GenesisStateRootGenerater, + ))); + + ext + } + + #[test] + fn test_genesis_receipt() { + let mut ext = new_test_ext(); + ext.execute_with(|| { + let domain_id = register_genesis_domain(0u64); + + // The genesis receipt should be added to the block tree + let block_tree_node_at_0 = BlockTree::::get(domain_id, 0); + assert_eq!(block_tree_node_at_0.len(), 1); + + let genesis_node = + DomainBlocks::::get(block_tree_node_at_0.first().unwrap()).unwrap(); + assert!(genesis_node.operator_ids.is_empty()); + assert_eq!(HeadReceiptNumber::::get(domain_id), 0); + + // The genesis receipt should be able pass the verification and is unchallengeable + let genesis_receipt = genesis_node.execution_receipt; + let invalid_genesis_receipt = { + let mut receipt = genesis_receipt.clone(); + receipt.final_state_root = H256::random(); + receipt + }; + assert_ok!(verify_execution_receipt::( + domain_id, + &genesis_receipt + )); + assert_err!( + verify_execution_receipt::(domain_id, &invalid_genesis_receipt), + Error::BadGenesisReceipt + ); + }); + } + + #[test] + fn test_new_head_receipt() { + let creator = 0u64; + let operator_id = 1u64; + let block_tree_pruning_depth = ::BlockTreePruningDepth::get() as u64; + + let mut ext = new_test_ext(); + ext.execute_with(|| { + let domain_id = register_genesis_domain(creator); + + // The genesis node of the block tree + let genesis_node = get_block_tree_node_at::(domain_id, 0).unwrap(); + let mut receipt = genesis_node.execution_receipt; + let mut receipt_of_block_1 = None; + for block_number in 1..=(block_tree_pruning_depth + 3) { + // Run to `block_number` + run_to_block::( + block_number, + frame_system::Pallet::::block_hash(block_number - 1), + ); + + // Submit a bundle with the receipt of the last block + let bundle_extrinsics_root = H256::random(); + let bundle = create_dummy_bundle_with_receipts( + domain_id, + block_number, + operator_id, + bundle_extrinsics_root, + receipt, + ); + assert_ok!(crate::Pallet::::submit_bundle_v2( + RawOrigin::None.into(), + bundle, + )); + // `bundle_extrinsics_root` should be tracked in `ExecutionInbox` + assert_eq!( + ExecutionInbox::::get((domain_id, block_number, block_number)), + vec![bundle_extrinsics_root] + ); + + // Head receipt number should be updated + let head_receipt_number = HeadReceiptNumber::::get(domain_id); + assert_eq!(head_receipt_number, block_number - 1); + + // As we only extending the block tree there should be no fork + let parent_block_tree_nodes = + BlockTree::::get(domain_id, head_receipt_number); + assert_eq!(parent_block_tree_nodes.len(), 1); + + // The submitter should be added to `operator_ids` + let parent_domain_block_receipt = parent_block_tree_nodes.first().unwrap(); + let parent_node = DomainBlocks::::get(parent_domain_block_receipt).unwrap(); + assert_eq!(parent_node.operator_ids.len(), 1); + assert_eq!(parent_node.operator_ids[0], operator_id); + + // Construct a `NewHead` receipt of the just submitted bundle, which will be included + // in the next bundle + receipt = create_dummy_receipt( + block_number, + frame_system::Pallet::::block_hash(block_number), + *parent_domain_block_receipt, + vec![bundle_extrinsics_root], + ); + assert_eq!( + execution_receipt_type::(domain_id, &receipt), + ReceiptType::Accepted(AcceptedReceiptType::NewHead) + ); + assert_ok!(verify_execution_receipt::(domain_id, &receipt)); + + // Record receipt of block #1 for later use + if block_number == 1 { + receipt_of_block_1.replace(receipt.clone()); + } + } + + // The receipt of the block 1 is pruned at the last iteration, verify it will result in + // `InvalidExtrinsicsRoots` error as `ExecutionInbox` of block 1 is pruned + let pruned_receipt = receipt_of_block_1.unwrap(); + assert!(BlockTree::::get(domain_id, 1).is_empty()); + assert!(ExecutionInbox::::get((domain_id, 1, 1)).is_empty()); + assert_eq!( + execution_receipt_type::(domain_id, &pruned_receipt), + ReceiptType::Rejected(RejectedReceiptType::Pruned) + ); + assert_err!( + verify_execution_receipt::(domain_id, &pruned_receipt), + Error::InvalidExtrinsicsRoots + ); + }); + } + + #[test] + fn test_confirm_head_receipt() { + let creator = 0u64; + let operator_id1 = 1u64; + let operator_id2 = 2u64; + let mut ext = new_test_ext(); + ext.execute_with(|| { + let domain_id = register_genesis_domain(creator); + extend_block_tree(domain_id, operator_id1, 3); + + let head_receipt_number = HeadReceiptNumber::::get(domain_id); + + // Get the head receipt + let current_head_receipt = + get_block_tree_node_at::(domain_id, head_receipt_number) + .unwrap() + .execution_receipt; + + // Receipt should be valid + assert_eq!( + execution_receipt_type::(domain_id, ¤t_head_receipt), + ReceiptType::Accepted(AcceptedReceiptType::CurrentHead) + ); + assert_ok!(verify_execution_receipt::( + domain_id, + ¤t_head_receipt + )); + + // Re-submit the receipt will add confirm to the head receipt + let bundle = create_dummy_bundle_with_receipts( + domain_id, + frame_system::Pallet::::current_block_number(), + operator_id2, + H256::random(), + current_head_receipt, + ); + assert_ok!(crate::Pallet::::submit_bundle_v2( + RawOrigin::None.into(), + bundle, + )); + let head_node = get_block_tree_node_at::(domain_id, head_receipt_number).unwrap(); + assert_eq!(head_node.operator_ids, vec![operator_id1, operator_id2]); + }); + } + + #[test] + fn test_stale_receipt() { + let creator = 0u64; + let operator_id1 = 1u64; + let operator_id2 = 2u64; + let mut ext = new_test_ext(); + ext.execute_with(|| { + let domain_id = register_genesis_domain(creator); + extend_block_tree(domain_id, operator_id1, 3); + + // Receipt that comfirm a non-head receipt is stale receipt + let head_receipt_number = HeadReceiptNumber::::get(domain_id); + let stale_receipt = get_block_tree_node_at::(domain_id, head_receipt_number - 1) + .unwrap() + .execution_receipt; + let stale_receipt_hash = stale_receipt.hash(); + + // Stale receipt can pass the verification + assert_eq!( + execution_receipt_type::(domain_id, &stale_receipt), + ReceiptType::Stale + ); + assert_ok!(verify_execution_receipt::(domain_id, &stale_receipt)); + + // Stale receipt can be submitted but won't be added to the block tree + let bundle = create_dummy_bundle_with_receipts( + domain_id, + frame_system::Pallet::::current_block_number(), + operator_id2, + H256::random(), + stale_receipt, + ); + assert_ok!(crate::Pallet::::submit_bundle_v2( + RawOrigin::None.into(), + bundle, + )); + + assert_eq!( + DomainBlocks::::get(stale_receipt_hash) + .unwrap() + .operator_ids, + vec![operator_id1] + ); + }); + } + + #[test] + fn test_new_branch_receipt() { + let creator = 0u64; + let operator_id1 = 1u64; + let operator_id2 = 2u64; + let mut ext = new_test_ext(); + ext.execute_with(|| { + let domain_id = register_genesis_domain(creator); + extend_block_tree(domain_id, operator_id1, 3); + + let head_receipt_number = HeadReceiptNumber::::get(domain_id); + assert_eq!( + BlockTree::::get(domain_id, head_receipt_number).len(), + 1 + ); + + // Construct new branch receipt that fork away from an existing node of + // the block tree + let new_branch_receipt = { + let mut head_receipt = + get_block_tree_node_at::(domain_id, head_receipt_number) + .unwrap() + .execution_receipt; + head_receipt.final_state_root = H256::random(); + head_receipt + }; + let new_branch_receipt_hash = new_branch_receipt.hash(); + + // New branch receipt can pass the verification + assert_eq!( + execution_receipt_type::(domain_id, &new_branch_receipt), + ReceiptType::Accepted(AcceptedReceiptType::NewBranch) + ); + assert_ok!(verify_execution_receipt::( + domain_id, + &new_branch_receipt + )); + + // Submit the new branch receipt will create fork in the block tree + let bundle = create_dummy_bundle_with_receipts( + domain_id, + frame_system::Pallet::::current_block_number(), + operator_id2, + H256::random(), + new_branch_receipt, + ); + assert_ok!(crate::Pallet::::submit_bundle_v2( + RawOrigin::None.into(), + bundle, + )); + + let nodes = BlockTree::::get(domain_id, head_receipt_number); + assert_eq!(nodes.len(), 2); + for n in nodes.iter() { + let block = DomainBlocks::::get(n).unwrap(); + if *n == new_branch_receipt_hash { + assert_eq!(block.operator_ids, vec![operator_id2]); + } else { + assert_eq!(block.operator_ids, vec![operator_id1]); + } + } + }); + } + + #[test] + fn test_invalid_receipt() { + let creator = 0u64; + let operator_id = 1u64; + let mut ext = new_test_ext(); + ext.execute_with(|| { + let domain_id = register_genesis_domain(creator); + extend_block_tree(domain_id, operator_id, 3); + + let head_receipt_number = HeadReceiptNumber::::get(domain_id); + let current_head_receipt = + get_block_tree_node_at::(domain_id, head_receipt_number) + .unwrap() + .execution_receipt; + + // In future receipt will result in `UnknownParentBlockReceipt` error as its parent + // receipt is missing from the block tree + let mut future_receipt = current_head_receipt.clone(); + future_receipt.domain_block_number = head_receipt_number + 2; + future_receipt.consensus_block_number = head_receipt_number + 2; + ExecutionInbox::::insert( + ( + domain_id, + future_receipt.domain_block_number, + future_receipt.consensus_block_number, + ), + future_receipt.block_extrinsics_roots.clone(), + ); + assert_eq!( + execution_receipt_type::(domain_id, &future_receipt), + ReceiptType::Rejected(RejectedReceiptType::InFuture) + ); + assert_err!( + verify_execution_receipt::(domain_id, &future_receipt), + Error::UnknownParentBlockReceipt + ); + + // Receipt with unknown extrinsics roots + let mut unknown_extrinsics_roots_receipt = current_head_receipt.clone(); + unknown_extrinsics_roots_receipt.block_extrinsics_roots = vec![H256::random()]; + assert_err!( + verify_execution_receipt::(domain_id, &unknown_extrinsics_roots_receipt), + Error::InvalidExtrinsicsRoots + ); + + // Receipt with unknown consensus block hash + let mut unknown_consensus_block_receipt = current_head_receipt.clone(); + unknown_consensus_block_receipt.consensus_block_hash = H256::random(); + assert_err!( + verify_execution_receipt::(domain_id, &unknown_consensus_block_receipt), + Error::BuiltOnUnknownConsensusBlock + ); + + // Receipt with unknown parent receipt + let mut unknown_parent_receipt = current_head_receipt; + unknown_parent_receipt.parent_domain_block_receipt_hash = H256::random(); + assert_err!( + verify_execution_receipt::(domain_id, &unknown_parent_receipt), + Error::UnknownParentBlockReceipt + ); + }); + } +} diff --git a/crates/pallet-domains/src/domain_registry.rs b/crates/pallet-domains/src/domain_registry.rs index 25a7121183..e1052deec8 100644 --- a/crates/pallet-domains/src/domain_registry.rs +++ b/crates/pallet-domains/src/domain_registry.rs @@ -1,8 +1,11 @@ //! Domain registry for domains +use crate::block_tree::import_genesis_receipt; use crate::pallet::DomainStakingSummary; use crate::staking::StakingSummary; -use crate::{Config, DomainRegistry, FreezeIdentifier, NextDomainId, RuntimeRegistry}; +use crate::{ + Config, DomainRegistry, ExecutionReceiptOf, FreezeIdentifier, NextDomainId, RuntimeRegistry, +}; use codec::{Decode, Encode}; use frame_support::traits::fungible::{Inspect, MutateFreeze}; use frame_support::traits::tokens::{Fortitude, Preservation}; @@ -10,7 +13,8 @@ use frame_support::weights::Weight; use frame_support::{ensure, PalletError}; use scale_info::TypeInfo; use sp_core::Get; -use sp_domains::{DomainId, GenesisDomain, RuntimeId}; +use sp_domains::domain::generate_genesis_state_root; +use sp_domains::{DomainId, GenesisDomain, ReceiptHash, RuntimeId, RuntimeType}; use sp_runtime::traits::{CheckedAdd, Zero}; use sp_std::collections::btree_map::BTreeMap; use sp_std::collections::btree_set::BTreeSet; @@ -28,6 +32,7 @@ pub enum Error { InsufficientFund, DomainNameTooLong, BalanceFreeze, + FailedToGenerateGenesisStateRoot, } #[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)] @@ -64,13 +69,13 @@ impl DomainConfig { } #[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)] -pub struct DomainObject { +pub struct DomainObject { /// The address of the domain creator, used to validate updating the domain config. pub owner_account_id: AccountId, /// The consensus chain block number when the domain first instantiated. pub created_at: Number, /// The hash of the genesis execution receipt for this domain. - pub genesis_receipt_hash: Hash, + pub genesis_receipt_hash: ReceiptHash, /// The domain config. pub domain_config: DomainConfig, } @@ -124,11 +129,16 @@ pub(crate) fn do_instantiate_domain( ) -> Result { can_instantiate_domain::(&owner_account_id, &domain_config)?; + let runtime_obj = RuntimeRegistry::::get(domain_config.runtime_id) + .expect("Runtime object must exist as checked in `can_instantiate_domain`; qed"); + let genesis_receipt = + initialize_genesis_receipt::(runtime_obj.runtime_type, runtime_obj.code)?; + let genesis_receipt_hash = genesis_receipt.hash(); + let domain_obj = DomainObject { owner_account_id: owner_account_id.clone(), created_at, - // TODO: drive the `genesis_receipt_hash` from genesis config through host function - genesis_receipt_hash: T::Hash::default(), + genesis_receipt_hash, domain_config, }; let domain_id = NextDomainId::::get(); @@ -155,21 +165,35 @@ pub(crate) fn do_instantiate_domain( }, ); - // TODO: initialize the genesis block in the domain block tree once we can drive the - // genesis ER from genesis config through host function + import_genesis_receipt::(domain_id, genesis_receipt); Ok(domain_id) } +fn initialize_genesis_receipt( + runtime_type: RuntimeType, + runtime_code: Vec, +) -> Result, Error> { + let consensus_genesis_hash = frame_system::Pallet::::block_hash(T::BlockNumber::zero()); + let genesis_state_root = generate_genesis_state_root(runtime_type, runtime_code) + .ok_or(Error::FailedToGenerateGenesisStateRoot)?; + Ok(ExecutionReceiptOf::::genesis( + consensus_genesis_hash, + genesis_state_root.into(), + )) +} + #[cfg(test)] mod tests { use super::*; use crate::pallet::{DomainRegistry, NextDomainId, RuntimeRegistry}; use crate::runtime_registry::RuntimeObject; - use crate::tests::{new_test_ext, Test}; + use crate::tests::{new_test_ext, GenesisStateRootGenerater, Test}; use frame_support::traits::Currency; + use sp_domains::GenesisReceiptExtension; use sp_runtime::traits::One; use sp_version::RuntimeVersion; + use std::sync::Arc; type Balances = pallet_balances::Pallet; @@ -188,6 +212,9 @@ mod tests { }; let mut ext = new_test_ext(); + ext.register_extension(GenesisReceiptExtension::new(Arc::new( + GenesisStateRootGenerater, + ))); ext.execute_with(|| { assert_eq!(NextDomainId::::get(), 0.into()); diff --git a/crates/pallet-domains/src/lib.rs b/crates/pallet-domains/src/lib.rs index 5193f38684..f8b6bbe156 100644 --- a/crates/pallet-domains/src/lib.rs +++ b/crates/pallet-domains/src/lib.rs @@ -24,12 +24,14 @@ mod benchmarking; #[cfg(test)] mod tests; +pub mod block_tree; pub mod domain_registry; pub mod runtime_registry; mod staking; mod staking_epoch; pub mod weights; +use crate::block_tree::verify_execution_receipt; use frame_support::traits::fungible::{Inspect, InspectFreeze}; use frame_support::traits::Get; use frame_system::offchain::SubmitTransaction; @@ -39,7 +41,7 @@ use sp_domains::bundle_producer_election::{is_below_threshold, BundleProducerEle use sp_domains::fraud_proof::FraudProof; use sp_domains::{DomainId, OpaqueBundle, OperatorId, OperatorPublicKey, RuntimeId}; use sp_runtime::traits::{BlockNumberProvider, CheckedSub, One, Zero}; -use sp_runtime::transaction_validity::TransactionValidityError; +use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidityError}; use sp_runtime::{RuntimeAppPublic, SaturatedConversion}; use sp_std::vec::Vec; use subspace_core_primitives::U256; @@ -57,11 +59,31 @@ pub trait FreezeIdentifier { fn domain_instantiation_id(domain_id: DomainId) -> FungibleFreezeId; } +pub type ExecutionReceiptOf = sp_domains::v2::ExecutionReceipt< + ::BlockNumber, + ::Hash, + ::DomainNumber, + ::DomainHash, + BalanceOf, +>; + +pub type OpaqueBundleOf = sp_domains::v2::OpaqueBundle< + ::BlockNumber, + ::Hash, + ::DomainNumber, + ::DomainHash, + BalanceOf, +>; + #[frame_support::pallet] mod pallet { // TODO: a complaint on `submit_bundle` call, revisit once new v2 features are complete. #![allow(clippy::large_enum_variant)] + use crate::block_tree::{ + execution_receipt_type, process_execution_receipt, DomainBlock, Error as BlockTreeError, + ReceiptType, + }; use crate::domain_registry::{ do_instantiate_domain, DomainConfig, DomainObject, Error as DomainRegistryError, }; @@ -79,7 +101,7 @@ mod pallet { do_finalize_domain_current_epoch, do_unlock_pending_withdrawals, PendingNominatorUnlock, }; use crate::weights::WeightInfo; - use crate::{BalanceOf, FreezeIdentifier, NominatorId}; + use crate::{BalanceOf, FreezeIdentifier, NominatorId, OpaqueBundleOf}; use codec::FullCodec; use frame_support::pallet_prelude::{StorageMap, *}; use frame_support::traits::fungible::{InspectFreeze, Mutate, MutateFreeze}; @@ -89,10 +111,13 @@ mod pallet { use sp_core::H256; use sp_domains::fraud_proof::FraudProof; use sp_domains::transaction::InvalidTransactionCode; - use sp_domains::{DomainId, GenesisDomain, OpaqueBundle, OperatorId, RuntimeId, RuntimeType}; + use sp_domains::{ + DomainId, ExtrinsicsRoot, GenesisDomain, OpaqueBundle, OperatorId, ReceiptHash, RuntimeId, + RuntimeType, + }; use sp_runtime::traits::{ - AtLeast32BitUnsigned, BlockNumberProvider, Bounded, CheckEqual, MaybeDisplay, SimpleBitOps, - Zero, + AtLeast32BitUnsigned, BlockNumberProvider, Bounded, CheckEqual, CheckedAdd, MaybeDisplay, + One, SimpleBitOps, Zero, }; use sp_runtime::SaturatedConversion; use sp_std::collections::btree_set::BTreeSet; @@ -135,7 +160,8 @@ mod pallet { + AsRef<[u8]> + AsMut<[u8]> + MaxEncodedLen - + Into; + + Into + + From; /// Same with `pallet_subspace::Config::ConfirmationDepthK`. type ConfirmationDepthK: Get; @@ -164,6 +190,17 @@ mod pallet { /// Identifier used for Freezing the funds used for staking. type FreezeIdentifier: FreezeIdentifier; + /// The block tree pruning depth, its value should <= `BlockHashCount` because we + /// need the consensus block hash to verify execution receipt, which is used to + /// construct the node of the block tree. + /// + /// TODO: `BlockTreePruningDepth` <= `BlockHashCount` is not enough to guarantee the consensus block + /// hash must exists while verifying receipt because the domain block is not mapping to the consensus + /// block one by one, we need to either store the consensus block hash in runtime manually or store + /// the consensus block hash in the client side and use host function to get them in runtime. + #[pallet::constant] + type BlockTreePruningDepth: Get; + /// The maximum block size limit for all domain. #[pallet::constant] type MaxDomainBlockSize: Get; @@ -209,7 +246,7 @@ mod pallet { /// Bundles submitted successfully in current block. #[pallet::storage] - pub(super) type SuccessfulBundles = StorageValue<_, Vec, ValueQuery>; + pub(super) type SuccessfulBundles = StorageMap<_, Identity, DomainId, Vec, ValueQuery>; /// Stores the next runtime id. #[pallet::storage] @@ -330,14 +367,66 @@ mod pallet { /// The domain registry #[pallet::storage] - pub(super) type DomainRegistry = StorageMap< + pub(super) type DomainRegistry = + StorageMap<_, Identity, DomainId, DomainObject, OptionQuery>; + + /// The domain block tree, map (`domain_id`, `domain_block_number`) to the hash of a domain blocks, + /// which can be used get the domain block in `DomainBlocks` + #[pallet::storage] + pub(super) type BlockTree = StorageDoubleMap< _, Identity, DomainId, - DomainObject, + Identity, + T::DomainNumber, + BTreeSet, + ValueQuery, + >; + + /// Mapping of domain block hash to domain block + #[pallet::storage] + pub(super) type DomainBlocks = StorageMap< + _, + Identity, + ReceiptHash, + DomainBlock>, OptionQuery, >; + /// The head receipt number of each domain + #[pallet::storage] + pub(super) type HeadReceiptNumber = + StorageMap<_, Identity, DomainId, T::DomainNumber, ValueQuery>; + + /// A set of `bundle_extrinsics_root` from all bundles that successfully submitted to the consensus + /// block, these extrinsics will be used to construct the domain block and `ExecutionInbox` is used + /// to ensure subsequent ERs of that domain block include all pre-validated extrinsic bundles. + #[pallet::storage] + pub type ExecutionInbox = StorageNMap< + _, + ( + NMapKey, + NMapKey, + NMapKey, + ), + Vec, + ValueQuery, + >; + + /// The block number of the best domain block, increase by one when the first bundle of the domain is + /// successfully submitted to current consensus block, which mean a new domain block with this block + /// number will be produce. Used as a pointer in `ExecutionInbox` to identify the current under building + /// domain block, also used as a mapping of consensus block number to domain block number. + #[pallet::storage] + pub(super) type HeadDomainNumber = + StorageMap<_, Identity, DomainId, T::DomainNumber, ValueQuery>; + + /// The genesis domian that scheduled to register at block #1, should be removed once + /// https://github.com/paritytech/substrate/issues/14541 is resolved. + #[pallet::storage] + type PendingGenesisDomain = + StorageValue<_, GenesisDomain, OptionQuery>; + #[derive(TypeInfo, Encode, Decode, PalletError, Debug, PartialEq)] pub enum BundleError { /// Can not find the operator for given operator id. @@ -355,30 +444,7 @@ mod pallet { /// The Bundle is created too long ago. StaleBundle, /// An invalid execution receipt found in the bundle. - Receipt(ExecutionReceiptError), - } - - impl From for Error { - #[inline] - fn from(e: BundleError) -> Self { - Self::Bundle(e) - } - } - - #[derive(TypeInfo, Encode, Decode, PalletError, Debug, PartialEq)] - pub enum ExecutionReceiptError { - /// The parent execution receipt is unknown. - MissingParent, - /// The execution receipt has been pruned. - Pruned, - /// The execution receipt points to a block unknown to the history. - UnknownBlock, - /// The execution receipt is too far in the future. - TooFarInFuture, - /// Receipts are not consecutive. - Inconsecutive, - /// Receipts in a bundle can not be empty. - Empty, + Receipt(BlockTreeError), } impl From for Error { @@ -399,10 +465,14 @@ mod pallet { } } + impl From for Error { + fn from(err: BlockTreeError) -> Self { + Error::BlockTree(err) + } + } + #[pallet::error] pub enum Error { - /// Invalid bundle. - Bundle(BundleError), /// Invalid fraud proof. FraudProof, /// Runtime registry specific errors @@ -411,6 +481,8 @@ mod pallet { Staking(StakingError), /// Domain registry specific errors DomainRegistry(DomainRegistryError), + /// Block tree specific errors + BlockTree(BlockTreeError), } #[pallet::event] @@ -484,6 +556,7 @@ mod pallet { #[pallet::call] impl Pallet { // TODO: proper weight + // TODO: replace it with `submit_bundle_v2` after all usage of it is removed #[allow(deprecated)] #[pallet::call_index(0)] #[pallet::weight(Weight::from_all(10_000))] @@ -501,7 +574,7 @@ mod pallet { let bundle_hash = opaque_bundle.hash(); - SuccessfulBundles::::append(bundle_hash); + SuccessfulBundles::::append(domain_id, bundle_hash); Self::note_domain_bundle(domain_id); @@ -514,6 +587,76 @@ mod pallet { Ok(()) } + #[pallet::call_index(10)] + #[pallet::weight(Weight::from_all(10_000))] + // TODO: proper benchmark + pub fn submit_bundle_v2( + origin: OriginFor, + opaque_bundle: OpaqueBundleOf, + ) -> DispatchResult { + ensure_none(origin)?; + + log::trace!(target: "runtime::domains", "Processing bundle: {opaque_bundle:?}"); + + let domain_id = opaque_bundle.domain_id(); + let bundle_hash = opaque_bundle.hash(); + let extrinsics_root = opaque_bundle.extrinsics_root(); + let operator_id = opaque_bundle.operator_id(); + let receipt = opaque_bundle.into_receipt(); + + match execution_receipt_type::(domain_id, &receipt) { + // The stale receipt should not be further processed, but we still track them for purposes + // of measuring the bundle production rate. + ReceiptType::Stale => { + Self::note_domain_bundle(domain_id); + return Ok(()); + } + ReceiptType::Rejected(rejected_receipt_type) => { + return Err(Error::::BlockTree(rejected_receipt_type.into()).into()) + } + // Add the exeuctione receipt to the block tree + ReceiptType::Accepted(accepted_receipt_type) => { + process_execution_receipt::( + domain_id, + operator_id, + receipt, + accepted_receipt_type, + ) + .map_err(Error::::from)?; + } + } + + // `SuccessfulBundles` is empty means this is the first accepted bundle for this domain in this + // consensus block, which also mean a domain block will be produced thus update `HeadDomainNumber` + // to this domain block's block number. + if SuccessfulBundles::::get(domain_id).is_empty() { + let next_number = HeadDomainNumber::::get(domain_id) + .checked_add(&One::one()) + .ok_or::>(BlockTreeError::MaxHeadDomainNumber.into())?; + HeadDomainNumber::::set(domain_id, next_number); + } + + // Put the `extrinsics_root` to the inbox of the current under building domain block + let head_domain_number = HeadDomainNumber::::get(domain_id); + let consensus_block_number = frame_system::Pallet::::current_block_number(); + ExecutionInbox::::append( + (domain_id, head_domain_number, consensus_block_number), + extrinsics_root, + ); + + SuccessfulBundles::::append(domain_id, bundle_hash); + + Self::note_domain_bundle(domain_id); + + Self::deposit_event(Event::BundleStored { + domain_id, + bundle_hash, + bundle_author: operator_id, + }); + + Ok(()) + } + #[pallet::call_index(1)] #[pallet::weight( match fraud_proof { @@ -718,38 +861,11 @@ mod pallet { #[pallet::genesis_build] impl GenesisBuild for GenesisConfig { fn build(&self) { + // Delay the genesis domain register to block #1 due to the required `GenesisReceiptExtension` is not + // registered during genesis storage build, remove once https://github.com/paritytech/substrate/issues/14541 + // is resolved. if let Some(genesis_domain) = &self.genesis_domain { - // Register the genesis domain runtime - let runtime_id = register_runtime_at_genesis::( - genesis_domain.runtime_name.clone(), - genesis_domain.runtime_type.clone(), - genesis_domain.runtime_version.clone(), - genesis_domain.code.clone(), - Zero::zero(), - ) - .expect("Genesis runtime registration must always succeed"); - - // Instantiate the genesis domain - let domain_config = DomainConfig::from_genesis::(genesis_domain, runtime_id); - let domain_owner = genesis_domain.owner_account_id.clone(); - let domain_id = - do_instantiate_domain::(domain_config, domain_owner.clone(), Zero::zero()) - .expect("Genesis domain instantiation must always succeed"); - - // Register domain_owner as the genesis operator. - let operator_config = OperatorConfig { - signing_key: genesis_domain.signing_key.clone(), - minimum_nominator_stake: genesis_domain - .minimum_nominator_stake - .saturated_into(), - nomination_tax: genesis_domain.nomination_tax, - }; - let operator_stake = T::MinOperatorStake::get(); - do_register_operator::(domain_owner, domain_id, operator_stake, operator_config) - .expect("Genesis operator registration must succeed"); - - do_finalize_domain_current_epoch::(domain_id, Zero::zero(), Zero::zero()) - .expect("Genesis epoch must succeed"); + PendingGenesisDomain::::set(Some(genesis_domain.clone())); } } } @@ -758,13 +874,57 @@ mod pallet { // TODO: proper benchmark impl Hooks for Pallet { fn on_initialize(block_number: T::BlockNumber) -> Weight { - SuccessfulBundles::::kill(); + if block_number.is_one() { + if let Some(ref genesis_domain) = PendingGenesisDomain::::take() { + // Register the genesis domain runtime + let runtime_id = register_runtime_at_genesis::( + genesis_domain.runtime_name.clone(), + genesis_domain.runtime_type.clone(), + genesis_domain.runtime_version.clone(), + genesis_domain.code.clone(), + Zero::zero(), + ) + .expect("Genesis runtime registration must always succeed"); + + // Instantiate the genesis domain + let domain_config = DomainConfig::from_genesis::(genesis_domain, runtime_id); + let domain_owner = genesis_domain.owner_account_id.clone(); + let domain_id = do_instantiate_domain::( + domain_config, + domain_owner.clone(), + Zero::zero(), + ) + .expect("Genesis domain instantiation must always succeed"); + + // Register domain_owner as the genesis operator. + let operator_config = OperatorConfig { + signing_key: genesis_domain.signing_key.clone(), + minimum_nominator_stake: genesis_domain + .minimum_nominator_stake + .saturated_into(), + nomination_tax: genesis_domain.nomination_tax, + }; + let operator_stake = T::MinOperatorStake::get(); + do_register_operator::( + domain_owner, + domain_id, + operator_stake, + operator_config, + ) + .expect("Genesis operator registration must succeed"); + + do_finalize_domain_current_epoch::(domain_id, Zero::zero(), Zero::zero()) + .expect("Genesis epoch must succeed"); + } + } do_upgrade_runtimes::(block_number); do_unlock_pending_withdrawals::(block_number) .expect("Pending unlocks should not fail due to checks at epoch"); + let _ = SuccessfulBundles::::clear(u32::MAX, None); + Weight::zero() } @@ -789,7 +949,8 @@ mod pallet { type Call = Call; fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> { match call { - Call::submit_bundle { opaque_bundle } => { + Call::submit_bundle { opaque_bundle: _ } => Ok(()), + Call::submit_bundle_v2 { opaque_bundle } => { Self::pre_dispatch_submit_bundle(opaque_bundle) } Call::submit_fraud_proof { fraud_proof: _ } => Ok(()), @@ -800,6 +961,27 @@ mod pallet { fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { match call { Call::submit_bundle { opaque_bundle } => { + let bundle_create_at = + opaque_bundle.sealed_header.header.consensus_block_number; + let current_block_number = frame_system::Pallet::::current_block_number(); + if let Err(e) = Self::check_stale_bundle(current_block_number, bundle_create_at) + { + log::debug!( + target: "runtime::domains", + "Bad bundle {:?}, error: {e:?}", opaque_bundle.domain_id(), + ); + return InvalidTransactionCode::Bundle.into(); + } + ValidTransaction::with_tag_prefix("SubspaceSubmitBundle") + .priority(TransactionPriority::MAX) + .longevity(T::ConfirmationDepthK::get().try_into().unwrap_or_else(|_| { + panic!("Block number always fits in TransactionLongevity; qed") + })) + .and_provides(opaque_bundle.hash()) + .propagate(true) + .build() + } + Call::submit_bundle_v2 { opaque_bundle } => { if let Err(e) = Self::validate_bundle(opaque_bundle) { log::debug!( target: "runtime::domains", @@ -835,8 +1017,16 @@ mod pallet { } impl Pallet { - pub fn successful_bundles() -> Vec { - SuccessfulBundles::::get() + pub fn successful_bundles(domain_id: DomainId) -> Vec { + SuccessfulBundles::::get(domain_id) + } + + pub fn successful_bundles_of_all_domains() -> Vec { + let mut res = Vec::new(); + for mut bundles in SuccessfulBundles::::iter_values() { + res.append(&mut bundles); + } + res } pub fn domain_runtime_code(domain_id: DomainId) -> Option> { @@ -883,64 +1073,77 @@ impl Pallet { } fn pre_dispatch_submit_bundle( - _opaque_bundle: &OpaqueBundle, + opaque_bundle: &OpaqueBundleOf, ) -> Result<(), TransactionValidityError> { - // TODO: Validate domain block tree - Ok(()) - } - - fn validate_bundle( - OpaqueBundle { - sealed_header, - receipt: _, - extrinsics: _, - }: &OpaqueBundle, - ) -> Result<(), BundleError> { - let operator_id = sealed_header.header.proof_of_election.operator_id; - - let operator = Operators::::get(operator_id).ok_or(BundleError::InvalidOperatorId)?; + let domain_id = opaque_bundle.domain_id(); + let receipt = &opaque_bundle.sealed_header.header.receipt; - if !operator - .signing_key - .verify(&sealed_header.pre_hash(), &sealed_header.signature) - { - return Err(BundleError::BadBundleSignature); - } - - let header = &sealed_header.header; + // TODO: Implement bundle validation. - let current_block_number = frame_system::Pallet::::current_block_number(); + verify_execution_receipt::(domain_id, receipt) + .map_err(|_| InvalidTransaction::Call.into()) + } - // Reject the stale bundles so that they can't be used by attacker to occupy the block space without cost. + // Check if a bundle is stale + fn check_stale_bundle( + current_block_number: T::BlockNumber, + bundle_create_at: T::BlockNumber, + ) -> Result<(), BundleError> { let confirmation_depth_k = T::ConfirmationDepthK::get(); if let Some(finalized) = current_block_number.checked_sub(&confirmation_depth_k) { { - // Ideally, `bundle.header.primary_number` is `current_block_number - 1`, we need + // Ideally, `bundle_create_at` is `current_block_number - 1`, we need // to handle the edge case that `T::ConfirmationDepthK` happens to be 1. let is_stale_bundle = if confirmation_depth_k.is_zero() { unreachable!( "ConfirmationDepthK is guaranteed to be non-zero at genesis config" ) } else if confirmation_depth_k == One::one() { - header.consensus_block_number < finalized + bundle_create_at < finalized } else { - header.consensus_block_number <= finalized + bundle_create_at <= finalized }; if is_stale_bundle { log::debug!( target: "runtime::domains", "Bundle created on an ancient consensus block, current_block_number: {current_block_number:?}, \ - ConfirmationDepthK: {confirmation_depth_k:?}, `bundle.header.primary_number`: {:?}, `finalized`: {finalized:?}", - header.consensus_block_number, + ConfirmationDepthK: {confirmation_depth_k:?}, `bundle_create_at`: {:?}, `finalized`: {finalized:?}", + bundle_create_at, ); return Err(BundleError::StaleBundle); } } } + Ok(()) + } + + fn validate_bundle(opaque_bundle: &OpaqueBundleOf) -> Result<(), BundleError> { + let sealed_header = &opaque_bundle.sealed_header; + let operator_id = sealed_header.header.proof_of_election.operator_id; + + let operator = Operators::::get(operator_id).ok_or(BundleError::InvalidOperatorId)?; + + if !operator + .signing_key + .verify(&sealed_header.pre_hash(), &sealed_header.signature) + { + return Err(BundleError::BadBundleSignature); + } + + let domain_id = opaque_bundle.domain_id(); + let receipt = &sealed_header.header.receipt; + let bundle_create_at = sealed_header.header.consensus_block_number; + + let current_block_number = frame_system::Pallet::::current_block_number(); + + // Reject the stale bundles so that they can't be used by attacker to occupy the block space without cost. + Self::check_stale_bundle(current_block_number, bundle_create_at)?; // TODO: Implement bundle validation. + verify_execution_receipt::(domain_id, receipt).map_err(BundleError::Receipt)?; + // TODO: The current staking distribution may be unusable when there is an epoch // transition, track the last stake distribution in that case. let proof_of_election = &sealed_header.header.proof_of_election; @@ -949,8 +1152,6 @@ impl Pallet { .verify_vrf_signature(&operator.signing_key) .map_err(|_| BundleError::BadVrfSignature)?; - let domain_id = proof_of_election.domain_id; - let domain_stake_summary = DomainStakingSummary::::get(domain_id).ok_or(BundleError::InvalidDomainId)?; diff --git a/crates/pallet-domains/src/runtime_registry.rs b/crates/pallet-domains/src/runtime_registry.rs index d579bb627c..99eda3a4fc 100644 --- a/crates/pallet-domains/src/runtime_registry.rs +++ b/crates/pallet-domains/src/runtime_registry.rs @@ -179,7 +179,9 @@ pub(crate) fn do_upgrade_runtimes(at: T::BlockNumber) { mod tests { use crate::pallet::{NextRuntimeId, RuntimeRegistry, ScheduledRuntimeUpgrades}; use crate::runtime_registry::{Error as RuntimeRegistryError, RuntimeObject}; - use crate::tests::{new_test_ext, DomainRuntimeUpgradeDelay, Domains, System, Test}; + use crate::tests::{ + new_test_ext, DomainRuntimeUpgradeDelay, Domains, ReadRuntimeVersion, System, Test, + }; use crate::Error; use codec::Encode; use frame_support::assert_ok; @@ -190,18 +192,6 @@ mod tests { use sp_runtime::{Digest, DispatchError}; use sp_version::RuntimeVersion; - struct ReadRuntimeVersion(Vec); - - impl sp_core::traits::ReadRuntimeVersion for ReadRuntimeVersion { - fn read_runtime_version( - &self, - _wasm_code: &[u8], - _ext: &mut dyn sp_externalities::Externalities, - ) -> Result, String> { - Ok(self.0.clone()) - } - } - #[test] fn create_domain_runtime() { let version = RuntimeVersion { diff --git a/crates/pallet-domains/src/tests.rs b/crates/pallet-domains/src/tests.rs index 919c5d3a5a..6089433664 100644 --- a/crates/pallet-domains/src/tests.rs +++ b/crates/pallet-domains/src/tests.rs @@ -6,9 +6,10 @@ use frame_support::weights::Weight; use scale_info::TypeInfo; use sp_core::crypto::Pair; use sp_core::{Get, H256, U256}; +use sp_domains::v2::{BundleHeader, ExecutionReceipt, OpaqueBundle, SealedBundleHeader}; use sp_domains::{ - create_dummy_bundle_with_receipts_generic, BundleHeader, DomainId, DomainsFreezeIdentifier, - ExecutionReceipt, OpaqueBundle, OperatorId, OperatorPair, ProofOfElection, SealedBundleHeader, + DomainId, DomainsFreezeIdentifier, GenerateGenesisStateRoot, OperatorId, OperatorPair, + ProofOfElection, RuntimeType, }; use sp_runtime::testing::Header; use sp_runtime::traits::{BlakeTwo256, IdentityLookup}; @@ -23,6 +24,9 @@ type Balance = u128; // TODO: Remove when DomainRegistry is usable. const DOMAIN_ID: DomainId = DomainId::new(0); +// Operator id used for testing +const OPERATOR_ID: OperatorId = 0u64; + frame_support::construct_runtime!( pub struct Test where @@ -67,7 +71,6 @@ impl frame_system::Config for Test { } parameter_types! { - pub const ReceiptsPruningDepth: BlockNumber = 256; pub const MaximumReceiptDrift: BlockNumber = 128; pub const InitialDomainTxRange: u64 = 10; pub const DomainTxRangeAdjustmentInterval: u64 = 100; @@ -77,6 +80,7 @@ parameter_types! { pub const MaxDomainBlockWeight: Weight = Weight::from_parts(1024 * 1024, 0); pub const DomainInstantiationDeposit: Balance = 100; pub const MaxDomainNameLength: u32 = 16; + pub const BlockTreePruningDepth: u32 = 256; } static CONFIRMATION_DEPTH_K: AtomicU64 = AtomicU64::new(10); @@ -161,6 +165,7 @@ impl pallet_domains::Config for Test { type DomainInstantiationDeposit = DomainInstantiationDeposit; type MaxDomainNameLength = MaxDomainNameLength; type Share = Balance; + type BlockTreePruningDepth = BlockTreePruningDepth; type StakeWithdrawalLockingPeriod = StakeWithdrawalLockingPeriod; type StakeEpochDuration = StakeEpochDuration; } @@ -173,62 +178,98 @@ pub(crate) fn new_test_ext() -> sp_io::TestExternalities { t.into() } -fn create_dummy_receipt( - consensus_block_number: BlockNumber, +pub(crate) fn create_dummy_receipt( + block_number: BlockNumber, consensus_block_hash: Hash, -) -> ExecutionReceipt { + parent_domain_block_receipt_hash: H256, + block_extrinsics_roots: Vec, +) -> ExecutionReceipt { ExecutionReceipt { - consensus_block_number, + domain_block_number: block_number, + parent_domain_block_receipt_hash, + consensus_block_number: block_number, consensus_block_hash, - domain_block_number: consensus_block_number, - domain_hash: H256::random(), - trace: if consensus_block_number == 0 { - Vec::new() - } else { - vec![H256::random(), H256::random()] - }, - trace_root: Default::default(), + block_extrinsics_roots, + final_state_root: Default::default(), + execution_trace_root: Default::default(), + total_rewards: 0, } } fn create_dummy_bundle( domain_id: DomainId, - consensus_block_number: BlockNumber, + block_number: BlockNumber, consensus_block_hash: Hash, -) -> OpaqueBundle { - let pair = OperatorPair::from_seed(&U256::from(0u32).into()); +) -> OpaqueBundle { + let execution_receipt = create_dummy_receipt( + block_number, + consensus_block_hash, + Default::default(), + vec![], + ); + create_dummy_bundle_with_receipts( + domain_id, + block_number, + OPERATOR_ID, + Default::default(), + execution_receipt, + ) +} - let execution_receipt = create_dummy_receipt(consensus_block_number, consensus_block_hash); +pub(crate) fn create_dummy_bundle_with_receipts( + domain_id: DomainId, + block_number: BlockNumber, + operator_id: OperatorId, + bundle_extrinsics_root: H256, + receipt: ExecutionReceipt, +) -> OpaqueBundle { + let pair = OperatorPair::from_seed(&U256::from(0u32).into()); let header = BundleHeader { - consensus_block_number, - consensus_block_hash, - extrinsics_root: Default::default(), + operator_id, + consensus_block_number: block_number, proof_of_election: ProofOfElection::dummy(domain_id, 0u64), + receipt, + bundle_size: 0u32, + estimated_bundle_weight: Default::default(), + bundle_extrinsics_root, }; let signature = pair.sign(header.hash().as_ref()); OpaqueBundle { sealed_header: SealedBundleHeader::new(header, signature), - receipt: execution_receipt, extrinsics: Vec::new(), + execution_trace: if block_number == 0 { + Vec::new() + } else { + vec![H256::random(), H256::random()] + }, } } -#[allow(dead_code)] -fn create_dummy_bundle_with_receipts( - domain_id: DomainId, - consensus_block_number: BlockNumber, - consensus_block_hash: Hash, - receipt: ExecutionReceipt, -) -> OpaqueBundle { - create_dummy_bundle_with_receipts_generic::( - domain_id, - consensus_block_number, - consensus_block_hash, - receipt, - ) +pub(crate) struct GenesisStateRootGenerater; + +impl GenerateGenesisStateRoot for GenesisStateRootGenerater { + fn generate_genesis_state_root( + &self, + _runtime_type: RuntimeType, + _runtime_code: Vec, + ) -> Option { + Some(Default::default()) + } +} + +pub(crate) struct ReadRuntimeVersion(pub Vec); + +impl sp_core::traits::ReadRuntimeVersion for ReadRuntimeVersion { + fn read_runtime_version( + &self, + _wasm_code: &[u8], + _ext: &mut dyn sp_externalities::Externalities, + ) -> Result, String> { + Ok(self.0.clone()) + } } // TODO: Unblock once bundle producer election v2 is finished. diff --git a/crates/sp-domains/src/lib.rs b/crates/sp-domains/src/lib.rs index 4f1c27461a..5ef44372fc 100644 --- a/crates/sp-domains/src/lib.rs +++ b/crates/sp-domains/src/lib.rs @@ -21,6 +21,7 @@ pub mod bundle_producer_election; pub mod fraud_proof; pub mod merkle_tree; pub mod transaction; +pub mod v2; use bundle_producer_election::{BundleProducerElectionParams, VrfProofError}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; @@ -76,6 +77,12 @@ impl sp_runtime::BoundToRuntimeAppPublic for OperatorKey { /// Derived from the Balance and can't be smaller than u128. pub type StakeWeight = u128; +/// The hash of a execution receipt. +pub type ReceiptHash = H256; + +/// The Merkle root of all extrinsics included in a bundle. +pub type ExtrinsicsRoot = H256; + /// Unique identifier of a domain. #[derive( Clone, @@ -419,7 +426,7 @@ where } } -#[derive(Serialize, Deserialize)] +#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct GenesisDomain { // Domain runtime items pub runtime_name: Vec, @@ -505,7 +512,7 @@ pub trait GenerateGenesisStateRoot: Send + Sync { fn generate_genesis_state_root( &self, runtime_type: RuntimeType, - raw_runtime_genesis_config: Vec, + runtime_code: Vec, ) -> Option; } @@ -529,13 +536,13 @@ pub trait Domain { fn generate_genesis_state_root( &mut self, runtime_type: RuntimeType, - raw_runtime_genesis_config: Vec, + runtime_code: Vec, ) -> Option { use sp_externalities::ExternalitiesExt; self.extension::() .expect("No `GenesisReceiptExtension` associated for the current context!") - .generate_genesis_state_root(runtime_type, raw_runtime_genesis_config) + .generate_genesis_state_root(runtime_type, runtime_code) } } @@ -547,6 +554,7 @@ sp_api::decl_runtime_apis! { /// Extract the bundles stored successfully from the given extrinsics. fn extract_successful_bundles( + domain_id: DomainId, extrinsics: Vec, ) -> OpaqueBundles; diff --git a/crates/sp-domains/src/v2.rs b/crates/sp-domains/src/v2.rs new file mode 100644 index 0000000000..cf52ae05e6 --- /dev/null +++ b/crates/sp-domains/src/v2.rs @@ -0,0 +1,177 @@ +// Domain primitives for the v2 architecture +// TODO: the v1 primitives can be removed and replaced by them after the domain client side +// retired all of the v1 usage. + +use crate::{ + DomainId, ExtrinsicsRoot, OperatorId, OperatorSignature, ProofOfElection, ReceiptHash, +}; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_api::HashT; +use sp_core::H256; +use sp_runtime::traits::{BlakeTwo256, Zero}; +use sp_runtime::OpaqueExtrinsic; +use sp_std::vec::Vec; +use sp_weights::Weight; + +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] +pub struct BundleHeader { + /// The operator id of the bundle author. + pub operator_id: OperatorId, + /// The consensus chain's best block number when the bundle is created. Used for detect stale + /// bundle and prevent attacker from reusing them to occupy the block space without cost. + pub consensus_block_number: Number, + /// Proof of bundle producer election. + pub proof_of_election: ProofOfElection, + /// Execution receipt that should extend the receipt chain or add confirmations + /// to the head receipt. + pub receipt: ExecutionReceipt, + /// The size of the bundle body in bytes. Used to calculate the storage cost. + pub bundle_size: u32, + /// The total (estimated) weight of all extrinsics in the bundle. Used to prevent overloading + /// the bundle with compute. + pub estimated_bundle_weight: Weight, + /// The Merkle root of all new extrinsics included in this bundle. + pub bundle_extrinsics_root: ExtrinsicsRoot, +} + +impl + BundleHeader +{ + /// Returns the hash of this header. + pub fn hash(&self) -> H256 { + BlakeTwo256::hash_of(self) + } +} + +/// Header of bundle. +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] +pub struct SealedBundleHeader { + /// Unsealed header. + pub header: BundleHeader, + /// Signature of the bundle. + pub signature: OperatorSignature, +} + +impl + SealedBundleHeader +{ + /// Constructs a new instance of [`SealedBundleHeader`]. + pub fn new( + header: BundleHeader, + signature: OperatorSignature, + ) -> Self { + Self { header, signature } + } + + /// Returns the hash of the inner unsealed header. + pub fn pre_hash(&self) -> H256 { + self.header.hash() + } + + /// Returns the hash of this header. + pub fn hash(&self) -> H256 { + BlakeTwo256::hash_of(self) + } +} + +/// Domain bundle. +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] +pub struct Bundle { + /// Sealed bundle header. + pub sealed_header: SealedBundleHeader, + /// List of storage roots collected during the domain block execution. + pub execution_trace: Vec, + /// The accompanying extrinsics. + pub extrinsics: Vec, +} + +impl< + Extrinsic: Encode, + Number: Encode, + Hash: Encode, + DomainNumber: Encode, + DomainHash: Encode, + Balance: Encode, + > Bundle +{ + /// Returns the hash of this bundle. + pub fn hash(&self) -> H256 { + BlakeTwo256::hash_of(self) + } + + /// Returns the domain_id of this bundle. + pub fn domain_id(&self) -> DomainId { + self.sealed_header.header.proof_of_election.domain_id + } + + // Return the `bundle_extrinsics_root` + pub fn extrinsics_root(&self) -> ExtrinsicsRoot { + self.sealed_header.header.bundle_extrinsics_root + } + + // Return the `operator_id` + pub fn operator_id(&self) -> OperatorId { + self.sealed_header.header.operator_id + } + + /// Consumes [`Bundle`] to extract the execution receipt. + pub fn into_receipt(self) -> ExecutionReceipt { + self.sealed_header.header.receipt + } +} + +/// Bundle with opaque extrinsics. +pub type OpaqueBundle = + Bundle; + +/// Receipt of a domain block execution. +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] +pub struct ExecutionReceipt { + // The index of the current domain block that forms the basis of this ER. + pub domain_block_number: DomainNumber, + // A pointer to the hash of the ER for the last domain block. + pub parent_domain_block_receipt_hash: ReceiptHash, + // A pointer to the consensus block index which contains all of the bundles that were used to derive and + // order all extrinsics executed by the current domain block for this ER. + pub consensus_block_number: Number, + // The block hash correspond to `consensus_block_number`. + pub consensus_block_hash: Hash, + // All `extrinsics_roots` for all bundles being executed by this block. Used to ensure these are contained + // within the state of the `execution_inbox`. + pub block_extrinsics_roots: Vec, + // The final state root for the current domain block reflected by this ER. Used for verifying storage proofs + // for domains. + pub final_state_root: DomainHash, + // The Merkle root of the execution trace for the current domain block. Used for verifying fraud proofs. + pub execution_trace_root: H256, + // All SSC rewards for this ER to be shared across operators. + pub total_rewards: Balance, +} + +impl< + Number: Encode + Zero, + Hash: Encode + Default, + DomainNumber: Encode + Zero, + DomainHash: Encode, + Balance: Encode + Zero, + > ExecutionReceipt +{ + /// Returns the hash of this execution receipt. + pub fn hash(&self) -> ReceiptHash { + BlakeTwo256::hash_of(self) + } + + pub fn genesis(consensus_genesis_hash: Hash, genesis_state_root: DomainHash) -> Self { + ExecutionReceipt { + domain_block_number: Zero::zero(), + parent_domain_block_receipt_hash: Default::default(), + consensus_block_hash: consensus_genesis_hash, + consensus_block_number: Zero::zero(), + block_extrinsics_roots: sp_std::vec![], + final_state_root: genesis_state_root, + execution_trace_root: Default::default(), + total_rewards: Zero::zero(), + } + } +} diff --git a/crates/subspace-node/src/domain.rs b/crates/subspace-node/src/domain.rs index 0827be7fbf..470d15a838 100644 --- a/crates/subspace-node/src/domain.rs +++ b/crates/subspace-node/src/domain.rs @@ -83,16 +83,14 @@ where pub fn generate_genesis_block( &self, runtime_type: RuntimeType, - raw_runtime_genesis_config: Vec, + runtime_code: Vec, ) -> sp_blockchain::Result { let domain_genesis_block_builder = match runtime_type { RuntimeType::Evm => { - let runtime_genesis_config: evm_domain_runtime::RuntimeGenesisConfig = - serde_json::from_slice(&raw_runtime_genesis_config) - .map_err(|err| sp_blockchain::Error::Application(Box::new(err)))?; - + let mut runtime_cfg = evm_domain_runtime::RuntimeGenesisConfig::default(); + runtime_cfg.system.code = runtime_code; GenesisBlockBuilder::new( - &runtime_genesis_config, + &runtime_cfg, false, self.backend.clone(), self.executor.clone(), @@ -115,9 +113,9 @@ where fn generate_genesis_state_root( &self, runtime_type: RuntimeType, - raw_runtime_genesis_config: Vec, + runtime_code: Vec, ) -> Option { - self.generate_genesis_block(runtime_type, raw_runtime_genesis_config) + self.generate_genesis_block(runtime_type, runtime_code) .map(|genesis_block| *genesis_block.header().state_root()) .ok() .map(Into::into) diff --git a/crates/subspace-runtime/Cargo.toml b/crates/subspace-runtime/Cargo.toml index 1cee32bb37..4d0afa829d 100644 --- a/crates/subspace-runtime/Cargo.toml +++ b/crates/subspace-runtime/Cargo.toml @@ -55,6 +55,7 @@ sp-session = { version = "4.0.0-dev", default-features = false, git = "https://g sp-std = { version = "8.0.0", default-features = false, git = "https://github.com/subspace/substrate", rev = "55c157cff49b638a59d81a9f971f0f9a66829c71" } sp-transaction-pool = { version = "4.0.0-dev", default-features = false, git = "https://github.com/subspace/substrate", rev = "55c157cff49b638a59d81a9f971f0f9a66829c71" } sp-version = { version = "22.0.0", default-features = false, git = "https://github.com/subspace/substrate", rev = "55c157cff49b638a59d81a9f971f0f9a66829c71" } +static_assertions = "1.1.0" subspace-core-primitives = { version = "0.1.0", default-features = false, path = "../subspace-core-primitives" } subspace-runtime-primitives = { version = "0.1.0", default-features = false, path = "../subspace-runtime-primitives" } subspace-verification = { version = "0.1.0", default-features = false, path = "../subspace-verification" } diff --git a/crates/subspace-runtime/src/domains.rs b/crates/subspace-runtime/src/domains.rs index 8fb5c22064..4fbfdeff90 100644 --- a/crates/subspace-runtime/src/domains.rs +++ b/crates/subspace-runtime/src/domains.rs @@ -11,14 +11,16 @@ use subspace_core_primitives::Randomness; use subspace_verification::derive_randomness; pub(crate) fn extract_successful_bundles( + domain_id: DomainId, extrinsics: Vec, ) -> sp_domains::OpaqueBundles { - let successful_bundles = Domains::successful_bundles(); + let successful_bundles = Domains::successful_bundles(domain_id); extrinsics .into_iter() .filter_map(|uxt| match uxt.function { RuntimeCall::Domains(pallet_domains::Call::submit_bundle { opaque_bundle }) - if successful_bundles.contains(&opaque_bundle.hash()) => + if opaque_bundle.domain_id() == domain_id + && successful_bundles.contains(&opaque_bundle.hash()) => { Some(opaque_bundle) } @@ -33,7 +35,7 @@ pub(crate) fn extract_receipts( extrinsics: Vec, domain_id: DomainId, ) -> Vec> { - let successful_bundles = Domains::successful_bundles(); + let successful_bundles = Domains::successful_bundles(domain_id); extrinsics .into_iter() .filter_map(|uxt| match uxt.function { diff --git a/crates/subspace-runtime/src/lib.rs b/crates/subspace-runtime/src/lib.rs index 5a7d02a60f..4796b94458 100644 --- a/crates/subspace-runtime/src/lib.rs +++ b/crates/subspace-runtime/src/lib.rs @@ -70,6 +70,7 @@ use sp_std::prelude::*; #[cfg(feature = "std")] use sp_version::NativeVersion; use sp_version::RuntimeVersion; +use static_assertions::const_assert; use subspace_core_primitives::crypto::Scalar; use subspace_core_primitives::objects::BlockObjectMapping; use subspace_core_primitives::{ @@ -449,7 +450,6 @@ impl pallet_offences_subspace::Config for Runtime { } parameter_types! { - pub const ReceiptsPruningDepth: BlockNumber = 256; pub const MaximumReceiptDrift: BlockNumber = 128; pub const InitialDomainTxRange: u64 = INITIAL_DOMAIN_TX_RANGE; pub const DomainTxRangeAdjustmentInterval: u64 = TX_RANGE_ADJUSTMENT_INTERVAL_BLOCKS; @@ -464,11 +464,16 @@ parameter_types! { pub const MaxBundlesPerBlock: u32 = 10; pub const DomainInstantiationDeposit: Balance = 100 * SSC; pub const MaxDomainNameLength: u32 = 32; + pub const BlockTreePruningDepth: u32 = 256; // TODO: revisit these pub const StakeWithdrawalLockingPeriod: BlockNumber = 100; pub const StakeEpochDuration: DomainNumber = 5; } +// `BlockTreePruningDepth` should <= `BlockHashCount` because we need the consensus block hash to verify +// execution receipt, which is used to construct the node of the block tree. +const_assert!(BlockTreePruningDepth::get() <= BlockHashCount::get()); + impl pallet_domains::Config for Runtime { type RuntimeEvent = RuntimeEvent; type DomainNumber = DomainNumber; @@ -487,6 +492,7 @@ impl pallet_domains::Config for Runtime { type DomainInstantiationDeposit = DomainInstantiationDeposit; type MaxDomainNameLength = MaxDomainNameLength; type Share = Balance; + type BlockTreePruningDepth = BlockTreePruningDepth; type StakeWithdrawalLockingPeriod = StakeWithdrawalLockingPeriod; type StakeEpochDuration = StakeEpochDuration; } @@ -824,13 +830,14 @@ impl_runtime_apis! { } fn extract_successful_bundles( + domain_id: DomainId, extrinsics: Vec<::Extrinsic>, ) -> sp_domains::OpaqueBundles { - crate::domains::extract_successful_bundles(extrinsics) + crate::domains::extract_successful_bundles(domain_id, extrinsics) } fn successful_bundle_hashes() -> Vec { - Domains::successful_bundles() + Domains::successful_bundles_of_all_domains() } fn extrinsics_shuffling_seed(header: ::Header) -> Randomness { diff --git a/domains/client/block-preprocessor/src/lib.rs b/domains/client/block-preprocessor/src/lib.rs index 7d963625c7..66254dd15b 100644 --- a/domains/client/block-preprocessor/src/lib.rs +++ b/domains/client/block-preprocessor/src/lib.rs @@ -294,7 +294,7 @@ where let bundles = self .consensus_client .runtime_api() - .extract_successful_bundles(consensus_block_hash, primary_extrinsics)?; + .extract_successful_bundles(consensus_block_hash, self.domain_id, primary_extrinsics)?; if bundles.is_empty() && maybe_new_runtime.is_none() { return Ok(None); diff --git a/domains/client/domain-operator/src/tests.rs b/domains/client/domain-operator/src/tests.rs index 74c7cafdc4..a70e6e782d 100644 --- a/domains/client/domain-operator/src/tests.rs +++ b/domains/client/domain-operator/src/tests.rs @@ -50,6 +50,8 @@ async fn test_domain_block_production() { Ferdie, BasePath::new(directory.path().join("ferdie")), ); + // Produce 1 consensus block to initialize genesis domain + ferdie.produce_block_with_slot(1.into()).await.unwrap(); // Run Alice (a evm domain authority node) let alice = domain_test_service::DomainNodeBuilder::new( @@ -76,7 +78,7 @@ async fn test_domain_block_production() { .unwrap(); } // Domain block only produced when there is bundle contains in the primary block - assert_eq!(ferdie.client.info().best_number, 50); + assert_eq!(ferdie.client.info().best_number, 51); assert_eq!(alice.client.info().best_number, 25); let consensus_block_hash = ferdie.client.info().best_hash; @@ -144,6 +146,11 @@ async fn collected_receipts_should_be_on_the_same_branch_with_current_best_block Ferdie, BasePath::new(directory.path().join("ferdie")), ); + // Produce 1 consensus block to initialize genesis domain + consensus_node + .produce_block_with_slot(1.into()) + .await + .unwrap(); // Run Alice (a evm domain authority node) let alice = domain_test_service::DomainNodeBuilder::new( @@ -160,7 +167,7 @@ async fn collected_receipts_should_be_on_the_same_branch_with_current_best_block let best_consensus_hash = consensus_node.client.info().best_hash; let best_consensus_number = consensus_node.client.info().best_number; - assert_eq!(best_consensus_number, 3); + assert_eq!(best_consensus_number, 4); assert_eq!(alice.client.info().best_number, 3); let domain_block_2_hash = *alice @@ -178,25 +185,32 @@ async fn collected_receipts_should_be_on_the_same_branch_with_current_best_block let parent_hash = *best_header.parent_hash(); - // Consensus chain forks: + // Domain chain forks: // 3 // / // 2 -- 3a // \ // 3b -- 4 + + // Consensus chain forks (it has 1 more block compare to the domain chain): + // 4 + // / + // 3 -- 4a + // \ + // 4b -- 5 let slot = consensus_node.produce_slot(); - let fork_block_hash_3a = consensus_node + let fork_block_hash_4a = consensus_node .produce_block_with_slot_at(slot, parent_hash, Some(vec![])) .await - .expect("Produced first consensus fork block 3a at height #3"); - // A fork block 3a at #3 produced. - assert_eq!(number_of(&consensus_node, fork_block_hash_3a), 3); - assert_ne!(fork_block_hash_3a, best_consensus_hash); + .expect("Produced first consensus fork block 4a at height #4"); + // A fork block 4a at #4 produced. + assert_eq!(number_of(&consensus_node, fork_block_hash_4a), 4); + assert_ne!(fork_block_hash_4a, best_consensus_hash); // Best hash unchanged due to the longest chain fork choice. assert_eq!(consensus_node.client.info().best_hash, best_consensus_hash); - // Hash of block number #3 unchanged. + // Hash of block number #4 unchanged. assert_eq!( - consensus_node.client.hash(3).unwrap().unwrap(), + consensus_node.client.hash(4).unwrap().unwrap(), best_consensus_hash ); @@ -210,7 +224,7 @@ async fn collected_receipts_should_be_on_the_same_branch_with_current_best_block ) }; - // Produce a bundle after the fork block #3a has been produced. + // Produce a bundle after the fork block #4a has been produced. let signed_bundle = consensus_node .notify_new_slot_and_wait_for_bundle(slot) .await; @@ -229,18 +243,18 @@ async fn collected_receipts_should_be_on_the_same_branch_with_current_best_block ); let slot = consensus_node.produce_slot(); - let fork_block_hash_3b = consensus_node + let fork_block_hash_4b = consensus_node .produce_block_with_slot_at(slot, parent_hash, Some(vec![])) .await .expect("Produced second consensus fork block 3b at height #3"); - // Another fork block 3b at #3 produced, - assert_eq!(number_of(&consensus_node, fork_block_hash_3b), 3); - assert_ne!(fork_block_hash_3b, best_consensus_hash); + // Another fork block 3b at #4 produced, + assert_eq!(number_of(&consensus_node, fork_block_hash_4b), 4); + assert_ne!(fork_block_hash_4b, best_consensus_hash); // Best hash unchanged due to the longest chain fork choice. assert_eq!(consensus_node.client.info().best_hash, best_consensus_hash); - // Hash of block number #3 unchanged. + // Hash of block number #4 unchanged. assert_eq!( - consensus_node.client.hash(3).unwrap().unwrap(), + consensus_node.client.hash(4).unwrap().unwrap(), best_consensus_hash ); @@ -254,27 +268,27 @@ async fn collected_receipts_should_be_on_the_same_branch_with_current_best_block expected_receipts_consensus_info ); - // Produce a new tip at #4. + // Produce a new tip at #5. let slot = consensus_node.produce_slot(); produce_block_with!( - consensus_node.produce_block_with_slot_at(slot, fork_block_hash_3b, Some(vec![])), + consensus_node.produce_block_with_slot_at(slot, fork_block_hash_4b, Some(vec![])), alice ) .await - .expect("Produce a new block on top of the second fork block at height #3"); + .expect("Produce a new block on top of the second fork block at height #4"); let new_best_hash = consensus_node.client.info().best_hash; let new_best_number = consensus_node.client.info().best_number; - assert_eq!(new_best_number, 4); + assert_eq!(new_best_number, 5); - // The domain best block should be reverted to #2 because the primary block #3b and #4 do + // The domain best block should be reverted to #2 because the primary block #4b and #5 do // not contains any bundles assert_eq!(alice.client.info().best_number, 2); assert_eq!(alice.client.info().best_hash, domain_block_2_hash); - // Hash of block number #3 is updated to the second fork block 3b. + // Hash of block number #4 is updated to the second fork block 4b. assert_eq!( - consensus_node.client.hash(3).unwrap().unwrap(), - fork_block_hash_3b + consensus_node.client.hash(4).unwrap().unwrap(), + fork_block_hash_4b ); let new_best_header = consensus_node @@ -283,20 +297,20 @@ async fn collected_receipts_should_be_on_the_same_branch_with_current_best_block .unwrap() .unwrap(); - assert_eq!(*new_best_header.parent_hash(), fork_block_hash_3b); + assert_eq!(*new_best_header.parent_hash(), fork_block_hash_4b); - // Produce a bundle after the new block #4 has been produced. + // Produce a bundle after the new block #5 has been produced. let (_slot, signed_bundle) = consensus_node .produce_slot_and_wait_for_bundle_submission() .await; - // In the new best fork, the receipt header number is 1 thus it produce the receipt - // of next block namely block 2 - let hash_2 = consensus_node.client.hash(2).unwrap().unwrap(); - let header_2 = consensus_node.client.header(hash_2).unwrap().unwrap(); + // In the new best fork, the receipt header number is 2 thus it produce the receipt + // of next block namely block 3 + let hash_3 = consensus_node.client.hash(3).unwrap().unwrap(); + let header_3 = consensus_node.client.header(hash_3).unwrap().unwrap(); assert_eq!( receipts_consensus_info(signed_bundle.unwrap()), - consensus_block_info(header_2) + consensus_block_info(header_3) ); } @@ -316,6 +330,8 @@ async fn test_domain_tx_propagate() { Ferdie, BasePath::new(directory.path().join("ferdie")), ); + // Produce 1 consensus block to initialize genesis domain + ferdie.produce_block_with_slot(1.into()).await.unwrap(); // Run Alice (a evm domain authority node) let alice = domain_test_service::DomainNodeBuilder::new( @@ -374,6 +390,8 @@ async fn test_executor_full_node_catching_up() { Ferdie, BasePath::new(directory.path().join("ferdie")), ); + // Produce 1 consensus block to initialize genesis domain + ferdie.produce_block_with_slot(1.into()).await.unwrap(); // Run Alice (a evm domain authority node) let alice = domain_test_service::DomainNodeBuilder::new( @@ -426,6 +444,8 @@ async fn test_executor_inherent_timestamp_is_set() { Ferdie, BasePath::new(directory.path().join("ferdie")), ); + // Produce 1 consensus block to initialize genesis domain + ferdie.produce_block_with_slot(1.into()).await.unwrap(); // Run Alice (a evm domain authority node) let alice = domain_test_service::DomainNodeBuilder::new( @@ -506,6 +526,8 @@ async fn test_invalid_state_transition_proof_creation_and_verification( Ferdie, BasePath::new(directory.path().join("ferdie")), ); + // Produce 1 consensus block to initialize genesis domain + ferdie.produce_block_with_slot(1.into()).await.unwrap(); // Run Alice (a evm domain authority node) let mut alice = domain_test_service::DomainNodeBuilder::new( @@ -642,6 +664,8 @@ async fn fraud_proof_verification_in_tx_pool_should_work() { Ferdie, BasePath::new(directory.path().join("ferdie")), ); + // Produce 1 consensus block to initialize genesis domain + ferdie.produce_block_with_slot(1.into()).await.unwrap(); // Run Alice (a evm domain authority node) let alice = domain_test_service::DomainNodeBuilder::new( @@ -814,6 +838,8 @@ async fn set_new_code_should_work() { Ferdie, BasePath::new(directory.path().join("ferdie")), ); + // Produce 1 consensus block to initialize genesis domain + ferdie.produce_block_with_slot(1.into()).await.unwrap(); // Run Alice (a evm domain authority node) let alice = domain_test_service::DomainNodeBuilder::new( @@ -885,6 +911,8 @@ async fn pallet_domains_unsigned_extrinsics_should_work() { Ferdie, BasePath::new(directory.path().join("ferdie")), ); + // Produce 1 consensus block to initialize genesis domain + ferdie.produce_block_with_slot(1.into()).await.unwrap(); // Run Alice (a evm domain authority node) let alice = domain_test_service::DomainNodeBuilder::new( @@ -983,6 +1011,8 @@ async fn duplicated_and_stale_bundle_should_be_rejected() { Ferdie, BasePath::new(directory.path().join("ferdie")), ); + // Produce 1 consensus block to initialize genesis domain + ferdie.produce_block_with_slot(1.into()).await.unwrap(); // Run Alice (a evm domain authority node) let alice = domain_test_service::DomainNodeBuilder::new( @@ -1059,6 +1089,8 @@ async fn existing_bundle_can_be_resubmitted_to_new_fork() { Ferdie, BasePath::new(directory.path().join("ferdie")), ); + // Produce 1 consensus block to initialize genesis domain + ferdie.produce_block_with_slot(1.into()).await.unwrap(); // Run Alice (a evm domain authority node) let alice = domain_test_service::DomainNodeBuilder::new( diff --git a/test/subspace-test-client/src/lib.rs b/test/subspace-test-client/src/lib.rs index 4413be5c6c..e9adcb3d5d 100644 --- a/test/subspace-test-client/src/lib.rs +++ b/test/subspace-test-client/src/lib.rs @@ -53,8 +53,10 @@ const MAX_PIECES_IN_SECTOR: u16 = 32; pub struct TestExecutorDispatch; impl sc_executor::NativeExecutionDispatch for TestExecutorDispatch { - /// Otherwise we only use the default Substrate host functions. - type ExtendHostFunctions = sp_consensus_subspace::consensus::HostFunctions; + type ExtendHostFunctions = ( + sp_consensus_subspace::consensus::HostFunctions, + sp_domains::domain::HostFunctions, + ); fn dispatch(method: &str, data: &[u8]) -> Option> { subspace_test_runtime::api::dispatch(method, data) diff --git a/test/subspace-test-runtime/src/lib.rs b/test/subspace-test-runtime/src/lib.rs index 97f1296fd9..aed3603453 100644 --- a/test/subspace-test-runtime/src/lib.rs +++ b/test/subspace-test-runtime/src/lib.rs @@ -511,7 +511,6 @@ impl pallet_offences_subspace::Config for Runtime { } parameter_types! { - pub const ReceiptsPruningDepth: BlockNumber = 256; pub const MaximumReceiptDrift: BlockNumber = 2; pub const InitialDomainTxRange: u64 = 10; pub const DomainTxRangeAdjustmentInterval: u64 = 100; @@ -524,6 +523,7 @@ parameter_types! { pub const MaxBundlesPerBlock: u32 = 10; pub const DomainInstantiationDeposit: Balance = 100 * SSC; pub const MaxDomainNameLength: u32 = 32; + pub const BlockTreePruningDepth: u32 = 256; pub const StakeWithdrawalLockingPeriod: BlockNumber = 20; pub const StakeEpochDuration: DomainNumber = 5; } @@ -546,6 +546,7 @@ impl pallet_domains::Config for Runtime { type DomainInstantiationDeposit = DomainInstantiationDeposit; type MaxDomainNameLength = MaxDomainNameLength; type Share = Balance; + type BlockTreePruningDepth = BlockTreePruningDepth; type StakeWithdrawalLockingPeriod = StakeWithdrawalLockingPeriod; type StakeEpochDuration = StakeEpochDuration; } @@ -897,14 +898,16 @@ fn extract_block_object_mapping(block: Block, successful_calls: Vec) -> Bl } fn extract_successful_bundles( + domain_id: DomainId, extrinsics: Vec, ) -> sp_domains::OpaqueBundles { - let successful_bundles = Domains::successful_bundles(); + let successful_bundles = Domains::successful_bundles(domain_id); extrinsics .into_iter() .filter_map(|uxt| match uxt.function { RuntimeCall::Domains(pallet_domains::Call::submit_bundle { opaque_bundle }) - if successful_bundles.contains(&opaque_bundle.hash()) => + if opaque_bundle.domain_id() == domain_id + && successful_bundles.contains(&opaque_bundle.hash()) => { Some(opaque_bundle) } @@ -919,7 +922,7 @@ fn extract_receipts( extrinsics: Vec, domain_id: DomainId, ) -> Vec> { - let successful_bundles = Domains::successful_bundles(); + let successful_bundles = Domains::successful_bundles(domain_id); extrinsics .into_iter() .filter_map(|uxt| match uxt.function { @@ -1185,13 +1188,14 @@ impl_runtime_apis! { } fn extract_successful_bundles( + domain_id: DomainId, extrinsics: Vec<::Extrinsic>, ) -> sp_domains::OpaqueBundles { - extract_successful_bundles(extrinsics) + extract_successful_bundles(domain_id, extrinsics) } fn successful_bundle_hashes() -> Vec { - Domains::successful_bundles() + Domains::successful_bundles_of_all_domains() } fn extrinsics_shuffling_seed(header: ::Header) -> Randomness { diff --git a/test/subspace-test-service/Cargo.toml b/test/subspace-test-service/Cargo.toml index 9b44d42ac7..9bf243f89c 100644 --- a/test/subspace-test-service/Cargo.toml +++ b/test/subspace-test-service/Cargo.toml @@ -45,12 +45,14 @@ sp-consensus = { git = "https://github.com/subspace/substrate", rev = "55c157cff sp-consensus-subspace = { version = "0.1.0", path = "../../crates/sp-consensus-subspace" } sp-consensus-slots = { version = "0.10.0-dev", default-features = false, git = "https://github.com/subspace/substrate", rev = "55c157cff49b638a59d81a9f971f0f9a66829c71" } sp-domains = { version = "0.1.0", path = "../../crates/sp-domains" } +sp-externalities = { version = "0.19.0", git = "https://github.com/subspace/substrate", rev = "55c157cff49b638a59d81a9f971f0f9a66829c71" } sp-keyring = { git = "https://github.com/subspace/substrate", rev = "55c157cff49b638a59d81a9f971f0f9a66829c71" } sp-timestamp = { version = "4.0.0-dev", git = "https://github.com/subspace/substrate", rev = "55c157cff49b638a59d81a9f971f0f9a66829c71" } sp-inherents = { version = "4.0.0-dev", git = "https://github.com/subspace/substrate", rev = "55c157cff49b638a59d81a9f971f0f9a66829c71" } sp-runtime = { git = "https://github.com/subspace/substrate", rev = "55c157cff49b638a59d81a9f971f0f9a66829c71" } subspace-core-primitives = { version = "0.1.0", default-features = false, path = "../../crates/subspace-core-primitives" } subspace-fraud-proof = { path = "../../crates/subspace-fraud-proof" } +subspace-node = { path = "../../crates/subspace-node" } subspace-runtime-primitives = { path = "../../crates/subspace-runtime-primitives" } subspace-service = { path = "../../crates/subspace-service" } subspace-test-client = { path = "../subspace-test-client" } diff --git a/test/subspace-test-service/src/lib.rs b/test/subspace-test-service/src/lib.rs index 7bb922e5d2..fadfc09c69 100644 --- a/test/subspace-test-service/src/lib.rs +++ b/test/subspace-test-service/src/lib.rs @@ -26,8 +26,8 @@ use futures::{select, FutureExt, SinkExt, StreamExt}; use jsonrpsee::RpcModule; use parking_lot::Mutex; use sc_block_builder::BlockBuilderProvider; -use sc_client_api::execution_extensions::ExecutionStrategies; -use sc_client_api::{backend, BlockchainEvents}; +use sc_client_api::execution_extensions::{ExecutionStrategies, ExtensionsFactory}; +use sc_client_api::{backend, BlockchainEvents, ExecutorProvider}; use sc_consensus::block_import::{ BlockCheckParams, BlockImportParams, ForkChoiceStrategy, ImportResult, }; @@ -56,7 +56,8 @@ use sp_consensus_subspace::digests::{CompatibleDigestItem, PreDigest}; use sp_consensus_subspace::FarmerPublicKey; use sp_core::traits::SpawnEssentialNamed; use sp_core::H256; -use sp_domains::OpaqueBundle; +use sp_domains::{GenerateGenesisStateRoot, GenesisReceiptExtension, OpaqueBundle}; +use sp_externalities::Extensions; use sp_inherents::{InherentData, InherentDataProvider}; use sp_keyring::Sr25519Keyring; use sp_runtime::generic::{BlockId, Digest}; @@ -72,6 +73,7 @@ use subspace_fraud_proof::domain_extrinsics_builder::DomainExtrinsicsBuilder; use subspace_fraud_proof::invalid_state_transition_proof::InvalidStateTransitionProofVerifier; use subspace_fraud_proof::invalid_transaction_proof::InvalidTransactionProofVerifier; use subspace_fraud_proof::verifier_api::VerifierClient; +use subspace_node::domain::DomainGenesisBlockBuilder; use subspace_runtime_primitives::opaque::Block; use subspace_runtime_primitives::{AccountId, Hash}; use subspace_service::tx_pre_validator::ConsensusChainTxPreValidator; @@ -182,6 +184,24 @@ type StorageChanges = sp_api::StorageChanges>; +struct MockExtensionsFactory(Arc); + +impl ExtensionsFactory for MockExtensionsFactory +where + Block: BlockT, +{ + fn extensions_for( + &self, + _block_hash: Block::Hash, + _block_number: NumberFor, + _capabilities: sp_core::offchain::Capabilities, + ) -> Extensions { + let mut exts = Extensions::new(); + exts.register(GenesisReceiptExtension::new(self.0.clone())); + exts + } +} + /// A mock Subspace consensus node instance used for testing. pub struct MockConsensusNode { /// `TaskManager`'s instance. @@ -251,6 +271,12 @@ impl MockConsensusNode { sc_service::new_full_parts::(&config, None, executor.clone()) .expect("Fail to new full parts"); + client + .execution_extensions() + .set_extensions_factory(MockExtensionsFactory(Arc::new( + DomainGenesisBlockBuilder::new(backend.clone(), executor.clone()), + ))); + let client = Arc::new(client); let select_chain = sc_consensus::LongestChain::new(backend.clone());