Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Refactor validate_block #2069

Merged
merged 5 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions pallets/parachain-system/proc-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,20 @@ pub fn register_validate_block(input: proc_macro::TokenStream) -> proc_macro::To
use super::*;

#[no_mangle]
unsafe fn validate_block(arguments: *const u8, arguments_len: usize) -> u64 {
let params = #crate_::validate_block::polkadot_parachain::load_params(
arguments,
arguments_len,
unsafe fn validate_block(arguments: *mut u8, arguments_len: usize) -> u64 {
// We convert the `arguments` into a boxed slice and then into `Bytes`.
let args = #crate_::validate_block::sp_std::boxed::Box::from_raw(
#crate_::validate_block::sp_std::slice::from_raw_parts_mut(
arguments,
arguments_len,
)
);
let args = #crate_::validate_block::bytes::Bytes::from(args);

// Then we decode from these bytes the `MemoryOptimizedValidationParams`.
let params = #crate_::validate_block::decode_from_bytes::<
#crate_::validate_block::MemoryOptimizedValidationParams
>(args).expect("Invalid arguments to `validate_block`.");

let res = #crate_::validate_block::implementation::validate_block::<
<#runtime as #crate_::validate_block::GetRuntimeBlockType>::RuntimeBlock,
Expand Down
30 changes: 0 additions & 30 deletions pallets/parachain-system/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,6 @@ pub mod pallet {
horizontal_messages,
} = data;

Self::validate_validation_data(&vfp);

// Check that the associated relay chain block number is as expected.
T::CheckAssociatedRelayNumber::check_associated_relay_number(
vfp.relay_parent_number,
Expand Down Expand Up @@ -769,34 +767,6 @@ impl<T: Config> GetChannelInfo for Pallet<T> {
}

impl<T: Config> Pallet<T> {
/// Validate the given [`PersistedValidationData`] against the
/// [`ValidationParams`](polkadot_parachain::primitives::ValidationParams).
///
/// This check will only be executed when the block is currently being executed in the context
/// of [`validate_block`]. If this is being executed in the context of block building or block
/// import, this is a no-op.
///
/// # Panics
///
/// Panics while validating the `PoV` on the relay chain if the [`PersistedValidationData`]
/// passed by the block author was incorrect.
fn validate_validation_data(validation_data: &PersistedValidationData) {
validate_block::with_validation_params(|params| {
assert_eq!(
params.parent_head, validation_data.parent_head,
"Parent head doesn't match"
);
assert_eq!(
params.relay_parent_number, validation_data.relay_parent_number,
"Relay parent number doesn't match",
);
assert_eq!(
params.relay_parent_storage_root, validation_data.relay_parent_storage_root,
"Relay parent storage root doesn't match",
);
});
}

/// Process all inbound downward messages relayed by the collator.
///
/// Checks if the sequence of the messages is valid, dispatches them and communicates the
Expand Down
185 changes: 128 additions & 57 deletions pallets/parachain-system/src/validate_block/implementation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,24 @@

//! The actual implementation of the validate block functionality.

use frame_support::traits::{ExecuteBlock, ExtrinsicCall, Get, IsSubType};
use sp_runtime::traits::{Block as BlockT, Extrinsic, HashFor, Header as HeaderT};

use sp_io::KillStorageResult;
use sp_std::prelude::*;
use super::MemoryOptimizedValidationParams;
use cumulus_primitives_core::{
relay_chain::v2::Hash as RHash, ParachainBlockData, PersistedValidationData,
};
use cumulus_primitives_parachain_inherent::ParachainInherentData;

use polkadot_parachain::primitives::{HeadData, ValidationParams, ValidationResult};
use polkadot_parachain::primitives::{
HeadData, RelayChainBlockNumber, ValidationParams, ValidationResult,
};

use codec::{Decode, Encode};

use frame_support::traits::{ExecuteBlock, ExtrinsicCall, Get, IsSubType};
use sp_core::storage::{ChildInfo, StateVersion};
use sp_externalities::{set_and_run_with_externalities, Externalities};
use sp_io::KillStorageResult;
use sp_runtime::traits::{Block as BlockT, Extrinsic, HashFor, Header as HeaderT};
use sp_std::{mem, prelude::*};
use sp_trie::MemoryDB;

type TrieBackend<B> = sp_state_machine::TrieBackend<MemoryDB<HashFor<B>>, HashFor<B>>;
Expand All @@ -38,43 +44,84 @@ fn with_externalities<F: FnOnce(&mut dyn Externalities) -> R, R>(f: F) -> R {
sp_externalities::with_externalities(f).expect("Environmental externalities not set.")
}

/// Validate a given parachain block on a validator.
/// Validate the given parachain block.
///
/// This function is doing roughly the following:
///
/// 1. We decode the [`ParachainBlockData`] from the `block_data` in `params`.
///
/// 2. We are doing some security checks like checking that the `parent_head` in `params`
/// is the parent of the block we are going to check. We also ensure that the `set_validation_data`
/// inherent is present in the block and that the validation data matches the values in `params`.
///
/// 3. We construct the sparse in-memory database from the storage proof inside the block data and
/// then ensure that the storage root matches the storage root in the `parent_head`.
///
/// 4. We replace all the storage related host functions with functions inside the wasm blob.
/// This means instead of calling into the host, we will stay inside the wasm execution. This is
/// very important as the relay chain validator hasn't the state required to verify the block. But
/// we have the in-memory database that contains all the values from the state of the parachain
/// that we require to verify the block.
///
/// 5. We are going to run `check_inherents`. This is important to check stuff like the timestamp
/// matching the real world time.
///
/// 6. The last step is to execute the entire block in the machinery we just have setup. Executing
/// the blocks include running all transactions in the block against our in-memory database and
/// ensuring that the final storage root matches the storage root in the header of the block. In the
/// end we return back the [`ValidationResult`] with all the required information for the validator.
#[doc(hidden)]
pub fn validate_block<
B: BlockT,
E: ExecuteBlock<B>,
PSC: crate::Config,
CI: crate::CheckInherents<B>,
>(
params: ValidationParams,
MemoryOptimizedValidationParams {
block_data,
parent_head,
relay_parent_number,
relay_parent_storage_root,
}: MemoryOptimizedValidationParams,
) -> ValidationResult
where
B::Extrinsic: ExtrinsicCall,
<B::Extrinsic as Extrinsic>::Call: IsSubType<crate::Call<PSC>>,
{
let block_data =
cumulus_primitives_core::ParachainBlockData::<B>::decode(&mut &params.block_data.0[..])
.expect("Invalid parachain block data");
let block_data = codec::decode_from_bytes::<ParachainBlockData<B>>(block_data)
.expect("Invalid parachain block data");

let parent_head =
B::Header::decode(&mut &params.parent_head.0[..]).expect("Invalid parent head");
let parent_header =
codec::decode_from_bytes::<B::Header>(parent_head.clone()).expect("Invalid parent head");

let (header, extrinsics, storage_proof) = block_data.deconstruct();

let head_data = HeadData(header.encode());
Copy link
Contributor

@koute koute Jan 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this allocation could be delayed? (e.g. by doing let head_data = HeadData(block.header().encode()) since block is alive until the very end).

(But might not make any difference; feel free to ignore this comment.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved it down now, but this will not really change anything as we will need to do this before executing the block (which consumes it)


let block = B::new(header, extrinsics);
assert!(parent_head.hash() == *block.header().parent_hash(), "Invalid parent hash",);
assert!(parent_header.hash() == *block.header().parent_hash(), "Invalid parent hash");

let inherent_data = extract_parachain_inherent_data(&block);
melekes marked this conversation as resolved.
Show resolved Hide resolved

validate_validation_data(
&inherent_data.validation_data,
relay_parent_number,
relay_parent_storage_root,
parent_head,
koute marked this conversation as resolved.
Show resolved Hide resolved
);

// Create the db
let db = match storage_proof.to_memory_db(Some(parent_head.state_root())) {
let db = match storage_proof.to_memory_db(Some(parent_header.state_root())) {
Ok((db, _)) => db,
Err(_) => panic!("Compact proof decoding failure."),
};

sp_std::mem::drop(storage_proof);

let backend = sp_state_machine::TrieBackendBuilder::new(db, *parent_head.state_root()).build();
// We use the storage root of the `parent_head` to ensure that it is the correct root.
// This is already being done above while creating the in-memory db, but let's be paranoid!!
let backend =
sp_state_machine::TrieBackendBuilder::new(db, *parent_header.state_root()).build();

let _guard = (
// Replace storage calls with our own implementations
Expand Down Expand Up @@ -115,22 +162,6 @@ where
sp_io::offchain_index::host_clear.replace_implementation(host_offchain_index_clear),
);

let inherent_data = block
.extrinsics()
.iter()
// Inherents are at the front of the block and are unsigned.
//
// If `is_signed` is returning `None`, we keep it safe and assume that it is "signed".
// We are searching for unsigned transactions anyway.
.take_while(|e| !e.is_signed().unwrap_or(true))
.filter_map(|e| e.call().is_sub_type())
.find_map(|c| match c {
crate::Call::set_validation_data { data: validation_data } =>
Some(validation_data.clone()),
_ => None,
})
.expect("Could not find `set_validation_data` inherent");

run_with_externalities::<B, _, _>(&backend, || {
let relay_chain_proof = crate::RelayChainStateProof::new(
PSC::SelfParaId::get(),
Expand All @@ -153,34 +184,74 @@ where
});

run_with_externalities::<B, _, _>(&backend, || {
super::set_and_run_with_validation_params(params, || {
E::execute_block(block);

let new_validation_code = crate::NewValidationCode::<PSC>::get();
let upward_messages = crate::UpwardMessages::<PSC>::get();
let processed_downward_messages = crate::ProcessedDownwardMessages::<PSC>::get();
let horizontal_messages = crate::HrmpOutboundMessages::<PSC>::get();
let hrmp_watermark = crate::HrmpWatermark::<PSC>::get();

let head_data =
if let Some(custom_head_data) = crate::CustomValidationHeadData::<PSC>::get() {
HeadData(custom_head_data)
} else {
head_data
};

ValidationResult {
head_data,
new_validation_code: new_validation_code.map(Into::into),
upward_messages,
processed_downward_messages,
horizontal_messages,
hrmp_watermark,
}
})
E::execute_block(block);

let new_validation_code = crate::NewValidationCode::<PSC>::get();
let upward_messages = crate::UpwardMessages::<PSC>::get();
let processed_downward_messages = crate::ProcessedDownwardMessages::<PSC>::get();
let horizontal_messages = crate::HrmpOutboundMessages::<PSC>::get();
let hrmp_watermark = crate::HrmpWatermark::<PSC>::get();

let head_data =
if let Some(custom_head_data) = crate::CustomValidationHeadData::<PSC>::get() {
HeadData(custom_head_data)
} else {
head_data
};

ValidationResult {
head_data,
new_validation_code: new_validation_code.map(Into::into),
upward_messages,
processed_downward_messages,
horizontal_messages,
hrmp_watermark,
}
})
}

/// Extract the [`ParachainInherentData`].
fn extract_parachain_inherent_data<B: BlockT, PSC: crate::Config>(
block: &B,
) -> &ParachainInherentData
where
B::Extrinsic: ExtrinsicCall,
<B::Extrinsic as Extrinsic>::Call: IsSubType<crate::Call<PSC>>,
{
block
.extrinsics()
.iter()
// Inherents are at the front of the block and are unsigned.
//
// If `is_signed` is returning `None`, we keep it safe and assume that it is "signed".
// We are searching for unsigned transactions anyway.
.take_while(|e| !e.is_signed().unwrap_or(true))
.filter_map(|e| e.call().is_sub_type())
.find_map(|c| match c {
crate::Call::set_validation_data { data: validation_data } => Some(validation_data),
_ => None,
})
.expect("Could not find `set_validation_data` inherent")
}

/// Validate the given [`PersistedValidationData`] against the [`ValidationParams`].
fn validate_validation_data(
validation_data: &PersistedValidationData,
relay_parent_number: RelayChainBlockNumber,
relay_parent_storage_root: RHash,
parent_head: bytes::Bytes,
) {
assert_eq!(parent_head, validation_data.parent_head.0, "Parent head doesn't match");
assert_eq!(
relay_parent_number, validation_data.relay_parent_number,
"Relay parent number doesn't match",
);
assert_eq!(
relay_parent_storage_root, validation_data.relay_parent_storage_root,
"Relay parent storage root doesn't match",
);
}

/// Run the given closure with the externalities set.
fn run_with_externalities<B: BlockT, R, F: FnOnce() -> R>(
backend: &TrieBackend<B>,
Expand Down
43 changes: 25 additions & 18 deletions pallets/parachain-system/src/validate_block/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,43 @@

//! A module that enables a runtime to work as parachain.

use polkadot_parachain::primitives::ValidationParams;

#[cfg(not(feature = "std"))]
#[doc(hidden)]
pub mod implementation;
#[cfg(test)]
mod tests;

#[cfg(not(feature = "std"))]
#[doc(hidden)]
pub use bytes;
#[cfg(not(feature = "std"))]
#[doc(hidden)]
pub use codec::decode_from_bytes;
#[cfg(not(feature = "std"))]
#[doc(hidden)]
pub use polkadot_parachain;
#[cfg(not(feature = "std"))]
#[doc(hidden)]
pub use sp_runtime::traits::GetRuntimeBlockType;
#[cfg(not(feature = "std"))]
#[doc(hidden)]
pub use sp_std;

// Stores the [`ValidationParams`] that are being passed to `validate_block`.
//
// This value will only be set when a parachain validator validates a given `PoV`.
environmental::environmental!(VALIDATION_PARAMS: ValidationParams);

/// Execute the given closure with the [`ValidationParams`].
/// Basically the same as [`ValidationParams`](polkadot_parachain::primitives::ValidationParams),
/// but a little bit optimized for our use case here.
///
/// Returns `None` if the [`ValidationParams`] are not set, because the code is currently not being
/// executed in the context of `validate_block`.
pub(crate) fn with_validation_params<R>(f: impl FnOnce(&ValidationParams) -> R) -> Option<R> {
VALIDATION_PARAMS::with(|v| f(v))
}

/// Set the [`ValidationParams`] for the local context and execute the given closure in this context.
#[cfg(not(feature = "std"))]
fn set_and_run_with_validation_params<R>(mut params: ValidationParams, f: impl FnOnce() -> R) -> R {
VALIDATION_PARAMS::using(&mut params, f)
/// `block_data` and `head_data` are represented as [`bytes::Bytes`] to make them reuse
/// the memory of the input parameter of the exported `validate_blocks` function.
///
/// The layout of this type must match exactly the layout of
/// [`ValidationParams`](polkadot_parachain::primitives::ValidationParams) to have the same
/// SCALE encoding.
#[derive(codec::Decode)]
#[cfg_attr(feature = "std", derive(codec::Encode))]
#[doc(hidden)]
pub struct MemoryOptimizedValidationParams {
koute marked this conversation as resolved.
Show resolved Hide resolved
pub parent_head: bytes::Bytes,
pub block_data: bytes::Bytes,
pub relay_parent_number: cumulus_primitives_core::relay_chain::v2::BlockNumber,
pub relay_parent_storage_root: cumulus_primitives_core::relay_chain::v2::Hash,
}
Loading