From b031fdc93da19c081c9f3a8501068e7610b83a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20K=C3=B6cher?= Date: Tue, 7 Nov 2023 22:40:15 +0100 Subject: [PATCH 1/2] validate-block: Fix `TrieCache` implementation The trie cache implementation was ignoring the `storage_root` when setting up the value cache. The problem with this is that the value cache works using `storage_keys` and these keys are not unique across different tries. A block can actually have different tries (main trie and multiple child tries). This pull request fixes the issue by not ignoring the `storage_root` and returning an unique `value_cache` per `storage_root`. It also adds a test for the seen bug and improves documentation that this doesn't happen again. --- Cargo.lock | 1 + cumulus/pallets/parachain-system/Cargo.toml | 1 + .../src/validate_block/tests.rs | 48 +++++++++++++++++-- .../src/validate_block/trie_cache.rs | 26 ++++++---- cumulus/test/runtime/src/test_pallet.rs | 24 ++++++++++ .../state-machine/src/trie_backend.rs | 5 +- substrate/test-utils/client/src/client_ext.rs | 4 +- substrate/test-utils/client/src/lib.rs | 2 +- 8 files changed, 96 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 764d4ed01aa5..753875a740de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3839,6 +3839,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "futures", "hex-literal", "impl-trait-for-tuples", "lazy_static", diff --git a/cumulus/pallets/parachain-system/Cargo.toml b/cumulus/pallets/parachain-system/Cargo.toml index 1c26b3a74861..5600c95a2a60 100644 --- a/cumulus/pallets/parachain-system/Cargo.toml +++ b/cumulus/pallets/parachain-system/Cargo.toml @@ -45,6 +45,7 @@ assert_matches = "1.5" hex-literal = "0.4.1" lazy_static = "1.4" rand = "0.8.5" +futures = "0.3.28" # Substrate sc-client-api = { path = "../../../substrate/client/api" } diff --git a/cumulus/pallets/parachain-system/src/validate_block/tests.rs b/cumulus/pallets/parachain-system/src/validate_block/tests.rs index 0cf68f25cc34..f17ac6007a09 100644 --- a/cumulus/pallets/parachain-system/src/validate_block/tests.rs +++ b/cumulus/pallets/parachain-system/src/validate_block/tests.rs @@ -21,12 +21,13 @@ use cumulus_test_client::{ runtime::{ self as test_runtime, Block, Hash, Header, TestPalletCall, UncheckedExtrinsic, WASM_BINARY, }, - transfer, BlockData, BuildParachainBlockData, Client, DefaultTestClientBuilderExt, HeadData, - InitBlockBuilder, TestClientBuilder, TestClientBuilderExt, ValidationParams, + transfer, BlockData, BlockOrigin, BuildParachainBlockData, Client, ClientBlockImportExt, + DefaultTestClientBuilderExt, HeadData, InitBlockBuilder, TestClientBuilder, + TestClientBuilderExt, ValidationParams, }; use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder; use sp_keyring::AccountKeyring::*; -use sp_runtime::traits::Header as HeaderT; +use sp_runtime::traits::{Block as BlockT, Header as HeaderT}; use std::{env, process::Command}; use crate::validate_block::MemoryOptimizedValidationParams; @@ -100,7 +101,7 @@ fn build_block_with_witness( } #[test] -fn validate_block_no_extra_extrinsics() { +fn validate_block_works() { sp_tracing::try_init_simple(); let (client, parent_head) = create_test_client(); @@ -290,3 +291,42 @@ fn validation_params_and_memory_optimized_validation_params_encode_and_decode() 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() { + sp_tracing::try_init_simple(); + + let (mut 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(), + ); + let block = block.into_block(); + + futures::executor::block_on(client.import(BlockOrigin::Own, block.clone())).unwrap(); + + let parent_head = block.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(), + ); + + let header = block.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); +} diff --git a/cumulus/pallets/parachain-system/src/validate_block/trie_cache.rs b/cumulus/pallets/parachain-system/src/validate_block/trie_cache.rs index 57876b87876f..e3de16bd2aee 100644 --- a/cumulus/pallets/parachain-system/src/validate_block/trie_cache.rs +++ b/cumulus/pallets/parachain-system/src/validate_block/trie_cache.rs @@ -29,7 +29,7 @@ use trie_db::{node::NodeOwned, Hasher}; /// have already been loaded and decoded from the storage proof. pub(crate) struct TrieCache<'a, H: Hasher> { node_cache: RefMut<'a, BTreeMap>>, - value_cache: Option, trie_db::CachedValue>>>, + value_cache: Option, trie_db::CachedValue>>>, } impl<'a, H: Hasher> trie_db::TrieCache> for TrieCache<'a, H> { @@ -67,7 +67,13 @@ impl<'a, H: Hasher> trie_db::TrieCache> for TrieCache<'a, H> { /// Provider of [`TrieCache`] instances. pub(crate) struct CacheProvider { node_cache: RefCell>>, - value_cache: RefCell, trie_db::CachedValue>>, + /// 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, trie_db::CachedValue>>>, } impl CacheProvider { @@ -81,22 +87,26 @@ impl CacheProvider { impl TrieCacheProvider for CacheProvider { type Cache<'a> = TrieCache<'a, H> where H: 'a; - fn as_trie_db_cache(&self, _storage_root: ::Out) -> Self::Cache<'_> { + fn as_trie_db_cache(&self, storage_root: ::Out) -> Self::Cache<'_> { TrieCache { - value_cache: Some(self.value_cache.borrow_mut()), + 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. - // Since we are using a simplified cache architecture, - // we do not have separate key spaces for different storage roots. - // The value cache is therefore disabled here. + // 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: ::Out) {} + fn merge<'a>(&'a self, _other: Self::Cache<'a>, _new_root: ::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 diff --git a/cumulus/test/runtime/src/test_pallet.rs b/cumulus/test/runtime/src/test_pallet.rs index a0763c9069cd..5d11b7f490c6 100644 --- a/cumulus/test/runtime/src/test_pallet.rs +++ b/cumulus/test/runtime/src/test_pallet.rs @@ -49,6 +49,30 @@ pub mod pallet { ); Ok(()) } + + /// A dispatchable that first reads two values from two different child tries, asserts they + /// are the expected values (if the values exist in the state) and then writes two different + /// values to these child tries. + #[pallet::weight(0)] + pub fn read_and_write_child_tries(_: OriginFor) -> DispatchResult { + let key = &b"hello"[..]; + let first_trie = &b"first"[..]; + let second_trie = &b"second"[..]; + let first_value = "world1".encode(); + let second_value = "world2".encode(); + + if let Some(res) = sp_io::default_child_storage::get(first_trie, key) { + assert_eq!(first_value, res); + } + if let Some(res) = sp_io::default_child_storage::get(second_trie, key) { + assert_eq!(second_value, res); + } + + sp_io::default_child_storage::set(first_trie, key, &first_value); + sp_io::default_child_storage::set(second_trie, key, &second_value); + + Ok(()) + } } #[derive(frame_support::DefaultNoBound)] diff --git a/substrate/primitives/state-machine/src/trie_backend.rs b/substrate/primitives/state-machine/src/trie_backend.rs index afa80addd2ba..7b337b5fd540 100644 --- a/substrate/primitives/state-machine/src/trie_backend.rs +++ b/substrate/primitives/state-machine/src/trie_backend.rs @@ -52,7 +52,10 @@ pub trait TrieCacheProvider { /// Return a [`trie_db::TrieDB`] compatible cache. /// - /// The `storage_root` parameter should be the storage root of the used trie. + /// The `storage_root` parameter *must* be the storage root of the trie this cache is used for. + /// + /// NOTE: Implementors should use the `storage_root` to differentiate between storage keys that + /// may belong to different tries. fn as_trie_db_cache(&self, storage_root: H::Out) -> Self::Cache<'_>; /// Returns a cache that can be used with a [`trie_db::TrieDBMut`]. diff --git a/substrate/test-utils/client/src/client_ext.rs b/substrate/test-utils/client/src/client_ext.rs index 8efa7b5f07f8..73581a4f0efa 100644 --- a/substrate/test-utils/client/src/client_ext.rs +++ b/substrate/test-utils/client/src/client_ext.rs @@ -20,9 +20,11 @@ use sc_client_api::{backend::Finalizer, client::BlockBackend}; use sc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy}; use sc_service::client::Client; -use sp_consensus::{BlockOrigin, Error as ConsensusError}; +use sp_consensus::Error as ConsensusError; use sp_runtime::{traits::Block as BlockT, Justification, Justifications}; +pub use sp_consensus::BlockOrigin; + /// Extension trait for a test client. pub trait ClientExt: Sized { /// Finalize a block. diff --git a/substrate/test-utils/client/src/lib.rs b/substrate/test-utils/client/src/lib.rs index 90e15e0f8d53..084dd2a1861c 100644 --- a/substrate/test-utils/client/src/lib.rs +++ b/substrate/test-utils/client/src/lib.rs @@ -21,7 +21,7 @@ pub mod client_ext; -pub use self::client_ext::{ClientBlockImportExt, ClientExt}; +pub use self::client_ext::{BlockOrigin, ClientBlockImportExt, ClientExt}; pub use sc_client_api::{execution_extensions::ExecutionExtensions, BadBlocks, ForkBlocks}; pub use sc_client_db::{self, Backend, BlocksPruning}; pub use sc_executor::{self, NativeElseWasmExecutor, WasmExecutionMethod, WasmExecutor}; From 10e6891197551516e4028586972403c6ed1de7dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20K=C3=B6cher?= Date: Wed, 8 Nov 2023 07:46:26 +0100 Subject: [PATCH 2/2] Fix warning --- .../pallets/parachain-system/src/validate_block/trie_cache.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cumulus/pallets/parachain-system/src/validate_block/trie_cache.rs b/cumulus/pallets/parachain-system/src/validate_block/trie_cache.rs index e3de16bd2aee..5d785910fbe0 100644 --- a/cumulus/pallets/parachain-system/src/validate_block/trie_cache.rs +++ b/cumulus/pallets/parachain-system/src/validate_block/trie_cache.rs @@ -29,7 +29,7 @@ use trie_db::{node::NodeOwned, Hasher}; /// have already been loaded and decoded from the storage proof. pub(crate) struct TrieCache<'a, H: Hasher> { node_cache: RefMut<'a, BTreeMap>>, - value_cache: Option, trie_db::CachedValue>>>, + value_cache: Option, trie_db::CachedValue>>>, } impl<'a, H: Hasher> trie_db::TrieCache> for TrieCache<'a, H> {