Skip to content

Commit

Permalink
Merge pull request #1694 from subspace/bundle-check
Browse files Browse the repository at this point in the history
Add more checks to the bundle verification
  • Loading branch information
NingLin-P authored Jul 26, 2023
2 parents bf08b6c + 53e74ef commit 229c59e
Show file tree
Hide file tree
Showing 3 changed files with 279 additions and 54 deletions.
74 changes: 74 additions & 0 deletions crates/pallet-domains/src/block_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use codec::{Decode, Encode};
use frame_support::{ensure, PalletError};
use scale_info::TypeInfo;
use sp_core::Get;
use sp_domains::merkle_tree::MerkleTree;
use sp_domains::{DomainId, ExecutionReceipt, OperatorId};
use sp_runtime::traits::{CheckedSub, One, Saturating, Zero};
use sp_std::cmp::Ordering;
Expand All @@ -26,6 +27,7 @@ pub enum Error {
MaxHeadDomainNumber,
MultipleERsAfterChallengePeriod,
MissingDomainBlock,
InvalidTraceRoot,
}

#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -121,6 +123,8 @@ pub(crate) fn verify_execution_receipt<T: Config>(
domain_block_number,
block_extrinsics_roots,
parent_domain_block_receipt_hash,
execution_trace,
execution_trace_root,
..
} = execution_receipt;

Expand All @@ -138,6 +142,24 @@ pub(crate) fn verify_execution_receipt<T: Config>(
!block_extrinsics_roots.is_empty() && *block_extrinsics_roots == execution_inbox,
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
);
}

let excepted_consensus_block_hash =
Expand Down Expand Up @@ -777,4 +799,56 @@ mod tests {
);
});
}

#[test]
fn test_invalid_trace_root_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, vec![operator_id1, operator_id2]);
extend_block_tree(domain_id, operator_id1, 3);

let head_receipt_number = HeadReceiptNumber::<Test>::get(domain_id);

// Get the head receipt
let current_head_receipt =
get_block_tree_node_at::<Test>(domain_id, head_receipt_number)
.unwrap()
.execution_receipt;

// Receipt with wrong value of `execution_trace_root`
let mut invalid_receipt = current_head_receipt.clone();
invalid_receipt.execution_trace_root = H256::random();
assert_err!(
verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
Error::InvalidTraceRoot
);

// Receipt with wrong value of trace
let mut invalid_receipt = current_head_receipt.clone();
invalid_receipt.execution_trace[0] = H256::random();
assert_err!(
verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
Error::InvalidTraceRoot
);

// Receipt with addtional trace
let mut invalid_receipt = current_head_receipt.clone();
invalid_receipt.execution_trace.push(H256::random());
assert_err!(
verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
Error::InvalidTraceRoot
);

// Receipt with missing trace
let mut invalid_receipt = current_head_receipt;
invalid_receipt.execution_trace.pop();
assert_err!(
verify_execution_receipt::<Test>(domain_id, &invalid_receipt),
Error::InvalidTraceRoot
);
});
}
}
129 changes: 84 additions & 45 deletions crates/pallet-domains/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ mod staking_epoch;
pub mod weights;

use crate::block_tree::verify_execution_receipt;
use crate::staking::Operator;
use codec::{Decode, Encode};
use frame_support::ensure;
use frame_support::traits::fungible::{Inspect, InspectFreeze};
use frame_support::traits::Get;
use frame_system::offchain::SubmitTransaction;
Expand All @@ -43,10 +45,9 @@ use sp_domains::bundle_producer_election::{is_below_threshold, BundleProducerEle
use sp_domains::fraud_proof::FraudProof;
use sp_domains::{
DomainBlockLimit, DomainId, DomainInstanceData, ExecutionReceipt, OpaqueBundle, OperatorId,
OperatorPublicKey, RuntimeId,
OperatorPublicKey, ProofOfElection, RuntimeId,
};
use sp_runtime::traits::{BlockNumberProvider, CheckedSub, One, Zero};
use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidityError};
use sp_runtime::traits::{BlakeTwo256, BlockNumberProvider, CheckedSub, Hash, One, Zero};
use sp_runtime::{RuntimeAppPublic, SaturatedConversion, Saturating};
use sp_std::boxed::Box;
use sp_std::collections::btree_map::BTreeMap;
Expand Down Expand Up @@ -469,6 +470,10 @@ mod pallet {
StaleBundle,
/// An invalid execution receipt found in the bundle.
Receipt(BlockTreeError),
/// Bundle size exceed the max bundle size limit in the domain config
BundleTooLarge,
// Bundle with an invalid extrinsic root
InvalidExtrinsicRoot,
}

impl<T> From<RuntimeRegistryError> for Error<T> {
Expand Down Expand Up @@ -969,9 +974,8 @@ mod pallet {
type Call = Call<T>;
fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
match call {
Call::submit_bundle { opaque_bundle } => {
Self::pre_dispatch_submit_bundle(opaque_bundle)
}
Call::submit_bundle { opaque_bundle } => Self::validate_bundle(opaque_bundle)
.map_err(|_| InvalidTransaction::Call.into()),
Call::submit_fraud_proof { fraud_proof: _ } => Ok(()),
_ => Err(InvalidTransaction::Call.into()),
}
Expand Down Expand Up @@ -1089,18 +1093,6 @@ impl<T: Config> Pallet<T> {
.map(|operator| (operator.signing_key, operator.current_total_stake))
}

fn pre_dispatch_submit_bundle(
opaque_bundle: &OpaqueBundleOf<T>,
) -> Result<(), TransactionValidityError> {
let domain_id = opaque_bundle.domain_id();
let receipt = &opaque_bundle.sealed_header.header.receipt;

// TODO: Implement bundle validation.

verify_execution_receipt::<T>(domain_id, receipt)
.map_err(|_| InvalidTransaction::Call.into())
}

// Check if a bundle is stale
fn check_stale_bundle(
current_block_number: T::BlockNumber,
Expand Down Expand Up @@ -1135,7 +1127,63 @@ impl<T: Config> Pallet<T> {
Ok(())
}

fn check_bundle_size(
opaque_bundle: &OpaqueBundleOf<T>,
max_size: u32,
) -> Result<(), BundleError> {
let bundle_size = opaque_bundle
.extrinsics
.iter()
.fold(0, |acc, xt| acc + xt.encoded_size() as u32);
ensure!(max_size >= bundle_size, BundleError::BundleTooLarge);
Ok(())
}

fn check_extrinsics_root(opaque_bundle: &OpaqueBundleOf<T>) -> Result<(), BundleError> {
let expected_extrinsics_root = BlakeTwo256::ordered_trie_root(
opaque_bundle
.extrinsics
.iter()
.map(|xt| xt.encode())
.collect(),
sp_core::storage::StateVersion::V1,
);
ensure!(
expected_extrinsics_root == opaque_bundle.extrinsics_root(),
BundleError::InvalidExtrinsicRoot
);
Ok(())
}

fn check_proof_of_election(
domain_id: DomainId,
operator_id: OperatorId,
operator: Operator<BalanceOf<T>, T::Share>,
bundle_slot_probability: (u64, u64),
proof_of_election: &ProofOfElection<T::DomainHash>,
) -> Result<(), BundleError> {
proof_of_election
.verify_vrf_signature(&operator.signing_key)
.map_err(|_| BundleError::BadVrfSignature)?;

let (operator_stake, total_domain_stake) =
Self::fetch_operator_stake_info(domain_id, &operator_id)?;

let threshold = sp_domains::bundle_producer_election::calculate_threshold(
operator_stake.saturated_into(),
total_domain_stake.saturated_into(),
bundle_slot_probability,
);

if !is_below_threshold(&proof_of_election.vrf_signature.output, threshold) {
return Err(BundleError::ThresholdUnsatisfied);
}

Ok(())
}

fn validate_bundle(opaque_bundle: &OpaqueBundleOf<T>) -> Result<(), BundleError> {
let domain_id = opaque_bundle.domain_id();
let operator_id = opaque_bundle.operator_id();
let sealed_header = &opaque_bundle.sealed_header;

Expand All @@ -1148,41 +1196,32 @@ impl<T: Config> Pallet<T> {
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::<T>::current_block_number();

// Reject the stale bundles so that they can't be used by attacker to occupy the block space without cost.
let current_block_number = frame_system::Pallet::<T>::current_block_number();
let bundle_create_at = sealed_header.header.consensus_block_number;
Self::check_stale_bundle(current_block_number, bundle_create_at)?;

// TODO: Implement bundle validation.
let domain_config = DomainRegistry::<T>::get(domain_id)
.ok_or(BundleError::InvalidDomainId)?
.domain_config;

verify_execution_receipt::<T>(domain_id, receipt).map_err(BundleError::Receipt)?;
// TODO: check bundle weight with `domain_config.max_block_weight`

let proof_of_election = &sealed_header.header.proof_of_election;
proof_of_election
.verify_vrf_signature(&operator.signing_key)
.map_err(|_| BundleError::BadVrfSignature)?;
Self::check_bundle_size(opaque_bundle, domain_config.max_block_size)?;

let bundle_slot_probability = DomainRegistry::<T>::get(domain_id)
.ok_or(BundleError::InvalidDomainId)?
.domain_config
.bundle_slot_probability;
Self::check_extrinsics_root(opaque_bundle)?;

let (operator_stake, total_domain_stake) =
Self::fetch_operator_stake_info(domain_id, &operator_id)?;

let threshold = sp_domains::bundle_producer_election::calculate_threshold(
operator_stake.saturated_into(),
total_domain_stake.saturated_into(),
bundle_slot_probability,
);
let proof_of_election = &sealed_header.header.proof_of_election;
Self::check_proof_of_election(
domain_id,
operator_id,
operator,
domain_config.bundle_slot_probability,
proof_of_election,
)?;

if !is_below_threshold(&proof_of_election.vrf_signature.output, threshold) {
return Err(BundleError::ThresholdUnsatisfied);
}
let receipt = &sealed_header.header.receipt;
verify_execution_receipt::<T>(domain_id, receipt).map_err(BundleError::Receipt)?;

Ok(())
}
Expand Down
Loading

0 comments on commit 229c59e

Please sign in to comment.