Refactoring Checkpoint: (WIP)

This commit is contained in:
2025-12-14 10:29:31 +03:00
parent 09735eb97a
commit c89d7cac55
1424 changed files with 6415 additions and 6064 deletions
@@ -0,0 +1,550 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! The actual implementation of the validate block functionality.
use super::{trie_cache, trie_recorder, MemoryOptimizedValidationParams};
use alloc::vec::Vec;
use codec::{Decode, Encode};
use pezcumulus_primitives_core::{
relay_chain::{BlockNumber as RNumber, Hash as RHash, UMPSignal, UMP_SEPARATOR},
ClaimQueueOffset, CoreSelector, PersistedValidationData, TeyrchainBlockData,
};
use pezframe_support::{
traits::{ExecuteBlock, Get, IsSubType},
BoundedVec,
};
use pezkuwi_teyrchain_primitives::primitives::{HeadData, ValidationResult};
use pezsp_core::storage::{well_known_keys, ChildInfo, StateVersion};
use pezsp_externalities::{set_and_run_with_externalities, Externalities};
use pezsp_io::{hashing::blake2_128, KillStorageResult};
use pezsp_runtime::traits::{
Block as BlockT, ExtrinsicCall, Hash as HashT, HashingFor, Header as HeaderT, LazyBlock,
};
use pezsp_state_machine::OverlayedChanges;
use pezsp_trie::{HashDBT, ProofSizeProvider, EMPTY_PREFIX};
use trie_recorder::{SeenNodes, SizeOnlyRecorderProvider};
type Ext<'a, Block, Backend> = pezsp_state_machine::Ext<'a, HashingFor<Block>, Backend>;
fn with_externalities<F: FnOnce(&mut dyn Externalities) -> R, R>(f: F) -> R {
pezsp_externalities::with_externalities(f).expect("Environmental externalities not set.")
}
// Recorder instance to be used during this validate_block call.
environmental::environmental!(recorder: trait ProofSizeProvider);
/// Validate the given teyrchain block.
///
/// This function is doing roughly the following:
///
/// 1. We decode the [`TeyrchainBlockData`] 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 teyrchain
/// that we require to verify the block.
///
/// 5. 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>(
MemoryOptimizedValidationParams {
block_data,
parent_head: teyrchain_head,
relay_parent_number,
relay_parent_storage_root,
}: MemoryOptimizedValidationParams,
) -> ValidationResult
where
B::Extrinsic: ExtrinsicCall,
<B::Extrinsic as ExtrinsicCall>::Call: IsSubType<crate::Call<PSC>>,
{
let _guard = (
// Replace storage calls with our own implementations
pezsp_io::storage::host_read.replace_implementation(host_storage_read),
pezsp_io::storage::host_set.replace_implementation(host_storage_set),
pezsp_io::storage::host_get.replace_implementation(host_storage_get),
pezsp_io::storage::host_exists.replace_implementation(host_storage_exists),
pezsp_io::storage::host_clear.replace_implementation(host_storage_clear),
pezsp_io::storage::host_root.replace_implementation(host_storage_root),
pezsp_io::storage::host_clear_prefix.replace_implementation(host_storage_clear_prefix),
pezsp_io::storage::host_append.replace_implementation(host_storage_append),
pezsp_io::storage::host_next_key.replace_implementation(host_storage_next_key),
pezsp_io::storage::host_start_transaction
.replace_implementation(host_storage_start_transaction),
pezsp_io::storage::host_rollback_transaction
.replace_implementation(host_storage_rollback_transaction),
pezsp_io::storage::host_commit_transaction
.replace_implementation(host_storage_commit_transaction),
pezsp_io::default_child_storage::host_get
.replace_implementation(host_default_child_storage_get),
pezsp_io::default_child_storage::host_read
.replace_implementation(host_default_child_storage_read),
pezsp_io::default_child_storage::host_set
.replace_implementation(host_default_child_storage_set),
pezsp_io::default_child_storage::host_clear
.replace_implementation(host_default_child_storage_clear),
pezsp_io::default_child_storage::host_storage_kill
.replace_implementation(host_default_child_storage_storage_kill),
pezsp_io::default_child_storage::host_exists
.replace_implementation(host_default_child_storage_exists),
pezsp_io::default_child_storage::host_clear_prefix
.replace_implementation(host_default_child_storage_clear_prefix),
pezsp_io::default_child_storage::host_root
.replace_implementation(host_default_child_storage_root),
pezsp_io::default_child_storage::host_next_key
.replace_implementation(host_default_child_storage_next_key),
pezsp_io::offchain_index::host_set.replace_implementation(host_offchain_index_set),
pezsp_io::offchain_index::host_clear.replace_implementation(host_offchain_index_clear),
pezcumulus_primitives_proof_size_hostfunction::storage_proof_size::host_storage_proof_size
.replace_implementation(host_storage_proof_size),
);
let block_data = codec::decode_from_bytes::<TeyrchainBlockData<B::LazyBlock>>(block_data)
.expect("Invalid teyrchain block data");
// Initialize hashmaps randomness.
pezsp_trie::add_extra_randomness(build_seed_from_head_data::<B>(
&block_data,
relay_parent_storage_root,
));
let mut parent_header =
codec::decode_from_bytes::<B::Header>(teyrchain_head.clone()).expect("Invalid parent head");
let (blocks, proof) = block_data.into_inner();
assert_eq!(
*blocks
.first()
.expect("BlockData should have at least one block")
.header()
.parent_hash(),
parent_header.hash(),
"Teyrchain head needs to be the parent of the first block"
);
blocks.iter().fold(parent_header.hash(), |p, b| {
assert_eq!(
p,
*b.header().parent_hash(),
"Not a valid chain of blocks :(; {:?} not a parent of {:?}?",
array_bytes::bytes2hex("0x", p.as_ref()),
array_bytes::bytes2hex("0x", b.header().parent_hash().as_ref()),
);
b.header().hash()
});
let mut processed_downward_messages = 0;
let mut upward_messages = BoundedVec::default();
let mut upward_message_signals = Vec::<Vec<_>>::new();
let mut horizontal_messages = BoundedVec::default();
let mut hrmp_watermark = Default::default();
let mut head_data = None;
let mut new_validation_code = None;
let num_blocks = blocks.len();
// Create the db
let mut db = match proof.to_memory_db(Some(parent_header.state_root())) {
Ok((db, _)) => db,
Err(_) => panic!("Compact proof decoding failure."),
};
core::mem::drop(proof);
let cache_provider = trie_cache::CacheProvider::new();
let seen_nodes = SeenNodes::<HashingFor<B>>::default();
for (block_index, mut block) in blocks.into_iter().enumerate() {
// 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 = pezsp_state_machine::TrieBackendBuilder::new_with_cache(
&db,
*parent_header.state_root(),
&cache_provider,
)
.build();
// We use the same recorder when executing all blocks. So, each node only contributes once
// to the total size of the storage proof. This recorder should only be used for
// `execute_block`.
let mut execute_recorder = SizeOnlyRecorderProvider::with_seen_nodes(seen_nodes.clone());
// `backend` with the `execute_recorder`. As the `execute_recorder`, this should only be
// used for `execute_block`.
let execute_backend = pezsp_state_machine::TrieBackendBuilder::wrap(&backend)
.with_recorder(execute_recorder.clone())
.build();
// We let all blocks contribute to the same overlay. Data written by a previous block will
// be directly accessible without going to the db.
let mut overlay = OverlayedChanges::default();
parent_header = block.header().clone();
run_with_externalities_and_recorder::<B, _, _>(
&backend,
&mut Default::default(),
&mut Default::default(),
|| {
E::verify_and_remove_seal(&mut block);
},
);
run_with_externalities_and_recorder::<B, _, _>(
&execute_backend,
// Here is the only place where we want to use the recorder.
// We want to ensure that we not accidentally read something from the proof, that
// was not yet read and thus, alter the proof size. Otherwise, we end up with
// mismatches in later blocks.
&mut execute_recorder,
&mut overlay,
|| {
E::execute_verified_block(block);
},
);
if overlay.storage(well_known_keys::CODE).is_some() && num_blocks > 1 {
panic!("When applying a runtime upgrade, only one block per PoV is allowed. Received {num_blocks}.")
}
run_with_externalities_and_recorder::<B, _, _>(
&backend,
&mut Default::default(),
// We are only reading here, but need to know what the old block has written. Thus, we
// are passing here the overlay.
&mut overlay,
|| {
// Ensure the validation data is correct.
validate_validation_data(
crate::ValidationData::<PSC>::get()
.expect("`ValidationData` must be set after executing a block; qed"),
&teyrchain_head,
relay_parent_number,
relay_parent_storage_root,
);
new_validation_code =
new_validation_code.take().or(crate::NewValidationCode::<PSC>::get());
let mut found_separator = false;
crate::UpwardMessages::<PSC>::get()
.into_iter()
.filter_map(|m| {
// Filter out the `UMP_SEPARATOR` and the `UMPSignals`.
if m == UMP_SEPARATOR {
found_separator = true;
None
} else if found_separator {
if upward_message_signals.iter().all(|s| *s != m) {
upward_message_signals.push(m);
}
None
} else {
// No signal or separator
Some(m)
}
})
.for_each(|m| {
upward_messages.try_push(m)
.expect(
"Number of upward messages should not be greater than `MAX_UPWARD_MESSAGE_NUM`",
)
});
processed_downward_messages += crate::ProcessedDownwardMessages::<PSC>::get();
horizontal_messages.try_extend(crate::HrmpOutboundMessages::<PSC>::get().into_iter()).expect(
"Number of horizontal messages should not be greater than `MAX_HORIZONTAL_MESSAGE_NUM`",
);
hrmp_watermark = crate::HrmpWatermark::<PSC>::get();
if block_index + 1 == num_blocks {
head_data = Some(
crate::CustomValidationHeadData::<PSC>::get()
.map_or_else(|| HeadData(parent_header.encode()), HeadData),
);
}
},
);
if block_index + 1 != num_blocks {
let mut changes = overlay
.drain_storage_changes(
&backend,
<PSC as pezframe_system::Config>::Version::get().state_version(),
)
.expect("Failed to get drain storage changes from the overlay.");
drop(backend);
// We just forward the changes directly to our db.
changes.transaction.drain().into_iter().for_each(|(_, (value, count))| {
// We only care about inserts and not deletes.
if count > 0 {
db.insert(EMPTY_PREFIX, &value);
let hash = HashingFor::<B>::hash(&value);
seen_nodes.borrow_mut().insert(hash);
}
});
}
}
if !upward_message_signals.is_empty() {
let mut selected_core: Option<(CoreSelector, ClaimQueueOffset)> = None;
let mut approved_peer = None;
upward_message_signals.iter().for_each(|s| {
match UMPSignal::decode(&mut &s[..]).expect("Failed to decode `UMPSignal`") {
UMPSignal::SelectCore(selector, offset) => match &selected_core {
Some(selected_core) if *selected_core != (selector, offset) => {
panic!(
"All `SelectCore` signals need to select the same core: {selected_core:?} vs {:?}",
(selector, offset),
)
},
Some(_) => {},
None => {
selected_core = Some((selector, offset));
},
},
UMPSignal::ApprovedPeer(new_approved_peer) => match &approved_peer {
Some(approved_peer) if *approved_peer != new_approved_peer => {
panic!(
"All `ApprovedPeer` signals need to select the same peer_id: {new_approved_peer:?} vs {approved_peer:?}",
)
},
Some(_) => {},
None => {
approved_peer = Some(new_approved_peer);
},
},
}
});
upward_messages
.try_push(UMP_SEPARATOR)
.expect("UMPSignals does not fit in UMPMessages");
upward_messages
.try_extend(upward_message_signals.into_iter())
.expect("UMPSignals does not fit in UMPMessages");
}
ValidationResult {
head_data: head_data.expect("HeadData not set"),
new_validation_code: new_validation_code.map(Into::into),
upward_messages,
processed_downward_messages,
horizontal_messages,
hrmp_watermark,
}
}
/// Validates the given [`PersistedValidationData`] against the data from the relay chain.
fn validate_validation_data(
validation_data: PersistedValidationData,
parent_header: &[u8],
relay_parent_number: RNumber,
relay_parent_storage_root: RHash,
) {
assert_eq!(parent_header, &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",
);
}
/// Build a seed from the head data of the teyrchain block.
///
/// Uses both the relay parent storage root and the hash of the blocks
/// in the block data, to make sure the seed changes every block and that
/// the user cannot find about it ahead of time.
fn build_seed_from_head_data<B: BlockT>(
block_data: &TeyrchainBlockData<B::LazyBlock>,
relay_parent_storage_root: crate::relay_chain::Hash,
) -> [u8; 16] {
let mut bytes_to_hash = Vec::with_capacity(
block_data.blocks().len() * size_of::<B::Hash>() + size_of::<crate::relay_chain::Hash>(),
);
bytes_to_hash.extend_from_slice(relay_parent_storage_root.as_ref());
block_data.blocks().iter().for_each(|block| {
bytes_to_hash.extend_from_slice(block.header().hash().as_ref());
});
blake2_128(&bytes_to_hash)
}
/// Run the given closure with the externalities and recorder set.
fn run_with_externalities_and_recorder<Block: BlockT, R, F: FnOnce() -> R>(
backend: &impl pezsp_state_machine::Backend<HashingFor<Block>>,
recorder: &mut SizeOnlyRecorderProvider<HashingFor<Block>>,
overlay: &mut OverlayedChanges<HashingFor<Block>>,
execute: F,
) -> R {
let mut ext = Ext::<Block, _>::new(overlay, backend);
recorder::using(recorder, || set_and_run_with_externalities(&mut ext, || execute()))
}
fn host_storage_read(key: &[u8], value_out: &mut [u8], value_offset: u32) -> Option<u32> {
match with_externalities(|ext| ext.storage(key)) {
Some(value) => {
let value_offset = value_offset as usize;
let data = &value[value_offset.min(value.len())..];
let written = core::cmp::min(data.len(), value_out.len());
value_out[..written].copy_from_slice(&data[..written]);
Some(value.len() as u32)
},
None => None,
}
}
fn host_storage_set(key: &[u8], value: &[u8]) {
with_externalities(|ext| ext.place_storage(key.to_vec(), Some(value.to_vec())))
}
fn host_storage_get(key: &[u8]) -> Option<bytes::Bytes> {
with_externalities(|ext| ext.storage(key).map(|value| value.into()))
}
fn host_storage_exists(key: &[u8]) -> bool {
with_externalities(|ext| ext.exists_storage(key))
}
fn host_storage_clear(key: &[u8]) {
with_externalities(|ext| ext.place_storage(key.to_vec(), None))
}
fn host_storage_proof_size() -> u64 {
recorder::with(|rec| rec.estimate_encoded_size()).expect("Recorder is always set; qed") as _
}
fn host_storage_root(version: StateVersion) -> Vec<u8> {
with_externalities(|ext| ext.storage_root(version))
}
fn host_storage_clear_prefix(prefix: &[u8], limit: Option<u32>) -> KillStorageResult {
with_externalities(|ext| ext.clear_prefix(prefix, limit, None).into())
}
fn host_storage_append(key: &[u8], value: Vec<u8>) {
with_externalities(|ext| ext.storage_append(key.to_vec(), value))
}
fn host_storage_next_key(key: &[u8]) -> Option<Vec<u8>> {
with_externalities(|ext| ext.next_storage_key(key))
}
fn host_storage_start_transaction() {
with_externalities(|ext| ext.storage_start_transaction())
}
fn host_storage_rollback_transaction() {
with_externalities(|ext| ext.storage_rollback_transaction().ok())
.expect("No open transaction that can be rolled back.");
}
fn host_storage_commit_transaction() {
with_externalities(|ext| ext.storage_commit_transaction().ok())
.expect("No open transaction that can be committed.");
}
fn host_default_child_storage_get(storage_key: &[u8], key: &[u8]) -> Option<Vec<u8>> {
let child_info = ChildInfo::new_default(storage_key);
with_externalities(|ext| ext.child_storage(&child_info, key))
}
fn host_default_child_storage_read(
storage_key: &[u8],
key: &[u8],
value_out: &mut [u8],
value_offset: u32,
) -> Option<u32> {
let child_info = ChildInfo::new_default(storage_key);
match with_externalities(|ext| ext.child_storage(&child_info, key)) {
Some(value) => {
let value_offset = value_offset as usize;
let data = &value[value_offset.min(value.len())..];
let written = core::cmp::min(data.len(), value_out.len());
value_out[..written].copy_from_slice(&data[..written]);
Some(value.len() as u32)
},
None => None,
}
}
fn host_default_child_storage_set(storage_key: &[u8], key: &[u8], value: &[u8]) {
let child_info = ChildInfo::new_default(storage_key);
with_externalities(|ext| {
ext.place_child_storage(&child_info, key.to_vec(), Some(value.to_vec()))
})
}
fn host_default_child_storage_clear(storage_key: &[u8], key: &[u8]) {
let child_info = ChildInfo::new_default(storage_key);
with_externalities(|ext| ext.place_child_storage(&child_info, key.to_vec(), None))
}
fn host_default_child_storage_storage_kill(
storage_key: &[u8],
limit: Option<u32>,
) -> KillStorageResult {
let child_info = ChildInfo::new_default(storage_key);
with_externalities(|ext| ext.kill_child_storage(&child_info, limit, None).into())
}
fn host_default_child_storage_exists(storage_key: &[u8], key: &[u8]) -> bool {
let child_info = ChildInfo::new_default(storage_key);
with_externalities(|ext| ext.exists_child_storage(&child_info, key))
}
fn host_default_child_storage_clear_prefix(
storage_key: &[u8],
prefix: &[u8],
limit: Option<u32>,
) -> KillStorageResult {
let child_info = ChildInfo::new_default(storage_key);
with_externalities(|ext| ext.clear_child_prefix(&child_info, prefix, limit, None).into())
}
fn host_default_child_storage_root(storage_key: &[u8], version: StateVersion) -> Vec<u8> {
let child_info = ChildInfo::new_default(storage_key);
with_externalities(|ext| ext.child_storage_root(&child_info, version))
}
fn host_default_child_storage_next_key(storage_key: &[u8], key: &[u8]) -> Option<Vec<u8>> {
let child_info = ChildInfo::new_default(storage_key);
with_externalities(|ext| ext.next_child_storage_key(&child_info, key))
}
fn host_offchain_index_set(_key: &[u8], _value: &[u8]) {}
fn host_offchain_index_clear(_key: &[u8]) {}
@@ -0,0 +1,70 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! A module that enables a runtime to work as teyrchain.
#[cfg(not(feature = "std"))]
#[doc(hidden)]
pub mod implementation;
#[cfg(test)]
mod tests;
#[cfg(not(feature = "std"))]
#[doc(hidden)]
pub mod trie_cache;
#[cfg(any(test, not(feature = "std")))]
#[doc(hidden)]
pub mod trie_recorder;
#[cfg(not(feature = "std"))]
#[doc(hidden)]
pub use alloc::{boxed::Box, slice};
#[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 pezkuwi_teyrchain_primitives;
#[cfg(not(feature = "std"))]
#[doc(hidden)]
pub use pezsp_runtime::traits::GetRuntimeBlockType;
#[cfg(not(feature = "std"))]
#[doc(hidden)]
pub use pezsp_std;
/// Basically the same as
/// [`ValidationParams`](pezkuwi_teyrchain_primitives::primitives::ValidationParams), but a little
/// bit optimized for our use case here.
///
/// `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`](pezkuwi_teyrchain_primitives::primitives::ValidationParams) to have the
/// same SCALE encoding.
#[derive(codec::Decode)]
#[cfg_attr(feature = "std", derive(codec::Encode))]
#[doc(hidden)]
pub struct MemoryOptimizedValidationParams {
pub parent_head: bytes::Bytes,
pub block_data: bytes::Bytes,
pub relay_parent_number: pezcumulus_primitives_core::relay_chain::BlockNumber,
pub relay_parent_storage_root: pezcumulus_primitives_core::relay_chain::Hash,
}
@@ -0,0 +1,777 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::{validate_block::MemoryOptimizedValidationParams, *};
use codec::{Decode, DecodeAll, Encode};
use pezcumulus_primitives_core::{relay_chain, PersistedValidationData, TeyrchainBlockData};
use pezcumulus_test_client::{
generate_extrinsic, generate_extrinsic_with_pair,
runtime::{
self as test_runtime, Block, Hash, Header, SudoCall, SystemCall, TestPalletCall,
UncheckedExtrinsic, WASM_BINARY,
},
seal_block, transfer, BlockData, BlockOrigin, BuildTeyrchainBlockData, Client,
DefaultTestClientBuilderExt, HeadData, InitBlockBuilder,
Sr25519Keyring::{Alice, Bob, Charlie},
TestClientBuilder, TestClientBuilderExt, ValidationParams,
};
use pezcumulus_test_relay_sproof_builder::RelayStateSproofBuilder;
use pezkuwi_teyrchain_primitives::primitives::ValidationResult;
use pezsc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy};
use pezsp_api::{ApiExt, Core, ProofRecorder, ProvideRuntimeApi};
use pezsp_consensus_slots::SlotDuration;
use pezsp_core::{Hasher, H256};
use pezsp_runtime::{
traits::{BlakeTwo256, Block as BlockT, Header as HeaderT},
DigestItem,
};
use pezsp_trie::{proof_size_extension::ProofSizeExt, recorder::IgnoredNodes, StorageProof};
use std::{env, process::Command};
fn call_validate_block_validation_result(
validation_code: &[u8],
parent_head: Header,
block_data: TeyrchainBlockData<Block>,
relay_parent_storage_root: Hash,
) -> pezcumulus_test_client::ExecutorResult<ValidationResult> {
pezcumulus_test_client::validate_block(
ValidationParams {
block_data: BlockData(block_data.encode()),
parent_head: HeadData(parent_head.encode()),
relay_parent_number: 1,
relay_parent_storage_root,
},
validation_code,
)
}
fn call_validate_block(
parent_head: Header,
block_data: TeyrchainBlockData<Block>,
relay_parent_storage_root: Hash,
) -> pezcumulus_test_client::ExecutorResult<Header> {
call_validate_block_validation_result(
WASM_BINARY.expect("You need to build the WASM binaries to run the tests!"),
parent_head,
block_data,
relay_parent_storage_root,
)
.map(|v| Header::decode(&mut &v.head_data.0[..]).expect("Decodes `Header`."))
}
/// Call `validate_block` in the runtime with `elastic-scaling` activated.
fn call_validate_block_elastic_scaling(
parent_head: Header,
block_data: TeyrchainBlockData<Block>,
relay_parent_storage_root: Hash,
) -> pezcumulus_test_client::ExecutorResult<Header> {
call_validate_block_validation_result(
test_runtime::elastic_scaling_500ms::WASM_BINARY
.expect("You need to build the WASM binaries to run the tests!"),
parent_head,
block_data,
relay_parent_storage_root,
)
.map(|v| Header::decode(&mut &v.head_data.0[..]).expect("Decodes `Header`."))
}
fn create_test_client() -> (Client, Header) {
let client = TestClientBuilder::new().enable_import_proof_recording().build();
let genesis_header = client
.header(client.chain_info().genesis_hash)
.ok()
.flatten()
.expect("Genesis header exists; qed");
(client, genesis_header)
}
/// Create test client using the runtime with `elastic-scaling` feature enabled.
fn create_elastic_scaling_test_client() -> (Client, Header) {
let mut builder = TestClientBuilder::new();
builder.genesis_init_mut().wasm = Some(
test_runtime::elastic_scaling_500ms::WASM_BINARY
.expect("You need to build the WASM binaries to run the tests!")
.to_vec(),
);
let client = builder.enable_import_proof_recording().build();
let genesis_header = client
.header(client.chain_info().genesis_hash)
.ok()
.flatten()
.expect("Genesis header exists; qed");
(client, genesis_header)
}
fn pop_seal(mut block: Block) -> Block {
assert!(block.header.digest.pop().unwrap().as_seal().is_some());
block
}
struct TestBlockData {
block: TeyrchainBlockData<Block>,
validation_data: PersistedValidationData,
}
fn build_block_with_witness(
client: &Client,
extra_extrinsics: Vec<UncheckedExtrinsic>,
parent_head: Header,
mut sproof_builder: RelayStateSproofBuilder,
pre_digests: Vec<DigestItem>,
) -> TestBlockData {
sproof_builder.para_id = test_runtime::TEYRCHAIN_ID.into();
sproof_builder.included_para_head = Some(HeadData(parent_head.encode()));
let validation_data = PersistedValidationData {
relay_parent_number: 1,
parent_head: parent_head.encode().into(),
..Default::default()
};
let pezcumulus_test_client::BlockBuilderAndSupportData {
mut block_builder,
persisted_validation_data,
} = client.init_block_builder_with_pre_digests(Some(validation_data), sproof_builder, pre_digests);
extra_extrinsics.into_iter().for_each(|e| block_builder.push(e).unwrap());
let mut block = block_builder.build_teyrchain_block(*parent_head.state_root());
block.blocks_mut()[0] = seal_block(block.blocks()[0].clone(), client);
TestBlockData { block, validation_data: persisted_validation_data }
}
fn build_multiple_blocks_with_witness(
client: &Client,
mut parent_head: Header,
mut sproof_builder: RelayStateSproofBuilder,
num_blocks: u32,
extra_extrinsics: impl Fn(u32) -> Vec<UncheckedExtrinsic>,
) -> TestBlockData {
let parent_head_root = *parent_head.state_root();
sproof_builder.para_id = test_runtime::TEYRCHAIN_ID.into();
sproof_builder.included_para_head = Some(HeadData(parent_head.encode()));
let timestamp = if sproof_builder.current_slot == 0u64 {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.expect("Time is always after UNIX_EPOCH; qed")
.as_millis() as u64;
sproof_builder.current_slot = (timestamp / 6000).into();
timestamp
} else {
sproof_builder
.current_slot
.timestamp(SlotDuration::from_millis(6000))
.unwrap()
.as_millis()
};
let validation_data = PersistedValidationData {
relay_parent_number: 1,
parent_head: parent_head.encode().into(),
..Default::default()
};
let mut persisted_validation_data = None;
let mut blocks = Vec::new();
let mut proof = StorageProof::empty();
let mut ignored_nodes = IgnoredNodes::<H256>::default();
for i in 0..num_blocks {
let pezcumulus_test_client::BlockBuilderAndSupportData {
mut block_builder,
persisted_validation_data: p_v_data,
} = client.init_block_builder_with_ignored_nodes(
parent_head.hash(),
Some(validation_data.clone()),
sproof_builder.clone(),
timestamp,
ignored_nodes.clone(),
);
persisted_validation_data = Some(p_v_data);
for ext in (extra_extrinsics)(i) {
block_builder.push(ext).unwrap();
}
let mut built_block = block_builder.build().unwrap();
built_block.block = seal_block(built_block.block, &client);
futures::executor::block_on({
let parent_hash = *built_block.block.header.parent_hash();
let state = client.state_at(parent_hash).unwrap();
let mut api = client.runtime_api();
let proof_recorder = ProofRecorder::<Block>::with_ignored_nodes(ignored_nodes.clone());
api.record_proof_with_recorder(proof_recorder.clone());
api.register_extension(ProofSizeExt::new(proof_recorder));
api.execute_block(parent_hash, pop_seal(built_block.block.clone()).into())
.unwrap();
let (mut header, extrinsics) = built_block.block.clone().deconstruct();
let seal = header.digest.pop().unwrap();
let mut import = BlockImportParams::new(BlockOrigin::Own, header);
import.body = Some(extrinsics);
import.post_digests.push(seal);
import.fork_choice = Some(ForkChoiceStrategy::Custom(true));
import.state_action = api.into_storage_changes(&state, parent_hash).unwrap().into();
BlockImport::import_block(&client, import)
})
.unwrap();
ignored_nodes.extend(IgnoredNodes::from_storage_proof::<BlakeTwo256>(
&built_block.proof.clone().unwrap(),
));
ignored_nodes.extend(IgnoredNodes::from_memory_db(built_block.storage_changes.transaction));
proof = StorageProof::merge([proof, built_block.proof.unwrap()]);
parent_head = built_block.block.header.clone();
blocks.push(built_block.block);
}
let proof = proof.into_compact_proof::<BlakeTwo256>(parent_head_root).unwrap();
TestBlockData {
block: TeyrchainBlockData::new(blocks, proof),
validation_data: persisted_validation_data.unwrap(),
}
}
#[test]
fn validate_block_works() {
pezsp_tracing::try_init_simple();
let (client, parent_head) = create_test_client();
let TestBlockData { block, validation_data } = build_block_with_witness(
&client,
Vec::new(),
parent_head.clone(),
Default::default(),
Default::default(),
);
let header = block.blocks()[0].header().clone();
let res_header =
call_validate_block(parent_head, block, validation_data.relay_parent_storage_root)
.expect("Calls `validate_block`");
assert_eq!(header, res_header);
}
#[test]
fn validate_multiple_blocks_work() {
pezsp_tracing::try_init_simple();
let blocks_per_pov = 4;
let (client, parent_head) = create_elastic_scaling_test_client();
let TestBlockData { block, validation_data } = build_multiple_blocks_with_witness(
&client,
parent_head.clone(),
Default::default(),
blocks_per_pov,
|i| {
vec![generate_extrinsic_with_pair(
&client,
Charlie.into(),
TestPalletCall::read_and_write_big_value {},
Some(i),
)]
},
);
assert!(block.proof().encoded_size() < 3 * 1024 * 1024);
let header = block.blocks().last().unwrap().header().clone();
let res_header = call_validate_block_elastic_scaling(
parent_head,
block,
validation_data.relay_parent_storage_root,
)
.expect("Calls `validate_block`");
assert_eq!(header, res_header);
}
#[test]
fn validate_block_with_extra_extrinsics() {
pezsp_tracing::try_init_simple();
let (client, parent_head) = create_test_client();
let extra_extrinsics = vec![
transfer(&client, Alice, Bob, 69),
transfer(&client, Bob, Charlie, 100),
transfer(&client, Charlie, Alice, 500),
];
let TestBlockData { block, validation_data } = build_block_with_witness(
&client,
extra_extrinsics,
parent_head.clone(),
Default::default(),
Default::default(),
);
let header = block.blocks()[0].header().clone();
let res_header =
call_validate_block(parent_head, block, validation_data.relay_parent_storage_root)
.expect("Calls `validate_block`");
assert_eq!(header, res_header);
}
#[test]
fn validate_block_returns_custom_head_data() {
pezsp_tracing::try_init_simple();
let expected_header = vec![1, 3, 3, 7, 4, 5, 6];
let (client, parent_head) = create_test_client();
let extra_extrinsics = vec![
transfer(&client, Alice, Bob, 69),
generate_extrinsic(
&client,
Charlie,
TestPalletCall::set_custom_validation_head_data {
custom_header: expected_header.clone(),
},
),
transfer(&client, Bob, Charlie, 100),
];
let TestBlockData { block, validation_data } = build_block_with_witness(
&client,
extra_extrinsics,
parent_head.clone(),
Default::default(),
Default::default(),
);
let header = block.blocks()[0].header().clone();
assert_ne!(expected_header, header.encode());
let res_header = call_validate_block_validation_result(
WASM_BINARY.expect("You need to build the WASM binaries to run the tests!"),
parent_head,
block,
validation_data.relay_parent_storage_root,
)
.expect("Calls `validate_block`")
.head_data
.0;
assert_eq!(expected_header, res_header);
}
#[test]
fn validate_block_rejects_invalid_seal() {
pezsp_tracing::try_init_simple();
if env::var("RUN_TEST").is_ok() {
let (client, parent_head) = create_test_client();
let TestBlockData { mut block, validation_data, .. } = build_block_with_witness(
&client,
Vec::new(),
parent_head.clone(),
Default::default(),
Default::default(),
);
let (id, data) =
block.blocks_mut()[0].header.digest.logs.last().unwrap().as_seal().unwrap();
let mut data = data.to_vec();
let random = BlakeTwo256::hash(&data);
data[..random.as_ref().len()].copy_from_slice(random.as_ref());
*block.blocks_mut()[0].header.digest.logs.last_mut().unwrap() = DigestItem::Seal(id, data);
call_validate_block(parent_head, block, validation_data.relay_parent_storage_root)
.unwrap_err();
} else {
let output = Command::new(env::current_exe().unwrap())
.args(["validate_block_rejects_invalid_seal", "--", "--nocapture"])
.env("RUN_TEST", "1")
.output()
.expect("Runs the test");
assert!(output.status.success());
assert!(dbg!(String::from_utf8(output.stderr).unwrap()).contains("Invalid AuRa seal"));
}
}
#[test]
fn validate_block_invalid_parent_hash() {
pezsp_tracing::try_init_simple();
if env::var("RUN_TEST").is_ok() {
let (client, parent_head) = create_test_client();
let TestBlockData { mut block, validation_data, .. } = build_block_with_witness(
&client,
Vec::new(),
parent_head.clone(),
Default::default(),
Default::default(),
);
block.blocks_mut()[0].header.set_parent_hash(Hash::from_low_u64_be(1));
call_validate_block(parent_head, block, validation_data.relay_parent_storage_root)
.unwrap_err();
} else {
let output = Command::new(env::current_exe().unwrap())
.args(["validate_block_invalid_parent_hash", "--", "--nocapture"])
.env("RUN_TEST", "1")
.output()
.expect("Runs the test");
assert!(output.status.success());
assert!(dbg!(String::from_utf8(output.stderr).unwrap())
.contains("Teyrchain head needs to be the parent of the first block"));
}
}
#[test]
fn validate_block_fails_on_invalid_validation_data() {
pezsp_tracing::try_init_simple();
if env::var("RUN_TEST").is_ok() {
let (client, parent_head) = create_test_client();
let TestBlockData { block, .. } = build_block_with_witness(
&client,
Vec::new(),
parent_head.clone(),
Default::default(),
Default::default(),
);
call_validate_block(parent_head, block, Hash::random()).unwrap_err();
} else {
let output = Command::new(env::current_exe().unwrap())
.args(["validate_block_fails_on_invalid_validation_data", "--", "--nocapture"])
.env("RUN_TEST", "1")
.output()
.expect("Runs the test");
assert!(output.status.success());
assert!(dbg!(String::from_utf8(output.stderr).unwrap())
.contains("Relay parent storage root doesn't match"));
}
}
/// Test that ensures that `ValidationParams` and `MemoryOptimizedValidationParams`
/// are encoding/decoding.
#[test]
fn validation_params_and_memory_optimized_validation_params_encode_and_decode() {
const BLOCK_DATA: &[u8] = &[1, 2, 3, 4, 5];
const PARENT_HEAD: &[u8] = &[1, 3, 4, 5, 6, 7, 9];
let validation_params = ValidationParams {
block_data: BlockData(BLOCK_DATA.encode()),
parent_head: HeadData(PARENT_HEAD.encode()),
relay_parent_number: 1,
relay_parent_storage_root: Hash::random(),
};
let encoded = validation_params.encode();
let decoded = MemoryOptimizedValidationParams::decode_all(&mut &encoded[..]).unwrap();
assert_eq!(decoded.relay_parent_number, validation_params.relay_parent_number);
assert_eq!(decoded.relay_parent_storage_root, validation_params.relay_parent_storage_root);
assert_eq!(decoded.block_data, validation_params.block_data.0);
assert_eq!(decoded.parent_head, validation_params.parent_head.0);
let encoded = decoded.encode();
let decoded = ValidationParams::decode_all(&mut &encoded[..]).unwrap();
assert_eq!(decoded, validation_params);
}
/// Test for ensuring that we are differentiating in the `validation::trie_cache` between different
/// child tries.
///
/// This is achieved by first building a block using `read_and_write_child_tries` that should set
/// the values in the child tries. In the second step we are building a second block with the same
/// extrinsic that reads the values from the child tries and it asserts that we read the correct
/// data from the state.
#[test]
fn validate_block_works_with_child_tries() {
pezsp_tracing::try_init_simple();
let (client, parent_head) = create_test_client();
let TestBlockData { block, .. } = build_block_with_witness(
&client,
vec![generate_extrinsic(&client, Charlie, TestPalletCall::read_and_write_child_tries {})],
parent_head.clone(),
Default::default(),
Default::default(),
);
let (mut header, extrinsics) = block.blocks()[0].clone().deconstruct();
let seal = header.digest.pop().unwrap();
let mut import = BlockImportParams::new(BlockOrigin::Own, header.clone());
import.body = Some(extrinsics);
import.post_digests.push(seal);
import.fork_choice = Some(ForkChoiceStrategy::Custom(true));
futures::executor::block_on(BlockImport::import_block(&client, import)).unwrap();
let parent_head = block.blocks()[0].header.clone();
let TestBlockData { block, validation_data } = build_block_with_witness(
&client,
vec![generate_extrinsic(&client, Alice, TestPalletCall::read_and_write_child_tries {})],
parent_head.clone(),
Default::default(),
Default::default(),
);
let header = block.blocks()[0].header().clone();
let res_header =
call_validate_block(parent_head, block, validation_data.relay_parent_storage_root)
.expect("Calls `validate_block`");
assert_eq!(header, res_header);
}
#[test]
fn state_changes_in_multiple_blocks_are_applied_in_exact_order() {
pezsp_tracing::try_init_simple();
let blocks_per_pov = 12;
// disable the core selection logic
let (client, genesis_head) = create_elastic_scaling_test_client();
// 1. Build the initial block that stores values in the map.
let TestBlockData { block: initial_block_data, .. } = build_block_with_witness(
&client,
vec![generate_extrinsic_with_pair(
&client,
Alice.into(),
TestPalletCall::store_values_in_map { max_key: 4095 },
Some(0),
)],
genesis_head.clone(),
RelayStateSproofBuilder { current_slot: 1.into(), ..Default::default() },
Vec::new(),
);
let initial_block = initial_block_data.blocks()[0].clone();
let (mut header, extrinsics) = initial_block.clone().deconstruct();
let seal = header.digest.pop().unwrap();
let mut import = BlockImportParams::new(BlockOrigin::Own, header.clone());
import.body = Some(extrinsics);
import.post_digests.push(seal);
import.fork_choice = Some(ForkChoiceStrategy::Custom(true));
futures::executor::block_on(BlockImport::import_block(&client, import)).unwrap();
let initial_block_header = initial_block.header().clone();
// 2. Build the PoV block that removes values from the map.
let TestBlockData { block: pov_block_data, validation_data: pov_validation_data } =
build_multiple_blocks_with_witness(
&client,
initial_block_header.clone(), // Start building PoV from the initial block's header
RelayStateSproofBuilder { current_slot: 2.into(), ..Default::default() },
blocks_per_pov,
|i| {
// Each block `i` (0-11) removes key `116 + i`.
let key_to_remove = 116 + i;
vec![generate_extrinsic_with_pair(
&client,
Bob.into(), // Use Bob to avoid nonce conflicts with Alice
TestPalletCall::remove_value_from_map { key: key_to_remove },
Some(i),
)]
},
);
// 3. Validate the PoV.
let final_pov_header = pov_block_data.blocks().last().unwrap().header().clone();
let res_header = call_validate_block_elastic_scaling(
initial_block_header, // The parent is the head of the initial block before the PoV
pov_block_data,
pov_validation_data.relay_parent_storage_root,
)
.expect("Calls `validate_block` after building the PoV");
assert_eq!(final_pov_header, res_header);
}
#[test]
fn validate_block_handles_ump_signal() {
use pezcumulus_primitives_core::{
relay_chain::{UMPSignal, UMP_SEPARATOR},
ClaimQueueOffset, CoreInfo, CoreSelector,
};
pezsp_tracing::try_init_simple();
let (client, parent_head) = create_elastic_scaling_test_client();
let extra_extrinsics =
vec![transfer(&client, Alice, Bob, 69), transfer(&client, Bob, Charlie, 100)];
let TestBlockData { block, validation_data } = build_block_with_witness(
&client,
extra_extrinsics,
parent_head.clone(),
Default::default(),
vec![CumulusDigestItem::CoreInfo(CoreInfo {
selector: CoreSelector(0),
claim_queue_offset: ClaimQueueOffset(0),
number_of_cores: 1.into(),
})
.to_digest_item()],
);
let upward_messages = call_validate_block_validation_result(
test_runtime::elastic_scaling_500ms::WASM_BINARY
.expect("You need to build the WASM binaries to run the tests!"),
parent_head,
block,
validation_data.relay_parent_storage_root,
)
.expect("Calls `validate_block`")
.upward_messages;
assert_eq!(
upward_messages,
vec![UMP_SEPARATOR, UMPSignal::SelectCore(CoreSelector(0), ClaimQueueOffset(0)).encode()]
);
}
#[test]
fn ensure_we_only_like_blockchains() {
pezsp_tracing::try_init_simple();
if env::var("RUN_TEST").is_ok() {
let (client, parent_head) = create_elastic_scaling_test_client();
let TestBlockData { mut block, validation_data } = build_multiple_blocks_with_witness(
&client,
parent_head.clone(),
Default::default(),
4,
|_| Default::default(),
);
// Reference some non existing parent.
block.blocks_mut()[2].header.parent_hash = Hash::default();
call_validate_block_elastic_scaling(
parent_head,
block,
validation_data.relay_parent_storage_root,
)
.unwrap_err();
} else {
let output = Command::new(env::current_exe().unwrap())
.args(["ensure_we_only_like_blockchains", "--", "--nocapture"])
.env("RUN_TEST", "1")
.output()
.expect("Runs the test");
assert!(output.status.success());
assert!(dbg!(String::from_utf8(output.stderr).unwrap())
.contains("Not a valid chain of blocks :("));
}
}
#[test]
fn rejects_multiple_blocks_per_pov_when_applying_runtime_upgrade() {
pezsp_tracing::try_init_simple();
if env::var("RUN_TEST").is_ok() {
let (client, genesis_head) = create_elastic_scaling_test_client();
let code = test_runtime::elastic_scaling_500ms::WASM_BINARY
.expect("You need to build the WASM binaries to run the tests!")
.to_vec();
let code_len = code.len() as u32;
let mut proof_builder =
RelayStateSproofBuilder { current_slot: 1.into(), ..Default::default() };
proof_builder.host_config.max_code_size = code_len * 2;
// Build the block that send the runtime upgrade.
let TestBlockData { block: initial_block_data, .. } = build_block_with_witness(
&client,
vec![generate_extrinsic_with_pair(
&client,
Alice.into(),
SudoCall::sudo {
call: Box::new(SystemCall::set_code_without_checks { code }.into()),
},
Some(0),
)],
genesis_head.clone(),
proof_builder,
Vec::new(),
);
let initial_block = initial_block_data.blocks()[0].clone();
let (mut header, extrinsics) = initial_block.clone().deconstruct();
let seal = header.digest.pop().unwrap();
let mut import = BlockImportParams::new(BlockOrigin::Own, header.clone());
import.body = Some(extrinsics);
import.post_digests.push(seal);
import.fork_choice = Some(ForkChoiceStrategy::Custom(true));
futures::executor::block_on(BlockImport::import_block(&client, import)).unwrap();
let initial_block_header = initial_block.header().clone();
let mut proof_builder = RelayStateSproofBuilder {
current_slot: 2.into(),
upgrade_go_ahead: Some(relay_chain::UpgradeGoAhead::GoAhead),
..Default::default()
};
proof_builder.host_config.max_code_size = code_len * 2;
// 2. Build a PoV that consists of multiple blocks.
let TestBlockData { block: pov_block_data, validation_data: pov_validation_data } =
build_multiple_blocks_with_witness(
&client,
initial_block_header.clone(), // Start building PoV from the initial block's header
proof_builder,
4,
|_| Vec::new(),
);
// 3. Validate the PoV.
call_validate_block_elastic_scaling(
initial_block_header, // The parent is the head of the initial block before the PoV
pov_block_data,
pov_validation_data.relay_parent_storage_root,
)
.unwrap_err();
} else {
let output = Command::new(env::current_exe().unwrap())
.args([
"rejects_multiple_blocks_per_pov_when_applying_runtime_upgrade",
"--",
"--nocapture",
])
.env("RUN_TEST", "1")
.output()
.expect("Runs the test");
assert!(output.status.success());
assert!(dbg!(String::from_utf8(output.stderr).unwrap())
.contains("only one block per PoV is allowed"));
}
}
@@ -0,0 +1,158 @@
// This file is part of Pezcumulus.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use alloc::boxed::Box;
use core::cell::{RefCell, RefMut};
use hashbrown::{hash_map::Entry, HashMap};
use pezsp_state_machine::TrieCacheProvider;
use pezsp_trie::{NodeCodec, RandomState};
use trie_db::{node::NodeOwned, Hasher};
/// Special purpose trie cache implementation that is able to cache an unlimited number
/// of values. To be used in `validate_block` to serve values and nodes that
/// have already been loaded and decoded from the storage proof.
pub struct TrieCache<'a, H: Hasher> {
node_cache: RefMut<'a, HashMap<H::Out, NodeOwned<H::Out>, RandomState>>,
value_cache: Option<RefMut<'a, HashMap<Box<[u8]>, trie_db::CachedValue<H::Out>, RandomState>>>,
}
impl<'a, H: Hasher> trie_db::TrieCache<NodeCodec<H>> for TrieCache<'a, H> {
fn lookup_value_for_key(&mut self, key: &[u8]) -> Option<&trie_db::CachedValue<H::Out>> {
self.value_cache.as_ref().and_then(|cache| cache.get(key))
}
fn cache_value_for_key(&mut self, key: &[u8], value: trie_db::CachedValue<H::Out>) {
self.value_cache.as_mut().and_then(|cache| cache.insert(key.into(), value));
}
fn get_or_insert_node(
&mut self,
hash: <NodeCodec<H> as trie_db::NodeCodec>::HashOut,
fetch_node: &mut dyn FnMut() -> trie_db::Result<
NodeOwned<H::Out>,
H::Out,
<NodeCodec<H> as trie_db::NodeCodec>::Error,
>,
) -> trie_db::Result<&NodeOwned<H::Out>, H::Out, <NodeCodec<H> as trie_db::NodeCodec>::Error> {
match self.node_cache.entry(hash) {
Entry::Occupied(entry) => Ok(entry.into_mut()),
Entry::Vacant(entry) => Ok(entry.insert(fetch_node()?)),
}
}
fn get_node(
&mut self,
hash: &H::Out,
) -> Option<&NodeOwned<<NodeCodec<H> as trie_db::NodeCodec>::HashOut>> {
self.node_cache.get(hash)
}
}
/// Provider of [`TrieCache`] instances.
pub struct CacheProvider<H: Hasher> {
node_cache: RefCell<HashMap<H::Out, NodeOwned<H::Out>, RandomState>>,
/// Cache: `storage_root` => `storage_key` => `value`.
///
/// One `block` can for example use multiple tries (child tries) and we need to distinguish the
/// cached (`storage_key`, `value`) between them. For this we are using the `storage_root` to
/// distinguish them (even if the storage root is the same for two child tries, it just means
/// that both are exactly the same trie and there would happen no collision).
value_cache: RefCell<
HashMap<H::Out, HashMap<Box<[u8]>, trie_db::CachedValue<H::Out>, RandomState>, RandomState>,
>,
}
impl<H: Hasher> CacheProvider<H> {
/// Constructs a new instance of [`CacheProvider`] with an uninitialized state
/// and empty node and value caches.
pub fn new() -> Self {
CacheProvider { node_cache: Default::default(), value_cache: Default::default() }
}
}
impl<H: Hasher> TrieCacheProvider<H> for &&CacheProvider<H> {
type Cache<'a>
= TrieCache<'a, H>
where
Self: 'a,
H: 'a;
fn as_trie_db_cache(&self, storage_root: <H as Hasher>::Out) -> Self::Cache<'_> {
TrieCacheProvider::<H>::as_trie_db_cache(**self, storage_root)
}
fn as_trie_db_mut_cache(&self) -> Self::Cache<'_> {
TrieCacheProvider::<H>::as_trie_db_mut_cache(**self)
}
fn merge<'a>(&'a self, other: Self::Cache<'a>, new_root: <H as Hasher>::Out) {
TrieCacheProvider::merge(**self, other, new_root)
}
}
impl<H: Hasher> TrieCacheProvider<H> for &CacheProvider<H> {
type Cache<'a>
= TrieCache<'a, H>
where
Self: 'a,
H: 'a;
fn as_trie_db_cache(&self, storage_root: <H as Hasher>::Out) -> Self::Cache<'_> {
TrieCacheProvider::<H>::as_trie_db_cache(*self, storage_root)
}
fn as_trie_db_mut_cache(&self) -> Self::Cache<'_> {
TrieCacheProvider::<H>::as_trie_db_mut_cache(*self)
}
fn merge<'a>(&'a self, other: Self::Cache<'a>, new_root: <H as Hasher>::Out) {
TrieCacheProvider::merge(*self, other, new_root)
}
}
impl<H: Hasher> TrieCacheProvider<H> for CacheProvider<H> {
type Cache<'a>
= TrieCache<'a, H>
where
H: 'a;
fn as_trie_db_cache(&self, storage_root: <H as Hasher>::Out) -> Self::Cache<'_> {
TrieCache {
value_cache: Some(RefMut::map(self.value_cache.borrow_mut(), |c| {
c.entry(storage_root).or_default()
})),
node_cache: self.node_cache.borrow_mut(),
}
}
fn as_trie_db_mut_cache(&self) -> Self::Cache<'_> {
// This method is called when we calculate the storage root.
// We are not interested in caching new values (as we would throw them away directly after a
// block is validated) and thus, we don't pass any `value_cache`.
TrieCache { value_cache: None, node_cache: self.node_cache.borrow_mut() }
}
fn merge<'a>(&'a self, _other: Self::Cache<'a>, _new_root: <H as Hasher>::Out) {
// This is called to merge the `value_cache` from `as_trie_db_mut_cache`, which is not
// activated, so we don't need to do anything here.
}
}
// This is safe here since we are single-threaded in WASM
unsafe impl<H: Hasher> Send for CacheProvider<H> {}
unsafe impl<H: Hasher> Sync for CacheProvider<H> {}
@@ -0,0 +1,296 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Provide a specialized trie-recorder and provider for use in validate-block.
//!
//! This file defines two main structs, [`SizeOnlyRecorder`] and
//! [`SizeOnlyRecorderProvider`]. They are used to track the current
//! proof-size without actually recording the accessed nodes themselves.
use alloc::rc::Rc;
use codec::Encode;
use core::cell::{RefCell, RefMut};
use hashbrown::{HashMap, HashSet};
use pezsp_trie::{NodeCodec, ProofSizeProvider, RandomState, StorageProof};
use trie_db::{Hasher, RecordedForKey, TrieAccess};
pub(crate) type SeenNodes<H> = Rc<RefCell<HashSet<<H as Hasher>::Out, RandomState>>>;
/// A trie recorder that only keeps track of the proof size.
///
/// The internal size counting logic should align
/// with ['pezsp_trie::recorder::Recorder'].
pub struct SizeOnlyRecorder<'a, H: Hasher> {
seen_nodes: RefMut<'a, HashSet<H::Out, RandomState>>,
encoded_size: RefMut<'a, usize>,
recorded_keys: RefMut<'a, HashMap<Rc<[u8]>, RecordedForKey, RandomState>>,
}
impl<'a, H: trie_db::Hasher> trie_db::TrieRecorder<H::Out> for SizeOnlyRecorder<'a, H> {
fn record(&mut self, access: TrieAccess<'_, H::Out>) {
let mut encoded_size_update = 0;
match access {
TrieAccess::NodeOwned { hash, node_owned } =>
if self.seen_nodes.insert(hash) {
let node = node_owned.to_encoded::<NodeCodec<H>>();
encoded_size_update += node.encoded_size();
},
TrieAccess::EncodedNode { hash, encoded_node } =>
if self.seen_nodes.insert(hash) {
encoded_size_update += encoded_node.encoded_size();
},
TrieAccess::Value { hash, value, full_key } => {
if self.seen_nodes.insert(hash) {
encoded_size_update += value.encoded_size();
}
self.recorded_keys
.entry(full_key.into())
.and_modify(|e| *e = RecordedForKey::Value)
.or_insert_with(|| RecordedForKey::Value);
},
TrieAccess::Hash { full_key } => {
self.recorded_keys
.entry(full_key.into())
.or_insert_with(|| RecordedForKey::Hash);
},
TrieAccess::NonExisting { full_key } => {
self.recorded_keys
.entry(full_key.into())
.and_modify(|e| *e = RecordedForKey::Value)
.or_insert_with(|| RecordedForKey::Value);
},
TrieAccess::InlineValue { full_key } => {
self.recorded_keys
.entry(full_key.into())
.and_modify(|e| *e = RecordedForKey::Value)
.or_insert_with(|| RecordedForKey::Value);
},
};
*self.encoded_size += encoded_size_update;
}
fn trie_nodes_recorded_for_key(&self, key: &[u8]) -> RecordedForKey {
self.recorded_keys.get(key).copied().unwrap_or(RecordedForKey::None)
}
}
#[derive(Clone)]
pub struct SizeOnlyRecorderProvider<H: Hasher> {
seen_nodes: SeenNodes<H>,
encoded_size: Rc<RefCell<usize>>,
recorded_keys: Rc<RefCell<HashMap<Rc<[u8]>, RecordedForKey, RandomState>>>,
}
impl<H: Hasher> Default for SizeOnlyRecorderProvider<H> {
fn default() -> Self {
Self {
seen_nodes: Default::default(),
encoded_size: Default::default(),
recorded_keys: Default::default(),
}
}
}
impl<H: Hasher> SizeOnlyRecorderProvider<H> {
/// Use the given `seen_nodes` to populate the internal state.
#[cfg(not(feature = "std"))]
pub(crate) fn with_seen_nodes(seen_nodes: SeenNodes<H>) -> Self {
Self { seen_nodes, ..Default::default() }
}
}
impl<H: trie_db::Hasher> pezsp_trie::TrieRecorderProvider<H> for SizeOnlyRecorderProvider<H> {
type Recorder<'a>
= SizeOnlyRecorder<'a, H>
where
H: 'a;
fn drain_storage_proof(self) -> Option<StorageProof> {
None
}
fn as_trie_recorder(&self, _storage_root: H::Out) -> Self::Recorder<'_> {
SizeOnlyRecorder {
encoded_size: self.encoded_size.borrow_mut(),
seen_nodes: self.seen_nodes.borrow_mut(),
recorded_keys: self.recorded_keys.borrow_mut(),
}
}
}
impl<H: trie_db::Hasher> ProofSizeProvider for SizeOnlyRecorderProvider<H> {
fn estimate_encoded_size(&self) -> usize {
*self.encoded_size.borrow()
}
}
// This is safe here since we are single-threaded in WASM
unsafe impl<H: Hasher> Send for SizeOnlyRecorderProvider<H> {}
unsafe impl<H: Hasher> Sync for SizeOnlyRecorderProvider<H> {}
#[cfg(test)]
mod tests {
use rand::Rng;
use pezsp_trie::{
cache::{CacheSize, SharedTrieCache},
MemoryDB, ProofSizeProvider, TrieRecorderProvider,
};
use trie_db::{Trie, TrieDBBuilder, TrieDBMutBuilder, TrieHash, TrieMut, TrieRecorder};
use trie_standardmap::{Alphabet, StandardMap, ValueMode};
use super::*;
type Recorder = pezsp_trie::recorder::Recorder<pezsp_core::Blake2Hasher>;
fn create_trie() -> (
pezsp_trie::MemoryDB<pezsp_core::Blake2Hasher>,
TrieHash<pezsp_trie::LayoutV1<pezsp_core::Blake2Hasher>>,
Vec<(Vec<u8>, Vec<u8>)>,
) {
let mut db = MemoryDB::default();
let mut root = Default::default();
let mut seed = Default::default();
let test_data: Vec<(Vec<u8>, Vec<u8>)> = StandardMap {
alphabet: Alphabet::Low,
min_key: 16,
journal_key: 0,
value_mode: ValueMode::Random,
count: 1000,
}
.make_with(&mut seed)
.into_iter()
.map(|(k, v)| {
// Double the length so we end up with some values of 2 bytes and some of 64
let v = [v.clone(), v].concat();
(k, v)
})
.collect();
// Fill database with values
{
let mut trie = TrieDBMutBuilder::<pezsp_trie::LayoutV1<pezsp_core::Blake2Hasher>>::new(
&mut db, &mut root,
)
.build();
for (k, v) in &test_data {
trie.insert(k, v).expect("Inserts data");
}
}
(db, root, test_data)
}
#[test]
fn recorder_equivalence_cache() {
let (db, root, test_data) = create_trie();
let mut rng = rand::thread_rng();
for _ in 1..10 {
let reference_recorder = Recorder::default();
let recorder_for_test: SizeOnlyRecorderProvider<pezsp_core::Blake2Hasher> =
SizeOnlyRecorderProvider::default();
let reference_cache: SharedTrieCache<pezsp_core::Blake2Hasher> =
SharedTrieCache::new(CacheSize::new(1024 * 5), None);
let cache_for_test: SharedTrieCache<pezsp_core::Blake2Hasher> =
SharedTrieCache::new(CacheSize::new(1024 * 5), None);
{
let local_cache = cache_for_test.local_cache_untrusted();
let mut trie_cache_for_reference = local_cache.as_trie_db_cache(root);
let mut reference_trie_recorder = reference_recorder.as_trie_recorder(root);
let reference_trie =
TrieDBBuilder::<pezsp_trie::LayoutV1<pezsp_core::Blake2Hasher>>::new(&db, &root)
.with_recorder(&mut reference_trie_recorder)
.with_cache(&mut trie_cache_for_reference)
.build();
let local_cache_for_test = reference_cache.local_cache_untrusted();
let mut trie_cache_for_test = local_cache_for_test.as_trie_db_cache(root);
let mut trie_recorder_under_test = recorder_for_test.as_trie_recorder(root);
let test_trie =
TrieDBBuilder::<pezsp_trie::LayoutV1<pezsp_core::Blake2Hasher>>::new(&db, &root)
.with_recorder(&mut trie_recorder_under_test)
.with_cache(&mut trie_cache_for_test)
.build();
// Access random values from the test data
for _ in 0..100 {
let index: usize = rng.gen_range(0..test_data.len());
test_trie.get(&test_data[index].0).unwrap().unwrap();
reference_trie.get(&test_data[index].0).unwrap().unwrap();
}
// Check that we have the same nodes recorded for both recorders
for (key, _) in test_data.iter() {
let reference = reference_trie_recorder.trie_nodes_recorded_for_key(key);
let test_value = trie_recorder_under_test.trie_nodes_recorded_for_key(key);
assert_eq!(format!("{:?}", reference), format!("{:?}", test_value));
}
}
// Check that we have the same size recorded for both recorders
assert_eq!(
reference_recorder.estimate_encoded_size(),
recorder_for_test.estimate_encoded_size()
);
}
}
#[test]
fn recorder_equivalence_no_cache() {
let (db, root, test_data) = create_trie();
let mut rng = rand::thread_rng();
for _ in 1..10 {
let reference_recorder = Recorder::default();
let recorder_for_test: SizeOnlyRecorderProvider<pezsp_core::Blake2Hasher> =
SizeOnlyRecorderProvider::default();
{
let mut reference_trie_recorder = reference_recorder.as_trie_recorder(root);
let reference_trie =
TrieDBBuilder::<pezsp_trie::LayoutV1<pezsp_core::Blake2Hasher>>::new(&db, &root)
.with_recorder(&mut reference_trie_recorder)
.build();
let mut trie_recorder_under_test = recorder_for_test.as_trie_recorder(root);
let test_trie =
TrieDBBuilder::<pezsp_trie::LayoutV1<pezsp_core::Blake2Hasher>>::new(&db, &root)
.with_recorder(&mut trie_recorder_under_test)
.build();
for _ in 0..200 {
let index: usize = rng.gen_range(0..test_data.len());
test_trie.get(&test_data[index].0).unwrap().unwrap();
reference_trie.get(&test_data[index].0).unwrap().unwrap();
}
// Check that we have the same nodes recorded for both recorders
for (key, _) in test_data.iter() {
let reference = reference_trie_recorder.trie_nodes_recorded_for_key(key);
let test_value = trie_recorder_under_test.trie_nodes_recorded_for_key(key);
assert_eq!(format!("{:?}", reference), format!("{:?}", test_value));
}
}
// Check that we have the same size recorded for both recorders
assert_eq!(
reference_recorder.estimate_encoded_size(),
recorder_for_test.estimate_encoded_size()
);
}
}
}