Skip to main content

soil_client/tracing/block/
mod.rs

1// This file is part of Soil.
2
3// Copyright (C) Soil contributors.
4// Copyright (C) Parity Technologies (UK) Ltd.
5// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
6
7//! Utilities for tracing block execution
8
9use std::{
10	collections::HashMap,
11	sync::{
12		atomic::{AtomicU64, Ordering},
13		Arc,
14	},
15	time::Instant,
16};
17
18use ::tracing::{
19	dispatcher,
20	span::{Attributes, Id, Record},
21	Dispatch, Level, Subscriber,
22};
23use codec::Encode;
24use parking_lot::Mutex;
25
26use super::{SpanDatum, TraceEvent, Values};
27use crate::blockchain::HeaderBackend;
28use crate::client_api::BlockBackend;
29use subsoil::api::{Core, ProvideRuntimeApi};
30use subsoil::core::hexdisplay::HexDisplay;
31use subsoil::runtime::{
32	generic::BlockId,
33	traits::{Block as BlockT, Header},
34};
35use subsoil::tracing::rpc::{BlockTrace, Span, TraceBlockResponse};
36use subsoil::tracing::{WASM_NAME_KEY, WASM_TARGET_KEY, WASM_TRACE_IDENTIFIER};
37
38// Default to only pallet, frame support and state related traces
39const DEFAULT_TARGETS: &str = "pallet,frame,state";
40const TRACE_TARGET: &str = "block_trace";
41// The name of a field required for all events.
42const REQUIRED_EVENT_FIELD: &str = "method";
43
44/// Something that can execute a block in a tracing context.
45pub trait TracingExecuteBlock<Block: BlockT>: Send + Sync {
46	/// Execute the given `block`.
47	///
48	/// The `block` is prepared to be executed right away, this means that any `Seal` was already
49	/// removed from the header. As this changes the `hash` of the block, `orig_hash` is passed
50	/// alongside to the callee.
51	///
52	/// The execution should be done sync on the same thread, because the caller will register
53	/// special tracing collectors.
54	fn execute_block(&self, orig_hash: Block::Hash, block: Block) -> crate::blockchain::Result<()>;
55}
56
57/// Default implementation of [`ExecuteBlock`].
58///
59/// Uses [`Core::execute_block`] to directly execute a block.
60struct DefaultExecuteBlock<Client> {
61	client: Arc<Client>,
62}
63
64impl<Client> DefaultExecuteBlock<Client> {
65	/// Creates a new instance.
66	pub fn new(client: Arc<Client>) -> Self {
67		Self { client }
68	}
69}
70
71impl<Client, Block> TracingExecuteBlock<Block> for DefaultExecuteBlock<Client>
72where
73	Client: ProvideRuntimeApi<Block> + Send + Sync + 'static,
74	Client::Api: Core<Block>,
75	Block: BlockT,
76{
77	fn execute_block(&self, _: Block::Hash, block: Block) -> crate::blockchain::Result<()> {
78		self.client
79			.runtime_api()
80			.execute_block(*block.header().parent_hash(), block.into())
81			.map_err(Into::into)
82	}
83}
84
85/// Tracing Block Result type alias
86pub type TraceBlockResult<T> = Result<T, Error>;
87
88/// Tracing Block error
89#[derive(Debug, thiserror::Error)]
90#[allow(missing_docs)]
91#[non_exhaustive]
92pub enum Error {
93	#[error("Invalid block Id: {0}")]
94	InvalidBlockId(#[from] crate::blockchain::Error),
95	#[error("Missing block component: {0}")]
96	MissingBlockComponent(String),
97	#[error("Dispatch error: {0}")]
98	Dispatch(String),
99}
100
101struct BlockSubscriber {
102	targets: Vec<(String, Level)>,
103	next_id: AtomicU64,
104	spans: Mutex<HashMap<Id, SpanDatum>>,
105	events: Mutex<Vec<TraceEvent>>,
106}
107
108impl BlockSubscriber {
109	fn new(targets: &str) -> Self {
110		let next_id = AtomicU64::new(1);
111		let mut targets: Vec<_> = targets.split(',').map(super::parse_target).collect();
112		// Ensure that WASM traces are always enabled
113		// Filtering happens when decoding the actual target / level
114		targets.push((WASM_TRACE_IDENTIFIER.to_owned(), Level::TRACE));
115		BlockSubscriber {
116			targets,
117			next_id,
118			spans: Mutex::new(HashMap::new()),
119			events: Mutex::new(Vec::new()),
120		}
121	}
122}
123
124impl Subscriber for BlockSubscriber {
125	fn enabled(&self, metadata: &::tracing::Metadata<'_>) -> bool {
126		if !metadata.is_span() && metadata.fields().field(REQUIRED_EVENT_FIELD).is_none() {
127			return false;
128		}
129
130		for (target, level) in &self.targets {
131			if metadata.level() <= level && metadata.target().starts_with(target) {
132				return true;
133			}
134		}
135
136		false
137	}
138
139	fn new_span(&self, attrs: &Attributes<'_>) -> Id {
140		let id = Id::from_u64(self.next_id.fetch_add(1, Ordering::Relaxed));
141		let mut values = Values::default();
142		attrs.record(&mut values);
143		let parent_id = attrs.parent().cloned();
144		let span = SpanDatum {
145			id: id.clone(),
146			parent_id,
147			name: attrs.metadata().name().to_owned(),
148			target: attrs.metadata().target().to_owned(),
149			level: *attrs.metadata().level(),
150			line: attrs.metadata().line().unwrap_or(0),
151			start_time: Instant::now(),
152			values,
153			overall_time: Default::default(),
154		};
155
156		self.spans.lock().insert(id.clone(), span);
157		id
158	}
159
160	fn record(&self, span: &Id, values: &Record<'_>) {
161		let mut span_data = self.spans.lock();
162		if let Some(s) = span_data.get_mut(span) {
163			values.record(&mut s.values);
164		}
165	}
166
167	fn record_follows_from(&self, _span: &Id, _follows: &Id) {
168		// Not currently used
169		unimplemented!("record_follows_from is not implemented");
170	}
171
172	fn event(&self, event: &::tracing::Event<'_>) {
173		let mut values = Values::default();
174		event.record(&mut values);
175		let parent_id = event.parent().cloned();
176		let trace_event = TraceEvent {
177			name: event.metadata().name().to_owned(),
178			target: event.metadata().target().to_owned(),
179			level: *event.metadata().level(),
180			values,
181			parent_id,
182		};
183		self.events.lock().push(trace_event);
184	}
185
186	fn enter(&self, _id: &Id) {}
187
188	fn exit(&self, _span: &Id) {}
189}
190
191/// Holds a reference to the client in order to execute the given block.
192/// Records spans & events for the supplied targets (eg. "pallet,frame,state") and
193/// only records events with the specified hex encoded storage key prefixes.
194/// Note: if `targets` or `storage_keys` is an empty string then nothing is
195/// filtered out.
196pub struct BlockExecutor<Block: BlockT, Client> {
197	client: Arc<Client>,
198	block: Block::Hash,
199	targets: Option<String>,
200	storage_keys: Option<String>,
201	methods: Option<String>,
202	execute_block: Arc<dyn TracingExecuteBlock<Block>>,
203}
204
205impl<Block, Client> BlockExecutor<Block, Client>
206where
207	Block: BlockT + 'static,
208	Client: HeaderBackend<Block>
209		+ BlockBackend<Block>
210		+ ProvideRuntimeApi<Block>
211		+ Send
212		+ Sync
213		+ 'static,
214	Client::Api: Core<Block>,
215{
216	/// Create a new `BlockExecutor`
217	pub fn new(
218		client: Arc<Client>,
219		block: Block::Hash,
220		targets: Option<String>,
221		storage_keys: Option<String>,
222		methods: Option<String>,
223		execute_block: Option<Arc<dyn TracingExecuteBlock<Block>>>,
224	) -> Self {
225		Self {
226			client: client.clone(),
227			block,
228			targets,
229			storage_keys,
230			methods,
231			execute_block: execute_block
232				.unwrap_or_else(|| Arc::new(DefaultExecuteBlock::new(client))),
233		}
234	}
235
236	/// Execute block, record all spans and events belonging to `Self::targets`
237	/// and filter out events which do not have keys starting with one of the
238	/// prefixes in `Self::storage_keys`.
239	pub fn trace_block(&self) -> TraceBlockResult<TraceBlockResponse> {
240		::tracing::debug!(target: "state_tracing", "Tracing block: {}", self.block);
241		// Prepare the block
242		let mut header = self
243			.client
244			.header(self.block)
245			.map_err(Error::InvalidBlockId)?
246			.ok_or_else(|| Error::MissingBlockComponent("Header not found".to_string()))?;
247		let extrinsics = self
248			.client
249			.block_body(self.block)
250			.map_err(Error::InvalidBlockId)?
251			.ok_or_else(|| Error::MissingBlockComponent("Extrinsics not found".to_string()))?;
252		::tracing::debug!(target: "state_tracing", "Found {} extrinsics", extrinsics.len());
253		let parent_hash = *header.parent_hash();
254		// Remove all `Seal`s as they are added by the consensus engines after building the block.
255		// On import they are normally removed by the consensus engine.
256		header.digest_mut().logs.retain(|d| d.as_seal().is_none());
257		let block = Block::new(header, extrinsics);
258
259		let targets = if let Some(t) = &self.targets { t } else { DEFAULT_TARGETS };
260		let block_subscriber = BlockSubscriber::new(targets);
261		let dispatch = Dispatch::new(block_subscriber);
262
263		{
264			let dispatcher_span = ::tracing::debug_span!(
265				target: "state_tracing",
266				"execute_block",
267				extrinsics_len = block.extrinsics().len(),
268			);
269			let _guard = dispatcher_span.enter();
270
271			if let Err(e) = dispatcher::with_default(&dispatch, || {
272				let span = ::tracing::info_span!(target: TRACE_TARGET, "trace_block");
273				let _enter = span.enter();
274				self.execute_block.execute_block(self.block, block)
275			}) {
276				return Err(Error::Dispatch(format!(
277					"Failed to collect traces and execute block: {e:?}"
278				)));
279			}
280		}
281
282		let block_subscriber = dispatch.downcast_ref::<BlockSubscriber>().ok_or_else(|| {
283			Error::Dispatch(
284				"Cannot downcast Dispatch to BlockSubscriber after tracing block".to_string(),
285			)
286		})?;
287		let spans: Vec<_> = block_subscriber
288			.spans
289			.lock()
290			.drain()
291			// Patch wasm identifiers
292			.filter_map(|(_, s)| patch_and_filter(s, targets))
293			.collect();
294		let events: Vec<_> = block_subscriber
295			.events
296			.lock()
297			.drain(..)
298			.filter(|e| {
299				self.storage_keys
300					.as_ref()
301					.map(|keys| event_values_filter(e, "key", keys))
302					.unwrap_or(false)
303			})
304			.filter(|e| {
305				self.methods
306					.as_ref()
307					.map(|methods| event_values_filter(e, "method", methods))
308					.unwrap_or(false)
309			})
310			.map(|s| s.into())
311			.collect();
312		::tracing::debug!(target: "state_tracing", "Captured {} spans and {} events", spans.len(), events.len());
313
314		Ok(TraceBlockResponse::BlockTrace(BlockTrace {
315			block_hash: block_id_as_string(BlockId::<Block>::Hash(self.block)),
316			parent_hash: block_id_as_string(BlockId::<Block>::Hash(parent_hash)),
317			tracing_targets: targets.to_string(),
318			storage_keys: self.storage_keys.clone().unwrap_or_default(),
319			methods: self.methods.clone().unwrap_or_default(),
320			spans,
321			events,
322		}))
323	}
324}
325
326fn event_values_filter(event: &TraceEvent, filter_kind: &str, values: &str) -> bool {
327	event
328		.values
329		.string_values
330		.get(filter_kind)
331		.map(|value| check_target(values, value, &event.level))
332		.unwrap_or(false)
333}
334
335/// Filter out spans that do not match our targets and if the span is from WASM update its `name`
336/// and `target` fields to the WASM values for those fields.
337// The `tracing` crate requires trace metadata to be static. This does not work for wasm code in
338// substrate, as it is regularly updated with new code from on-chain events. The workaround for this
339// is for substrate's WASM tracing wrappers to put the `name` and `target` data in the `values` map
340// (normally they would be in the static metadata assembled at compile time). Here, if a special
341// WASM `name` or `target` key is found in the `values` we remove it and put the key value pair in
342// the span's metadata, making it consistent with spans that come from native code.
343fn patch_and_filter(mut span: SpanDatum, targets: &str) -> Option<Span> {
344	if span.name == WASM_TRACE_IDENTIFIER {
345		span.values.bool_values.insert("wasm".to_owned(), true);
346		if let Some(n) = span.values.string_values.remove(WASM_NAME_KEY) {
347			span.name = n;
348		}
349		if let Some(t) = span.values.string_values.remove(WASM_TARGET_KEY) {
350			span.target = t;
351		}
352		if !check_target(targets, &span.target, &span.level) {
353			return None;
354		}
355	}
356
357	Some(span.into())
358}
359
360/// Check if a `target` matches any `targets` by prefix
361fn check_target(targets: &str, target: &str, level: &Level) -> bool {
362	for (t, l) in targets.split(',').map(super::parse_target) {
363		if target.starts_with(t.as_str()) && level <= &l {
364			return true;
365		}
366	}
367
368	false
369}
370
371fn block_id_as_string<T: BlockT>(block_id: BlockId<T>) -> String {
372	match block_id {
373		BlockId::Hash(h) => HexDisplay::from(&h.encode()).to_string(),
374		BlockId::Number(n) => HexDisplay::from(&n.encode()).to_string(),
375	}
376}