#[cfg(not(feature = "std"))]
extern crate alloc;
use crate::{
BalanceOf, BlockTree, BlockTreeNodeFor, BlockTreeNodes, Config, ConsensusBlockHash,
DomainBlockNumberFor, DomainHashingFor, DomainRuntimeUpgradeRecords, ExecutionInbox,
ExecutionReceiptOf, HeadDomainNumber, HeadReceiptNumber, InboxedBundleAuthor,
LatestConfirmedDomainExecutionReceipt, LatestSubmittedER, NewAddedHeadReceipt, Pallet,
ReceiptHashFor,
};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use codec::{Decode, Encode};
use frame_support::{ensure, PalletError};
use frame_system::pallet_prelude::BlockNumberFor;
use scale_info::TypeInfo;
use sp_core::Get;
use sp_domains::merkle_tree::MerkleTree;
use sp_domains::{
ChainId, DomainId, DomainsTransfersTracker, ExecutionReceipt, OnChainRewards, OperatorId,
Transfers,
};
use sp_runtime::traits::{BlockNumberProvider, CheckedSub, One, Saturating, Zero};
use sp_std::cmp::Ordering;
use sp_std::collections::btree_map::BTreeMap;
#[derive(TypeInfo, Encode, Decode, PalletError, Debug, PartialEq)]
pub enum Error {
InvalidExtrinsicsRoots,
UnknownParentBlockReceipt,
BuiltOnUnknownConsensusBlock,
InFutureReceipt,
PrunedReceipt,
StaleReceipt,
NewBranchReceipt,
BadGenesisReceipt,
UnexpectedReceiptType,
MaxHeadDomainNumber,
MissingDomainBlock,
InvalidTraceRoot,
InvalidExecutionTrace,
UnavailableConsensusBlockHash,
InvalidStateRoot,
BalanceOverflow,
DomainTransfersTracking,
InvalidDomainTransfers,
OverwritingER,
RuntimeNotFound,
LastBlockNotFound,
UnmatchedNewHeadReceipt,
}
#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
pub struct BlockTreeNode<Number, Hash, DomainNumber, DomainHash, Balance> {
pub execution_receipt: ExecutionReceipt<Number, Hash, DomainNumber, DomainHash, Balance>,
pub operator_ids: Vec<OperatorId>,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum AcceptedReceiptType {
NewHead,
CurrentHead,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum RejectedReceiptType {
InFuture,
Pruned,
Stale,
NewBranch,
}
impl From<RejectedReceiptType> for Error {
fn from(rejected_receipt: RejectedReceiptType) -> Error {
match rejected_receipt {
RejectedReceiptType::InFuture => Error::InFutureReceipt,
RejectedReceiptType::Pruned => Error::PrunedReceipt,
RejectedReceiptType::Stale => Error::StaleReceipt,
RejectedReceiptType::NewBranch => Error::NewBranchReceipt,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum ReceiptType {
Accepted(AcceptedReceiptType),
Rejected(RejectedReceiptType),
}
pub(crate) fn does_receipt_exists<T: Config>(
domain_id: DomainId,
domain_number: DomainBlockNumberFor<T>,
receipt_hash: ReceiptHashFor<T>,
) -> bool {
BlockTree::<T>::get(domain_id, domain_number)
.map(|h| h == receipt_hash)
.unwrap_or(false)
}
pub(crate) fn execution_receipt_type<T: Config>(
domain_id: DomainId,
execution_receipt: &ExecutionReceiptOf<T>,
) -> ReceiptType {
let receipt_number = execution_receipt.domain_block_number;
let head_receipt_number = HeadReceiptNumber::<T>::get(domain_id);
let head_receipt_extended = NewAddedHeadReceipt::<T>::get(domain_id).is_some();
let next_receipt_number = head_receipt_number.saturating_add(One::one());
let latest_confirmed_domain_block_number =
Pallet::<T>::latest_confirmed_domain_block_number(domain_id);
match receipt_number.cmp(&next_receipt_number) {
Ordering::Greater => ReceiptType::Rejected(RejectedReceiptType::InFuture),
Ordering::Equal => {
if head_receipt_extended {
ReceiptType::Rejected(RejectedReceiptType::InFuture)
} else {
ReceiptType::Accepted(AcceptedReceiptType::NewHead)
}
}
Ordering::Less => {
if !latest_confirmed_domain_block_number.is_zero()
&& receipt_number <= latest_confirmed_domain_block_number
{
return ReceiptType::Rejected(RejectedReceiptType::Pruned);
}
let already_exist = does_receipt_exists::<T>(
domain_id,
receipt_number,
execution_receipt.hash::<DomainHashingFor<T>>(),
);
if !already_exist {
return ReceiptType::Rejected(RejectedReceiptType::NewBranch);
}
let is_first_genesis_receipt =
receipt_number.is_zero() && HeadDomainNumber::<T>::get(domain_id).is_zero();
if receipt_number == head_receipt_number
&& (head_receipt_extended || is_first_genesis_receipt)
{
return ReceiptType::Accepted(AcceptedReceiptType::CurrentHead);
}
ReceiptType::Rejected(RejectedReceiptType::Stale)
}
}
}
pub(crate) fn verify_execution_receipt<T: Config>(
domain_id: DomainId,
execution_receipt: &ExecutionReceiptOf<T>,
) -> Result<(), Error> {
let ExecutionReceipt {
consensus_block_number,
consensus_block_hash,
domain_block_number,
inboxed_bundles,
parent_domain_block_receipt_hash,
execution_trace,
execution_trace_root,
final_state_root,
..
} = execution_receipt;
if let ReceiptType::Rejected(rejected_receipt_type) =
execution_receipt_type::<T>(domain_id, execution_receipt)
{
return Err(rejected_receipt_type.into());
}
if let Some(new_added_head_receipt) = NewAddedHeadReceipt::<T>::get(domain_id) {
ensure!(
new_added_head_receipt == execution_receipt.hash::<DomainHashingFor<T>>(),
Error::UnmatchedNewHeadReceipt,
);
return Ok(());
}
if domain_block_number.is_zero() {
ensure!(
does_receipt_exists::<T>(
domain_id,
*domain_block_number,
execution_receipt.hash::<DomainHashingFor<T>>(),
),
Error::BadGenesisReceipt
);
return Ok(());
}
if execution_trace.len() < 2 {
return Err(Error::InvalidExecutionTrace);
}
let maybe_domain_runtime_upgraded_at = {
let runtime_id = Pallet::<T>::runtime_id(domain_id).ok_or(Error::RuntimeNotFound)?;
DomainRuntimeUpgradeRecords::<T>::get(runtime_id).remove(consensus_block_number)
};
let excepted_consensus_block_hash =
match ConsensusBlockHash::<T>::get(domain_id, consensus_block_number) {
Some(hash) => hash,
None => {
let parent_block_number =
frame_system::Pallet::<T>::current_block_number() - One::one();
if *consensus_block_number == parent_block_number {
frame_system::Pallet::<T>::parent_hash()
} else if let Some(ref upgrade_entry) = maybe_domain_runtime_upgraded_at {
upgrade_entry.at_hash
} else {
return Err(Error::UnavailableConsensusBlockHash);
}
}
};
ensure!(
*consensus_block_hash == excepted_consensus_block_hash,
Error::BuiltOnUnknownConsensusBlock
);
let bundles_extrinsics_roots: Vec<_> =
inboxed_bundles.iter().map(|b| b.extrinsics_root).collect();
let execution_inbox =
ExecutionInbox::<T>::get((domain_id, domain_block_number, consensus_block_number));
let expected_extrinsics_roots: Vec<_> =
execution_inbox.iter().map(|b| b.extrinsics_root).collect();
ensure!(
(!bundles_extrinsics_roots.is_empty() || maybe_domain_runtime_upgraded_at.is_some())
&& bundles_extrinsics_roots == expected_extrinsics_roots,
Error::InvalidExtrinsicsRoots
);
let mut trace = Vec::with_capacity(execution_trace.len());
for root in execution_trace {
trace.push(
root.encode()
.try_into()
.map_err(|_| Error::InvalidTraceRoot)?,
);
}
let expected_execution_trace_root: sp_core::H256 = MerkleTree::from_leaves(trace.as_slice())
.root()
.ok_or(Error::InvalidTraceRoot)?
.into();
ensure!(
expected_execution_trace_root == *execution_trace_root,
Error::InvalidTraceRoot
);
if let Some(expected_final_state_root) = execution_trace.last() {
ensure!(
final_state_root == expected_final_state_root,
Error::InvalidStateRoot
);
}
if let Some(parent_block_number) = domain_block_number.checked_sub(&One::one()) {
let parent_block_exist = does_receipt_exists::<T>(
domain_id,
parent_block_number,
*parent_domain_block_receipt_hash,
);
ensure!(parent_block_exist, Error::UnknownParentBlockReceipt);
}
Ok(())
}
#[derive(Debug, PartialEq)]
pub(crate) struct ConfirmedDomainBlockInfo<ConsensusNumber, DomainNumber, Balance> {
pub consensus_block_number: ConsensusNumber,
pub domain_block_number: DomainNumber,
pub operator_ids: Vec<OperatorId>,
pub rewards: Balance,
pub invalid_bundle_authors: Vec<OperatorId>,
pub total_storage_fee: Balance,
pub paid_bundle_storage_fees: BTreeMap<OperatorId, u32>,
}
pub(crate) type ProcessExecutionReceiptResult<T> = Result<
Option<ConfirmedDomainBlockInfo<BlockNumberFor<T>, DomainBlockNumberFor<T>, BalanceOf<T>>>,
Error,
>;
pub(crate) fn process_execution_receipt<T: Config>(
domain_id: DomainId,
submitter: OperatorId,
execution_receipt: ExecutionReceiptOf<T>,
receipt_type: AcceptedReceiptType,
) -> ProcessExecutionReceiptResult<T> {
let er_hash = execution_receipt.hash::<DomainHashingFor<T>>();
let receipt_block_number = execution_receipt.domain_block_number;
match receipt_type {
AcceptedReceiptType::NewHead => {
add_new_receipt_to_block_tree::<T>(domain_id, submitter, execution_receipt)?;
HeadReceiptNumber::<T>::insert(domain_id, receipt_block_number);
NewAddedHeadReceipt::<T>::insert(domain_id, er_hash);
if let Some(to_prune) =
receipt_block_number.checked_sub(&T::BlockTreePruningDepth::get())
{
let BlockTreeNode {
execution_receipt,
operator_ids,
} = match prune_receipt::<T>(domain_id, to_prune)? {
Some(n) => n,
None => return Ok(None),
};
let mut paid_bundle_storage_fees = BTreeMap::new();
let mut invalid_bundle_authors = Vec::new();
let bundle_digests = ExecutionInbox::<T>::get((
domain_id,
to_prune,
execution_receipt.consensus_block_number,
));
for (index, bd) in bundle_digests.into_iter().enumerate() {
if let Some(bundle_author) = InboxedBundleAuthor::<T>::take(bd.header_hash) {
if execution_receipt.inboxed_bundles[index].is_invalid() {
invalid_bundle_authors.push(bundle_author);
} else {
paid_bundle_storage_fees
.entry(bundle_author)
.and_modify(|s| *s += bd.size)
.or_insert(bd.size);
}
}
}
let _ = ExecutionInbox::<T>::clear_prefix((domain_id, to_prune), u32::MAX, None);
LatestConfirmedDomainExecutionReceipt::<T>::insert(
domain_id,
execution_receipt.clone(),
);
ConsensusBlockHash::<T>::remove(
domain_id,
execution_receipt.consensus_block_number,
);
let block_fees = execution_receipt
.block_fees
.total_fees()
.ok_or(Error::BalanceOverflow)?;
ensure!(
execution_receipt
.transfers
.is_valid(ChainId::Domain(domain_id)),
Error::InvalidDomainTransfers
);
update_domain_transfers::<T>(domain_id, &execution_receipt.transfers, block_fees)
.map_err(|_| Error::DomainTransfersTracking)?;
update_domain_runtime_upgrade_records::<T>(
domain_id,
execution_receipt.consensus_block_number,
)?;
execution_receipt
.block_fees
.chain_rewards
.into_iter()
.for_each(|(chain_id, reward)| {
T::OnChainRewards::on_chain_rewards(chain_id, reward)
});
return Ok(Some(ConfirmedDomainBlockInfo {
consensus_block_number: execution_receipt.consensus_block_number,
domain_block_number: to_prune,
operator_ids,
rewards: execution_receipt.block_fees.domain_execution_fee,
invalid_bundle_authors,
total_storage_fee: execution_receipt.block_fees.consensus_storage_fee,
paid_bundle_storage_fees,
}));
}
}
AcceptedReceiptType::CurrentHead => {
BlockTreeNodes::<T>::mutate(er_hash, |maybe_node| {
let node = maybe_node.as_mut().expect(
"The domain block of `CurrentHead` receipt is checked to be exist in `execution_receipt_type`; qed"
);
node.operator_ids.push(submitter);
});
}
}
let key = (domain_id, submitter);
if receipt_block_number > Pallet::<T>::latest_submitted_er(key) {
LatestSubmittedER::<T>::insert(key, receipt_block_number)
}
Ok(None)
}
type TransferTrackerError<T> =
<<T as Config>::DomainsTransfersTracker as DomainsTransfersTracker<BalanceOf<T>>>::Error;
fn update_domain_transfers<T: Config>(
domain_id: DomainId,
transfers: &Transfers<BalanceOf<T>>,
block_fees: BalanceOf<T>,
) -> Result<(), TransferTrackerError<T>> {
let Transfers {
transfers_in,
transfers_out,
transfers_rejected,
rejected_transfers_claimed,
} = transfers;
let er_chain_id = ChainId::Domain(domain_id);
transfers_in
.iter()
.try_for_each(|(from_chain_id, amount)| {
T::DomainsTransfersTracker::confirm_transfer(*from_chain_id, er_chain_id, *amount)
})?;
transfers_out.iter().try_for_each(|(to_chain_id, amount)| {
T::DomainsTransfersTracker::note_transfer(er_chain_id, *to_chain_id, *amount)
})?;
transfers_rejected
.iter()
.try_for_each(|(from_chain_id, amount)| {
T::DomainsTransfersTracker::reject_transfer(*from_chain_id, er_chain_id, *amount)
})?;
rejected_transfers_claimed
.iter()
.try_for_each(|(to_chain_id, amount)| {
T::DomainsTransfersTracker::claim_rejected_transfer(er_chain_id, *to_chain_id, *amount)
})?;
T::DomainsTransfersTracker::reduce_domain_balance(domain_id, block_fees)?;
Ok(())
}
fn update_domain_runtime_upgrade_records<T: Config>(
domain_id: DomainId,
consensus_number: BlockNumberFor<T>,
) -> Result<(), Error> {
let runtime_id = Pallet::<T>::runtime_id(domain_id).ok_or(Error::RuntimeNotFound)?;
let mut domain_runtime_upgrade_records = DomainRuntimeUpgradeRecords::<T>::get(runtime_id);
if let Some(upgrade_entry) = domain_runtime_upgrade_records.get_mut(&consensus_number) {
if upgrade_entry.reference_count > One::one() {
upgrade_entry.reference_count =
upgrade_entry.reference_count.saturating_sub(One::one());
} else {
domain_runtime_upgrade_records.remove(&consensus_number);
}
if !domain_runtime_upgrade_records.is_empty() {
DomainRuntimeUpgradeRecords::<T>::set(runtime_id, domain_runtime_upgrade_records);
} else {
DomainRuntimeUpgradeRecords::<T>::remove(runtime_id);
}
}
Ok(())
}
fn add_new_receipt_to_block_tree<T: Config>(
domain_id: DomainId,
submitter: OperatorId,
execution_receipt: ExecutionReceiptOf<T>,
) -> Result<(), Error> {
let er_hash = execution_receipt.hash::<DomainHashingFor<T>>();
let domain_block_number = execution_receipt.domain_block_number;
ensure!(
!BlockTree::<T>::contains_key(domain_id, domain_block_number),
Error::OverwritingER,
);
BlockTree::<T>::insert(domain_id, domain_block_number, er_hash);
let block_tree_node = BlockTreeNode {
execution_receipt,
operator_ids: sp_std::vec![submitter],
};
BlockTreeNodes::<T>::insert(er_hash, block_tree_node);
Ok(())
}
pub(crate) fn import_genesis_receipt<T: Config>(
domain_id: DomainId,
genesis_receipt: ExecutionReceiptOf<T>,
) {
let er_hash = genesis_receipt.hash::<DomainHashingFor<T>>();
let domain_block_number = genesis_receipt.domain_block_number;
LatestConfirmedDomainExecutionReceipt::<T>::insert(domain_id, genesis_receipt.clone());
let block_tree_node = BlockTreeNode {
execution_receipt: genesis_receipt,
operator_ids: sp_std::vec![],
};
BlockTree::<T>::insert(domain_id, domain_block_number, er_hash);
BlockTreeNodes::<T>::insert(er_hash, block_tree_node);
}
pub(crate) fn prune_receipt<T: Config>(
domain_id: DomainId,
receipt_number: DomainBlockNumberFor<T>,
) -> Result<Option<BlockTreeNodeFor<T>>, Error> {
let receipt_hash = match BlockTree::<T>::take(domain_id, receipt_number) {
Some(er_hash) => er_hash,
None => return Ok(None),
};
let block_tree_node =
BlockTreeNodes::<T>::take(receipt_hash).ok_or(Error::MissingDomainBlock)?;
for operator_id in block_tree_node.operator_ids.iter() {
let key = (domain_id, operator_id);
let latest_submitted_er = Pallet::<T>::latest_submitted_er(key);
if block_tree_node.execution_receipt.domain_block_number == latest_submitted_er {
LatestSubmittedER::<T>::remove(key);
}
}
Ok(Some(block_tree_node))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{
create_dummy_bundle_with_receipts, create_dummy_receipt, extend_block_tree,
extend_block_tree_from_zero, get_block_tree_node_at, new_test_ext_with_extensions,
register_genesis_domain, run_to_block, BlockTreePruningDepth, Domains, Test,
};
use crate::FrozenDomains;
use frame_support::dispatch::RawOrigin;
use frame_support::{assert_err, assert_ok};
use frame_system::Origin;
use sp_core::H256;
use sp_domains::{BundleDigest, InboxedBundle, InvalidBundleType};
#[test]
fn test_genesis_receipt() {
let mut ext = new_test_ext_with_extensions();
ext.execute_with(|| {
let domain_id = register_genesis_domain(0u128, vec![0u64]);
let block_tree_node_at_0 = BlockTree::<Test>::get(domain_id, 0).unwrap();
let genesis_node = BlockTreeNodes::<Test>::get(block_tree_node_at_0).unwrap();
assert!(genesis_node.operator_ids.is_empty());
assert_eq!(HeadReceiptNumber::<Test>::get(domain_id), 0);
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::<Test>(
domain_id,
&genesis_receipt
));
assert_err!(
verify_execution_receipt::<Test>(domain_id, &invalid_genesis_receipt),
Error::NewBranchReceipt
);
});
}
#[test]
fn test_new_head_receipt() {
let creator = 0u128;
let operator_id = 1u64;
let block_tree_pruning_depth = <Test as Config>::BlockTreePruningDepth::get();
let mut ext = new_test_ext_with_extensions();
ext.execute_with(|| {
let domain_id = register_genesis_domain(creator, vec![operator_id]);
let genesis_node = get_block_tree_node_at::<Test>(domain_id, 0).unwrap();
let mut receipt = genesis_node.execution_receipt;
assert_eq!(
receipt.consensus_block_number,
frame_system::Pallet::<Test>::current_block_number()
);
let mut receipt_of_block_1 = None;
let mut bundle_header_hash_of_block_1 = None;
for block_number in 1..=(block_tree_pruning_depth as u64 + 3) {
run_to_block::<Test>(block_number, receipt.consensus_block_hash);
if block_number != 1 {
assert_eq!(
ConsensusBlockHash::<Test>::get(domain_id, block_number - 1),
Some(frame_system::Pallet::<Test>::block_hash(block_number - 1))
);
assert_eq!(
execution_receipt_type::<Test>(domain_id, &receipt),
ReceiptType::Accepted(AcceptedReceiptType::NewHead)
);
assert_ok!(verify_execution_receipt::<Test>(domain_id, &receipt));
}
let bundle_extrinsics_root = H256::random();
let bundle = create_dummy_bundle_with_receipts(
domain_id,
operator_id,
bundle_extrinsics_root,
receipt,
);
let bundle_header_hash = bundle.sealed_header.pre_hash();
let bundle_size = bundle.size();
assert_ok!(crate::Pallet::<Test>::submit_bundle(
RawOrigin::None.into(),
bundle,
));
assert_eq!(
ExecutionInbox::<Test>::get((domain_id, block_number as u32, block_number)),
vec![BundleDigest {
header_hash: bundle_header_hash,
extrinsics_root: bundle_extrinsics_root,
size: bundle_size,
}]
);
assert!(InboxedBundleAuthor::<Test>::contains_key(
bundle_header_hash
));
let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
assert_eq!(head_receipt_number, block_number as u32 - 1);
let parent_domain_block_receipt =
BlockTree::<Test>::get(domain_id, head_receipt_number).unwrap();
let parent_node = BlockTreeNodes::<Test>::get(parent_domain_block_receipt).unwrap();
assert_eq!(parent_node.operator_ids.len(), 1);
assert_eq!(parent_node.operator_ids[0], operator_id);
receipt = create_dummy_receipt(
block_number,
H256::random(),
parent_domain_block_receipt,
vec![bundle_extrinsics_root],
);
if block_number == 1 {
receipt_of_block_1.replace(receipt.clone());
bundle_header_hash_of_block_1.replace(bundle_header_hash);
}
}
let pruned_receipt = receipt_of_block_1.unwrap();
let pruned_bundle = bundle_header_hash_of_block_1.unwrap();
assert!(BlockTree::<Test>::get(domain_id, 1).is_none());
assert!(ExecutionInbox::<Test>::get((domain_id, 1, 1)).is_empty());
assert!(!InboxedBundleAuthor::<Test>::contains_key(pruned_bundle));
assert_eq!(
execution_receipt_type::<Test>(domain_id, &pruned_receipt),
ReceiptType::Rejected(RejectedReceiptType::Pruned)
);
assert_err!(
verify_execution_receipt::<Test>(domain_id, &pruned_receipt),
Error::PrunedReceipt
);
assert!(ConsensusBlockHash::<Test>::get(
domain_id,
pruned_receipt.consensus_block_number,
)
.is_none());
});
}
#[test]
fn test_confirm_current_head_receipt() {
let creator = 0u128;
let operator_id1 = 1u64;
let operator_id2 = 2u64;
let mut ext = new_test_ext_with_extensions();
ext.execute_with(|| {
let domain_id = register_genesis_domain(creator, vec![operator_id1, operator_id2]);
let next_head_receipt = extend_block_tree_from_zero(domain_id, operator_id1, 3);
assert_eq!(
execution_receipt_type::<Test>(domain_id, &next_head_receipt),
ReceiptType::Accepted(AcceptedReceiptType::NewHead)
);
assert_ok!(verify_execution_receipt::<Test>(
domain_id,
&next_head_receipt
));
let bundle = create_dummy_bundle_with_receipts(
domain_id,
operator_id1,
H256::random(),
next_head_receipt.clone(),
);
assert_ok!(crate::Pallet::<Test>::submit_bundle(
RawOrigin::None.into(),
bundle,
));
let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
let current_head_receipt =
get_block_tree_node_at::<Test>(domain_id, head_receipt_number)
.unwrap()
.execution_receipt;
assert_eq!(next_head_receipt, current_head_receipt);
assert_eq!(
execution_receipt_type::<Test>(domain_id, ¤t_head_receipt),
ReceiptType::Accepted(AcceptedReceiptType::CurrentHead)
);
assert_ok!(verify_execution_receipt::<Test>(
domain_id,
¤t_head_receipt
));
let bundle = create_dummy_bundle_with_receipts(
domain_id,
operator_id2,
H256::random(),
current_head_receipt,
);
assert_ok!(crate::Pallet::<Test>::submit_bundle(
RawOrigin::None.into(),
bundle,
));
let head_node = get_block_tree_node_at::<Test>(domain_id, head_receipt_number).unwrap();
assert_eq!(head_node.operator_ids, vec![operator_id1, operator_id2]);
});
}
#[test]
fn test_non_head_receipt() {
let creator = 0u128;
let operator_id1 = 1u64;
let operator_id2 = 2u64;
let mut ext = new_test_ext_with_extensions();
ext.execute_with(|| {
let domain_id = register_genesis_domain(creator, vec![operator_id1, operator_id2]);
extend_block_tree_from_zero(domain_id, operator_id1, 3);
let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
let stale_receipt = get_block_tree_node_at::<Test>(domain_id, head_receipt_number - 1)
.unwrap()
.execution_receipt;
let stale_receipt_hash = stale_receipt.hash::<DomainHashingFor<Test>>();
assert_eq!(
execution_receipt_type::<Test>(domain_id, &stale_receipt),
ReceiptType::Rejected(RejectedReceiptType::Stale)
);
assert_err!(
verify_execution_receipt::<Test>(domain_id, &stale_receipt),
Error::StaleReceipt
);
let bundle = create_dummy_bundle_with_receipts(
domain_id,
operator_id2,
H256::random(),
stale_receipt,
);
assert!(crate::Pallet::<Test>::submit_bundle(RawOrigin::None.into(), bundle).is_err());
assert_eq!(
BlockTreeNodes::<Test>::get(stale_receipt_hash)
.unwrap()
.operator_ids,
vec![operator_id1]
);
});
}
#[test]
fn test_previous_head_receipt() {
let creator = 0u128;
let operator_id1 = 1u64;
let operator_id2 = 2u64;
let mut ext = new_test_ext_with_extensions();
ext.execute_with(|| {
let domain_id = register_genesis_domain(creator, vec![operator_id1, operator_id2]);
extend_block_tree_from_zero(domain_id, operator_id1, 3);
assert!(NewAddedHeadReceipt::<Test>::get(domain_id).is_none());
let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
let previous_head_receipt =
get_block_tree_node_at::<Test>(domain_id, head_receipt_number)
.unwrap()
.execution_receipt;
assert_eq!(
execution_receipt_type::<Test>(domain_id, &previous_head_receipt),
ReceiptType::Rejected(RejectedReceiptType::Stale)
);
assert_err!(
verify_execution_receipt::<Test>(domain_id, &previous_head_receipt),
Error::StaleReceipt
);
let bundle = create_dummy_bundle_with_receipts(
domain_id,
operator_id2,
H256::random(),
previous_head_receipt,
);
assert!(crate::Pallet::<Test>::submit_bundle(RawOrigin::None.into(), bundle).is_err());
});
}
#[test]
fn test_new_branch_receipt() {
let creator = 0u128;
let operator_id1 = 1u64;
let operator_id2 = 2u64;
let mut ext = new_test_ext_with_extensions();
ext.execute_with(|| {
let domain_id = register_genesis_domain(creator, vec![operator_id1, operator_id2]);
extend_block_tree_from_zero(domain_id, operator_id1, 3);
let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
assert!(BlockTree::<Test>::get(domain_id, head_receipt_number).is_some());
let new_branch_receipt = {
let mut head_receipt =
get_block_tree_node_at::<Test>(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::<DomainHashingFor<Test>>();
assert_eq!(
execution_receipt_type::<Test>(domain_id, &new_branch_receipt),
ReceiptType::Rejected(RejectedReceiptType::NewBranch)
);
assert_err!(
verify_execution_receipt::<Test>(domain_id, &new_branch_receipt),
Error::NewBranchReceipt
);
let bundle = create_dummy_bundle_with_receipts(
domain_id,
operator_id2,
H256::random(),
new_branch_receipt,
);
assert!(crate::Pallet::<Test>::submit_bundle(RawOrigin::None.into(), bundle).is_err());
assert!(BlockTreeNodes::<Test>::get(new_branch_receipt_hash).is_none());
});
}
#[test]
fn test_prune_domain_execution_receipt() {
let creator = 0u128;
let operator_id = 1u64;
let mut ext = new_test_ext_with_extensions();
ext.execute_with(|| {
let domain_id = register_genesis_domain(creator, vec![operator_id]);
let _next_receipt = extend_block_tree_from_zero(domain_id, operator_id, 3);
let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
assert!(!FrozenDomains::<Test>::get().contains(&domain_id));
Domains::freeze_domain(Origin::<Test>::Root.into(), domain_id).unwrap();
assert!(FrozenDomains::<Test>::get().contains(&domain_id));
let head_receipt_hash = BlockTree::<Test>::get(domain_id, head_receipt_number).unwrap();
Domains::prune_domain_execution_receipt(
Origin::<Test>::Root.into(),
domain_id,
head_receipt_hash,
)
.unwrap();
assert_eq!(
HeadReceiptNumber::<Test>::get(domain_id),
head_receipt_number - 1
);
Domains::unfreeze_domain(Origin::<Test>::Root.into(), domain_id).unwrap();
assert!(!FrozenDomains::<Test>::get().contains(&domain_id));
})
}
#[test]
fn test_invalid_receipt() {
let creator = 0u128;
let operator_id = 1u64;
let mut ext = new_test_ext_with_extensions();
ext.execute_with(|| {
let domain_id = register_genesis_domain(creator, vec![operator_id]);
let next_receipt = extend_block_tree_from_zero(domain_id, operator_id, 3);
let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
let mut future_receipt = next_receipt.clone();
future_receipt.domain_block_number = head_receipt_number + 2;
future_receipt.consensus_block_number = head_receipt_number as u64 + 2;
ExecutionInbox::<Test>::insert(
(
domain_id,
future_receipt.domain_block_number,
future_receipt.consensus_block_number,
),
future_receipt
.inboxed_bundles
.clone()
.into_iter()
.map(|b| BundleDigest {
header_hash: H256::random(),
extrinsics_root: b.extrinsics_root,
size: 0,
})
.collect::<Vec<_>>(),
);
assert_eq!(
execution_receipt_type::<Test>(domain_id, &future_receipt),
ReceiptType::Rejected(RejectedReceiptType::InFuture)
);
assert_err!(
verify_execution_receipt::<Test>(domain_id, &future_receipt),
Error::InFutureReceipt
);
let mut unknown_extrinsics_roots_receipt = next_receipt.clone();
unknown_extrinsics_roots_receipt.inboxed_bundles =
vec![InboxedBundle::valid(H256::random(), H256::random())];
assert_err!(
verify_execution_receipt::<Test>(domain_id, &unknown_extrinsics_roots_receipt),
Error::InvalidExtrinsicsRoots
);
let mut unknown_consensus_block_receipt = next_receipt.clone();
unknown_consensus_block_receipt.consensus_block_hash = H256::random();
assert_err!(
verify_execution_receipt::<Test>(domain_id, &unknown_consensus_block_receipt),
Error::BuiltOnUnknownConsensusBlock
);
let mut unknown_parent_receipt = next_receipt.clone();
unknown_parent_receipt.parent_domain_block_receipt_hash = H256::random();
assert_err!(
verify_execution_receipt::<Test>(domain_id, &unknown_parent_receipt),
Error::UnknownParentBlockReceipt
);
let mut invalid_execution_trace_receipt = next_receipt;
invalid_execution_trace_receipt.execution_trace = vec![invalid_execution_trace_receipt
.execution_trace
.first()
.cloned()
.expect("First element should be there; qed")];
assert_err!(
verify_execution_receipt::<Test>(domain_id, &invalid_execution_trace_receipt),
Error::InvalidExecutionTrace
);
invalid_execution_trace_receipt.execution_trace = vec![];
assert_err!(
verify_execution_receipt::<Test>(domain_id, &invalid_execution_trace_receipt),
Error::InvalidExecutionTrace
);
});
}
#[test]
fn test_invalid_receipt_with_head_receipt_already_extended() {
let creator = 0u128;
let operator_id = 1u64;
let mut ext = new_test_ext_with_extensions();
ext.execute_with(|| {
let domain_id = register_genesis_domain(creator, vec![operator_id]);
let next_receipt = extend_block_tree_from_zero(domain_id, operator_id, 3);
let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
assert!(NewAddedHeadReceipt::<Test>::get(domain_id).is_none());
NewAddedHeadReceipt::<Test>::set(domain_id, Some(H256::random()));
let mut future_receipt = next_receipt.clone();
future_receipt.domain_block_number = head_receipt_number + 1;
future_receipt.consensus_block_number = head_receipt_number as u64 + 1;
ExecutionInbox::<Test>::insert(
(
domain_id,
future_receipt.domain_block_number,
future_receipt.consensus_block_number,
),
future_receipt
.inboxed_bundles
.clone()
.into_iter()
.map(|b| BundleDigest {
header_hash: H256::random(),
extrinsics_root: b.extrinsics_root,
size: 0,
})
.collect::<Vec<_>>(),
);
assert_eq!(
execution_receipt_type::<Test>(domain_id, &future_receipt),
ReceiptType::Rejected(RejectedReceiptType::InFuture)
);
assert_err!(
verify_execution_receipt::<Test>(domain_id, &future_receipt),
Error::InFutureReceipt
);
});
}
#[test]
fn test_invalid_trace_root_receipt() {
let creator = 0u128;
let operator_id1 = 1u64;
let operator_id2 = 2u64;
let mut ext = new_test_ext_with_extensions();
ext.execute_with(|| {
let domain_id = register_genesis_domain(creator, vec![operator_id1, operator_id2]);
let mut next_receipt = extend_block_tree_from_zero(domain_id, operator_id1, 3);
next_receipt.execution_trace.push(H256::random());
next_receipt.final_state_root = *next_receipt.execution_trace.last().unwrap();
let mut trace = Vec::with_capacity(next_receipt.execution_trace.len());
for root in &next_receipt.execution_trace {
trace.push(
root.encode()
.try_into()
.map_err(|_| Error::InvalidTraceRoot)
.expect("H256 to Blake3Hash should be successful; qed"),
);
}
let new_execution_trace_root = MerkleTree::from_leaves(trace.as_slice())
.root()
.expect("Compute merkle root of trace should success")
.into();
next_receipt.execution_trace_root = new_execution_trace_root;
assert_ok!(verify_execution_receipt::<Test>(domain_id, &next_receipt));
let mut invalid_receipt = next_receipt.clone();
invalid_receipt.execution_trace_root = H256::random();
assert_err!(
verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
Error::InvalidTraceRoot
);
let mut invalid_receipt = next_receipt.clone();
invalid_receipt.execution_trace[0] = H256::random();
assert_err!(
verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
Error::InvalidTraceRoot
);
let mut invalid_receipt = next_receipt.clone();
invalid_receipt.execution_trace.push(H256::random());
assert_err!(
verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
Error::InvalidTraceRoot
);
let mut invalid_receipt = next_receipt;
invalid_receipt.execution_trace.pop();
assert_err!(
verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
Error::InvalidTraceRoot
);
});
}
#[test]
fn test_collect_invalid_bundle_author() {
let creator = 0u128;
let challenge_period = BlockTreePruningDepth::get() as u64;
let operator_set: Vec<_> = (1..15).collect();
let mut ext = new_test_ext_with_extensions();
ext.execute_with(|| {
let domain_id = register_genesis_domain(creator, operator_set.clone());
let next_receipt = extend_block_tree_from_zero(domain_id, operator_set[0], 3);
for operator_id in operator_set.iter() {
let bundle = create_dummy_bundle_with_receipts(
domain_id,
*operator_id,
H256::random(),
next_receipt.clone(),
);
assert_ok!(crate::Pallet::<Test>::submit_bundle(
RawOrigin::None.into(),
bundle,
));
}
let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);
let head_node = get_block_tree_node_at::<Test>(domain_id, head_receipt_number).unwrap();
assert_eq!(head_node.operator_ids, operator_set);
let current_block_number = frame_system::Pallet::<Test>::current_block_number();
let execution_inbox = ExecutionInbox::<Test>::get((
domain_id,
current_block_number as u32,
current_block_number,
));
let bundles_extrinsics_roots: Vec<_> = execution_inbox
.into_iter()
.map(|b| b.extrinsics_root)
.collect();
assert_eq!(bundles_extrinsics_roots.len(), operator_set.len());
let mut bundles = vec![];
let mut invalid_bundle_authors = vec![];
for (i, (operator, extrinsics_root)) in operator_set
.iter()
.zip(bundles_extrinsics_roots)
.enumerate()
{
if i % 2 == 0 {
invalid_bundle_authors.push(*operator);
bundles.push(InboxedBundle::invalid(
InvalidBundleType::OutOfRangeTx(0),
extrinsics_root,
));
} else {
bundles.push(InboxedBundle::valid(H256::random(), extrinsics_root));
}
}
let mut target_receipt = create_dummy_receipt(
current_block_number,
H256::random(),
next_receipt.hash::<DomainHashingFor<Test>>(),
vec![],
);
target_receipt.inboxed_bundles = bundles;
let next_receipt = extend_block_tree(
domain_id,
operator_set[0],
(current_block_number + challenge_period) as u32 + 1,
target_receipt,
);
let confirmed_domain_block = process_execution_receipt::<Test>(
domain_id,
operator_set[0],
next_receipt,
AcceptedReceiptType::NewHead,
)
.unwrap()
.unwrap();
assert_eq!(
confirmed_domain_block.invalid_bundle_authors,
invalid_bundle_authors
);
});
}
}