referrerpolicy=no-referrer-when-downgrade

cumulus_client_consensus_aura/collators/slot_based/
block_import.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Cumulus.
3// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
4
5// Cumulus is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9
10// Cumulus is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14
15// You should have received a copy of the GNU General Public License
16// along with Cumulus. If not, see <https://www.gnu.org/licenses/>.
17
18use crate::LOG_TARGET;
19use codec::{Decode, Encode};
20use cumulus_client_consensus_common::old_finalized_hash;
21use cumulus_client_proof_size_recording::prepare_proof_size_recording_aux_data;
22use cumulus_primitives_core::{BlockBundleInfo, CoreInfo, CumulusDigestItem, RelayBlockIdentifier};
23use futures::{stream::FusedStream, StreamExt};
24use sc_client_api::{
25	backend::AuxStore,
26	client::{AuxDataOperations, FinalityNotification, PreCommitActions},
27	HeaderBackend,
28};
29use sc_consensus::{BlockImport, StateAction};
30use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
31use sp_api::{
32	ApiExt, CallApiAt, CallContext, Core, ProofRecorder, ProofRecorderIgnoredNodes,
33	ProvideRuntimeApi, StorageProof,
34};
35use sp_blockchain::{Error as ClientError, Result as ClientResult};
36use sp_consensus::BlockOrigin;
37use sp_runtime::traits::{Block as BlockT, HashingFor, Header as _};
38use sp_trie::proof_size_extension::{ProofSizeExt, RecordingProofSizeProvider};
39use std::sync::Arc;
40
41/// The aux storage key used to store the ignored nodes for the given block hash.
42fn ignored_nodes_key<H: Encode>(block_hash: H) -> Vec<u8> {
43	(b"cumulus_slot_based_nodes_to_ignore", block_hash).encode()
44}
45
46/// Prepare a transaction to write the ignored nodes to the aux storage.
47///
48/// Returns the key-value pairs that need to be written to the aux storage.
49fn prepare_ignored_nodes_transaction<Block: BlockT>(
50	block_hash: Block::Hash,
51	ignored_nodes: ProofRecorderIgnoredNodes<Block>,
52) -> impl Iterator<Item = (Vec<u8>, Vec<u8>)> {
53	let key = ignored_nodes_key(block_hash);
54	let encoded_nodes = <ProofRecorderIgnoredNodes<Block> as Encode>::encode(&ignored_nodes);
55
56	[(key, encoded_nodes)].into_iter()
57}
58
59/// Load the ignored nodes associated with a block.
60fn load_ignored_nodes<Block: BlockT, B: AuxStore>(
61	backend: &B,
62	block_hash: Block::Hash,
63) -> ClientResult<Option<ProofRecorderIgnoredNodes<Block>>> {
64	match backend.get_aux(&ignored_nodes_key(block_hash))? {
65		None => Ok(None),
66		Some(t) => ProofRecorderIgnoredNodes::<Block>::decode(&mut &t[..])
67			.map(Some)
68			.map_err(|e| ClientError::Backend(format!("Failed to decode ignored nodes: {}", e))),
69	}
70}
71
72/// Handle for receiving the block and the storage proof from the [`SlotBasedBlockImport`].
73///
74/// This handle should be passed to [`Params`](super::Params) or can also be dropped if the node is
75/// not running as collator.
76pub struct SlotBasedBlockImportHandle<Block> {
77	receiver: TracingUnboundedReceiver<(Block, Arc<StorageProof>)>,
78}
79
80impl<Block> SlotBasedBlockImportHandle<Block> {
81	/// Returns the next item.
82	///
83	/// The future will never return when the internal channel is closed.
84	pub async fn next(&mut self) -> (Block, Arc<StorageProof>) {
85		loop {
86			if self.receiver.is_terminated() {
87				futures::pending!()
88			} else if let Some(res) = self.receiver.next().await {
89				return res;
90			}
91		}
92	}
93}
94
95/// Register the clean up method for cleaning ignored nodes from blocks on which no further blocks
96/// will be imported.
97fn register_ignored_nodes_cleanup<C, Block>(client: Arc<C>)
98where
99	C: PreCommitActions<Block> + HeaderBackend<Block> + 'static,
100	Block: BlockT,
101{
102	let client_for_closure = client.clone();
103	let on_finality = move |notification: &FinalityNotification<Block>| -> AuxDataOperations {
104		let old_finalized = old_finalized_hash::<_, Block>(
105			&*client_for_closure,
106			&notification.tree_route,
107			*notification.header.parent_hash(),
108		);
109
110		notification
111			.stale_blocks
112			.iter()
113			// Delete the ignored nodes for all stale blocks.
114			.map(|b| (ignored_nodes_key(b.hash), None))
115			// We can not delete the ignored nodes for the finalized block, because blocks can still
116			// be imported on top of this block. However, once multiple blocks are finalized at
117			// once, blocks on the route to the finalized parent can no longer become parents
118			// either.
119			.chain(
120				notification
121					.tree_route
122					.iter()
123					.copied()
124					.map(|hash| (ignored_nodes_key(hash), None)),
125			)
126			.chain(std::iter::once((ignored_nodes_key(old_finalized), None)))
127			.collect()
128	};
129
130	client.register_finality_action(Box::new(on_finality));
131}
132
133/// Special block import for the slot based collator.
134pub struct SlotBasedBlockImport<Block: BlockT, BI, Client> {
135	inner: BI,
136	client: Arc<Client>,
137	sender: TracingUnboundedSender<(Block, Arc<StorageProof>)>,
138}
139
140impl<Block: BlockT, BI, Client> SlotBasedBlockImport<Block, BI, Client> {
141	/// Create a new instance.
142	///
143	/// The returned [`SlotBasedBlockImportHandle`] needs to be passed to the
144	/// [`Params`](super::Params), so that this block import instance can communicate with the
145	/// collation task. If the node is not running as a collator, just dropping the handle is fine.
146	pub fn new(inner: BI, client: Arc<Client>) -> (Self, SlotBasedBlockImportHandle<Block>)
147	where
148		Client: PreCommitActions<Block> + HeaderBackend<Block> + 'static,
149	{
150		let (sender, receiver) = tracing_unbounded("SlotBasedBlockImportChannel", 1000);
151
152		register_ignored_nodes_cleanup(client.clone());
153
154		(Self { sender, client, inner }, SlotBasedBlockImportHandle { receiver })
155	}
156
157	/// Get the [`ProofRecorderIgnoredNodes`] for `parent`.
158	///
159	/// If `parent` was not part of the same block bundle, the [`ProofRecorderIgnoredNodes`] are not
160	/// required and `None` will be returned.
161	fn get_ignored_nodes(
162		&self,
163		parent: Block::Hash,
164		core_info: &CoreInfo,
165		bundle_info: &BlockBundleInfo,
166		relay_block_identifier: &RelayBlockIdentifier,
167	) -> Option<ProofRecorderIgnoredNodes<Block>>
168	where
169		Client: AuxStore + HeaderBackend<Block> + Send + Sync,
170	{
171		let parent_header = self.client.header(parent).ok().flatten()?;
172		let parent_core_info = CumulusDigestItem::find_core_info(parent_header.digest())?;
173		let parent_bundle_info = CumulusDigestItem::find_block_bundle_info(parent_header.digest())?;
174		let parent_relay_block_identifier =
175			CumulusDigestItem::find_relay_block_identifier(parent_header.digest())?;
176
177		if parent_relay_block_identifier != *relay_block_identifier {
178			tracing::trace!(target: LOG_TARGET, ?parent_relay_block_identifier, ?relay_block_identifier, "Relay block identifier doesn't match");
179			return None;
180		}
181
182		if parent_core_info != *core_info {
183			tracing::trace!(target: LOG_TARGET, ?parent_core_info, ?core_info, "Core info doesn't match");
184			return None;
185		}
186
187		if parent_bundle_info.index.saturating_add(1) != bundle_info.index {
188			tracing::trace!(target: LOG_TARGET, ?parent_bundle_info, ?bundle_info, "Block is not a child, based on the index");
189			return None;
190		}
191
192		match load_ignored_nodes::<Block, _>(&*self.client, parent) {
193			Ok(nodes) => nodes,
194			Err(error) => {
195				tracing::trace!(target: LOG_TARGET, ?parent, ?error, "Failed to load `IgnoredNodes` from aux store");
196				None
197			},
198		}
199	}
200
201	/// Execute the given block and collect the storage proof.
202	///
203	/// We need to execute the block on this level here, because we are collecting the storage
204	/// proofs and combining them for blocks on the same core. So, blocks on the same core do not
205	/// need to include the same trie nodes multiple times and thus, not wasting storage proof size.
206	///
207	/// The proof must be recorded in exactly the same manner as during block building, because the
208	/// proof size is tracked via `ProofSizeExt` and affects runtime state. Without identical proof
209	/// recording, the computed state root would differ and block import would fail.
210	async fn execute_block_and_collect_storage_proof(
211		&self,
212		params: &mut sc_consensus::BlockImportParams<Block>,
213	) -> Result<Option<(Block, Arc<StorageProof>)>, sp_consensus::Error>
214	where
215		Client: ProvideRuntimeApi<Block>
216			+ CallApiAt<Block>
217			+ AuxStore
218			+ HeaderBackend<Block>
219			+ Send
220			+ Sync,
221		Client::StateBackend: Send,
222		Client::Api: Core<Block>,
223	{
224		let core_info = CumulusDigestItem::find_core_info(params.header.digest());
225		let bundle_info = CumulusDigestItem::find_block_bundle_info(params.header.digest());
226		let relay_block_identifier =
227			CumulusDigestItem::find_relay_block_identifier(params.header.digest());
228
229		let (Some(core_info), Some(bundle_info), Some(relay_block_identifier)) =
230			(core_info, bundle_info, relay_block_identifier)
231		else {
232			tracing::debug!(
233				target: LOG_TARGET,
234				number = ?params.header.number(),
235				"no bundle digests, skipping execute_block_and_collect_storage_proof",
236			);
237			return Ok(None);
238		};
239
240		let parent_hash = *params.header.parent_hash();
241
242		let mut nodes_to_ignore = self
243			.get_ignored_nodes(parent_hash, &core_info, &bundle_info, &relay_block_identifier)
244			.unwrap_or_default();
245
246		let recorder = ProofRecorder::<Block>::with_ignored_nodes(nodes_to_ignore.clone());
247		let proof_size_recorder = RecordingProofSizeProvider::new(recorder.clone());
248
249		let mut runtime_api = self.client.runtime_api();
250
251		// `record_proof_with_recorder` captures trie accesses, while `ProofSizeExt` replays the
252		// proof-size estimations in the same order they were observed during block building.
253		runtime_api.set_call_context(CallContext::Onchain { import: true });
254		runtime_api.record_proof_with_recorder(recorder.clone());
255		runtime_api.register_extension(ProofSizeExt::new(proof_size_recorder.clone()));
256
257		// Only blocks imported at the tip (built by other collators) are forwarded to the
258		// resubmission backfill task. Sync/gap/warp imports are historical and never part of our
259		// unincluded segment, so recording resubmission data for them would be wasted work (and
260		// would buffer their potentially large storage proofs).
261		let collect_for_resubmission = matches!(
262			params.origin,
263			BlockOrigin::NetworkBroadcast | BlockOrigin::ConsensusBroadcast
264		);
265
266		let body = params.body.clone().unwrap_or_default();
267		let resubmission_body = collect_for_resubmission.then(|| body.clone());
268		let block = Block::new(params.header.clone(), body);
269
270		tracing::debug!(
271			target: LOG_TARGET,
272			?parent_hash,
273			number = ?params.header.number(),
274			?core_info,
275			?bundle_info,
276			"execute_block_and_collect_storage_proof: calling runtime_api.execute_block",
277		);
278
279		runtime_api
280			.execute_block(parent_hash, block.into())
281			.map_err(|e| Box::new(e) as Box<_>)?;
282
283		let storage_proof =
284			runtime_api.extract_proof().expect("Proof recording was enabled above; qed");
285
286		let state = self.client.state_at(parent_hash).map_err(|e| Box::new(e) as Box<_>)?;
287		let gen_storage_changes = runtime_api
288			.into_storage_changes(&state, parent_hash)
289			.map_err(sp_consensus::Error::ChainLookup)?;
290
291		if params.header.state_root() != &gen_storage_changes.transaction_storage_root {
292			return Err(sp_consensus::Error::Other(Box::new(
293				sp_blockchain::Error::InvalidStateRoot,
294			)));
295		}
296
297		// Extend the ignored nodes with the nodes from the storage proof and the generated
298		// storage changes. This ensures that subsequent blocks in the same bundle don't
299		// redundantly include the same trie nodes in their proof.
300		nodes_to_ignore.extend(ProofRecorderIgnoredNodes::<Block>::from_storage_proof::<
301			HashingFor<Block>,
302		>(&storage_proof));
303		nodes_to_ignore.extend(ProofRecorderIgnoredNodes::<Block>::from_memory_db(
304			gen_storage_changes.transaction.clone(),
305		));
306
307		let block_hash = params.post_hash();
308		prepare_ignored_nodes_transaction::<Block>(block_hash, nodes_to_ignore).for_each(
309			|(k, v)| {
310				params.auxiliary.push((k, Some(v)));
311			},
312		);
313
314		// Extract and store proof size recordings
315		let recorded_sizes = proof_size_recorder
316			.recorded_estimations()
317			.into_iter()
318			.map(|size| size as u32)
319			.collect::<Vec<u32>>();
320
321		if !recorded_sizes.is_empty() {
322			prepare_proof_size_recording_aux_data(block_hash, recorded_sizes).for_each(|(k, v)| {
323				params.auxiliary.push((k, Some(v)));
324			});
325		}
326
327		params.state_action =
328			StateAction::ApplyChanges(sc_consensus::StorageChanges::Changes(gen_storage_changes));
329
330		let Some(resubmission_body) = resubmission_body else { return Ok(None) };
331
332		let block = Block::new(params.post_header(), resubmission_body);
333		Ok(Some((block, Arc::new(storage_proof))))
334	}
335}
336
337impl<Block: BlockT, BI: Clone, Client> Clone for SlotBasedBlockImport<Block, BI, Client> {
338	fn clone(&self) -> Self {
339		Self { inner: self.inner.clone(), client: self.client.clone(), sender: self.sender.clone() }
340	}
341}
342
343#[async_trait::async_trait]
344impl<Block, BI, Client> BlockImport<Block> for SlotBasedBlockImport<Block, BI, Client>
345where
346	Block: BlockT,
347	BI: BlockImport<Block> + Send + Sync,
348	BI::Error: Into<sp_consensus::Error>,
349	Client:
350		ProvideRuntimeApi<Block> + CallApiAt<Block> + AuxStore + HeaderBackend<Block> + Send + Sync,
351	Client::StateBackend: Send,
352	Client::Api: Core<Block>,
353{
354	type Error = sp_consensus::Error;
355
356	async fn check_block(
357		&self,
358		block: sc_consensus::BlockCheckParams<Block>,
359	) -> Result<sc_consensus::ImportResult, Self::Error> {
360		self.inner.check_block(block).await.map_err(Into::into)
361	}
362
363	async fn import_block(
364		&self,
365		mut params: sc_consensus::BlockImportParams<Block>,
366	) -> Result<sc_consensus::ImportResult, Self::Error> {
367		let origin = params.origin;
368
369		let imported = if !(origin == BlockOrigin::Own ||
370			params.with_state() ||
371			params.state_action.skip_execution_checks())
372		{
373			self.execute_block_and_collect_storage_proof(&mut params).await?
374		} else {
375			None
376		};
377
378		let result = self.inner.import_block(params).await.map_err(Into::into)?;
379
380		if let Some((block, proof)) = imported {
381			let _ = self.sender.unbounded_send((block, proof));
382		}
383
384		Ok(result)
385	}
386}