Skip to main content

pop_fork/
block.rs

1// SPDX-License-Identifier: GPL-3.0
2
3//! Block structure for forked blockchain state.
4//!
5//! This module provides the [`Block`] struct which represents a single block
6//! in a forked blockchain. Each block contains its metadata (number, hash,
7//! parent hash, header, extrinsics) and an associated storage layer for
8//! reading and modifying state.
9//!
10//! # Architecture
11//!
12//! ```text
13//! ┌─────────────────────────────────────────────────────────────────┐
14//! │                           Block                                  │
15//! │                                                                   │
16//! │   ┌──────────────────────────────────────────────────────────┐   │
17//! │   │ Metadata: number, hash, parent_hash, header, extrinsics  │   │
18//! │   └──────────────────────────────────────────────────────────┘   │
19//! │                              │                                    │
20//! │                              ▼                                    │
21//! │   ┌──────────────────────────────────────────────────────────┐   │
22//! │   │                  LocalStorageLayer                        │   │
23//! │   │  (tracks modifications on top of remote chain state)      │   │
24//! │   └──────────────────────────────────────────────────────────┘   │
25//! └─────────────────────────────────────────────────────────────────┘
26//! ```
27//!
28//! # Usage
29//!
30//! ```ignore
31//! use pop_fork::{Block, ForkRpcClient, StorageCache};
32//!
33//! // Create a fork point from a live chain
34//! let rpc = ForkRpcClient::connect(&endpoint).await?;
35//! let cache = StorageCache::in_memory().await?;
36//! let block_hash = rpc.finalized_head().await?;
37//! let fork_block = Block::fork_point(rpc, cache, block_hash).await?;
38//!
39//! // Access storage
40//! let value = fork_block.storage().get(fork_block.number, &key).await?;
41//!
42//! // Modify storage and commit to create a new block
43//! fork_block.storage().set(&key, Some(&new_value))?;
44//! ```
45
46use crate::{BlockError, ForkRpcClient, LocalStorageLayer, RemoteStorageLayer, StorageCache};
47use std::sync::Arc;
48use subxt::{Metadata, config::substrate::H256, ext::codec::Encode};
49use url::Url;
50
51/// A block in a forked blockchain.
52///
53/// Represents a single block with its metadata and associated storage state.
54/// Blocks can be created as fork points from live chains or as child blocks
55/// extending an existing fork.
56///
57/// # Storage Model
58///
59/// Each block has an associated [`LocalStorageLayer`] that tracks storage
60/// modifications. The storage layer uses a layered architecture:
61///
62/// - **Local modifications**: In-memory changes for the current block
63/// - **Committed state**: Previously committed blocks stored in SQLite
64/// - **Remote state**: Original chain state fetched lazily via RPC + cache for faster relaunches.
65///
66/// # Cloning
67///
68/// `Block` is cheap to clone, as `LocalStorageLayer` is cheap to clone.
69#[derive(Clone, Debug)]
70pub struct Block {
71	/// The block number (height).
72	pub number: u32,
73	/// The block hash.
74	pub hash: H256,
75	/// The parent block hash.
76	pub parent_hash: H256,
77	/// The encoded block header.
78	pub header: Vec<u8>,
79	/// The extrinsics (transactions) in this block.
80	pub extrinsics: Vec<Vec<u8>>,
81	/// The storage layer for this block.
82	///
83	/// Also manages runtime metadata versions, enabling dynamic lookup of
84	/// pallet and call indices for inherent providers.
85	storage: LocalStorageLayer,
86	/// The parent block. Keeping blocks in memory is cheap as the `LocalStorageLayer` is shared
87	/// between all fork-produced blocks.
88	pub parent: Option<Box<Block>>,
89}
90
91/// Handy type to allow specifying both number and hash as the fork point.
92#[derive(Clone, Copy)]
93pub enum BlockForkPoint {
94	/// Fork at a specific block number.
95	Number(u32),
96	/// Fork at a specific block hash.
97	Hash(H256),
98}
99
100impl From<u32> for BlockForkPoint {
101	fn from(number: u32) -> Self {
102		Self::Number(number)
103	}
104}
105
106impl From<H256> for BlockForkPoint {
107	fn from(hash: H256) -> Self {
108		Self::Hash(hash)
109	}
110}
111
112impl Block {
113	/// Create a new block at a fork point from a live chain.
114	///
115	/// This is the entry point for creating a forked chain. It fetches the block
116	/// header from the remote chain and sets up a [`LocalStorageLayer`] for tracking
117	/// subsequent modifications.
118	///
119	/// # Arguments
120	///
121	/// * `endpoint` - RPC client url.
122	/// * `cache` - Storage cache for persisting fetched and modified values
123	/// * `block_fork_point` - Hash or number of the block to fork from
124	///
125	/// # Returns
126	///
127	/// A new `Block` representing the fork point, with an empty extrinsics list
128	/// (since we're forking from existing chain state, not producing new blocks).
129	pub async fn fork_point(
130		endpoint: &Url,
131		cache: StorageCache,
132		block_fork_point: BlockForkPoint,
133	) -> Result<Self, BlockError> {
134		// Fetch header from remote chain
135		let rpc = ForkRpcClient::connect(endpoint).await?;
136		let (block_hash, header) = match block_fork_point {
137			BlockForkPoint::Number(block_number) => {
138				let (block_hash, block) =
139					if let Some(block_by_number) = rpc.block_by_number(block_number).await? {
140						block_by_number
141					} else {
142						return Err(BlockError::BlockNumberNotFound(block_number));
143					};
144				(block_hash, block.header)
145			},
146			BlockForkPoint::Hash(block_hash) => (
147				block_hash,
148				rpc.header(block_hash)
149					.await
150					.map_err(|_| BlockError::BlockHashNotFound(block_hash))?,
151			),
152		};
153		let block_number = header.number;
154		let parent_hash = header.parent_hash;
155
156		// Fetch full block to get extrinsics (needed for parachain inherents)
157		let extrinsics = rpc
158			.block_by_hash(block_hash)
159			.await?
160			.map(|block| block.extrinsics.into_iter().map(|ext| ext.0.to_vec()).collect::<Vec<_>>())
161			.unwrap_or_default();
162
163		// Fetch and decode runtime metadata
164		let metadata = rpc.metadata(block_hash).await?;
165
166		// Create storage layers (metadata is stored in LocalStorageLayer)
167		let remote = RemoteStorageLayer::new(rpc, cache);
168		let storage = LocalStorageLayer::new(remote, block_number, block_hash, metadata);
169
170		// Encode header for storage
171		let header_encoded = header.encode();
172
173		Ok(Self {
174			number: block_number,
175			hash: block_hash,
176			parent_hash,
177			header: header_encoded,
178			extrinsics, // Extrinsics from the forked block (needed for parachain inherents)
179			storage,
180			parent: None,
181		})
182	}
183
184	/// Create a new child block with the given hash, header, and extrinsics.
185	///
186	/// This commits the parent's storage modifications and creates a new block
187	/// that shares the same storage layer (including metadata versions).
188	///
189	/// # Arguments
190	///
191	/// * `hash` - The block hash
192	/// * `header` - The encoded block header
193	/// * `extrinsics` - The extrinsics (transactions) in this block
194	///
195	/// # Note
196	///
197	/// The child block shares the same storage layer as the parent, including
198	/// metadata versions. If a runtime upgrade occurred (`:code` storage changed),
199	/// the new metadata should be registered via `storage.register_metadata_version()`.
200	pub async fn child(
201		&mut self,
202		hash: H256,
203		header: Vec<u8>,
204		extrinsics: Vec<Vec<u8>>,
205	) -> Result<Self, BlockError> {
206		self.storage.commit().await?;
207		Ok(Self {
208			number: self.number + 1,
209			hash,
210			parent_hash: self.hash,
211			header,
212			extrinsics,
213			storage: self.storage.clone(),
214			parent: Some(Box::new(self.clone())),
215		})
216	}
217
218	/// Create a mocked Block for executing runtime calls on historical blocks.
219	///
220	/// This block uses the real block hash and number (for correct storage queries)
221	/// but placeholder values for other fields since the executor only needs storage access.
222	/// The storage layer delegates to remote for historical data.
223	///
224	/// # Arguments
225	///
226	/// * `hash` - The real block hash being queried
227	/// * `number` - The actual block number (needed for correct storage queries)
228	/// * `storage` - Storage layer that delegates to remote for historical data
229	pub fn mocked_for_call(hash: H256, number: u32, storage: LocalStorageLayer) -> Self {
230		Self {
231			number,
232			hash,
233			parent_hash: H256::zero(),
234			header: vec![],
235			extrinsics: vec![],
236			storage,
237			parent: None,
238		}
239	}
240
241	/// Get a reference to the storage layer.
242	///
243	/// Use this to read storage values at this block's height.
244	///
245	/// # Example
246	///
247	/// ```ignore
248	/// let value = block.storage().get(block.number, &key).await?;
249	/// ```
250	pub fn storage(&self) -> &LocalStorageLayer {
251		&self.storage
252	}
253
254	/// Get a mutable reference to the storage layer.
255	///
256	/// Use this to modify storage values. Modifications are tracked locally
257	/// and can be committed using [`LocalStorageLayer::commit`].
258	///
259	/// # Example
260	///
261	/// ```ignore
262	/// block.storage_mut().set(&key, Some(&value))?;
263	/// block.storage_mut().commit().await?;
264	/// ```
265	pub fn storage_mut(&mut self) -> &mut LocalStorageLayer {
266		&mut self.storage
267	}
268
269	/// Get the runtime metadata for this block.
270	///
271	/// This provides access to pallet and call indices for dynamic extrinsic
272	/// encoding. Use this in inherent providers to look up pallet indices
273	/// instead of relying on hardcoded values.
274	///
275	/// Returns an `Arc<Metadata>` which can be used like a reference (via `Deref`).
276	/// The metadata is shared across all blocks that use the same runtime version,
277	/// avoiding unnecessary cloning.
278	///
279	/// # Example
280	///
281	/// ```ignore
282	/// let metadata = block.metadata().await?;
283	/// let pallet = metadata.pallet_by_name("Timestamp")?;
284	/// let pallet_index = pallet.index();
285	/// let call_variant = pallet.call_variant_by_name("set")?;
286	/// let call_index = call_variant.index;
287	/// ```
288	pub async fn metadata(&self) -> Result<Arc<Metadata>, BlockError> {
289		Ok(self.storage.metadata_at(self.number).await?)
290	}
291
292	/// Get the runtime code (`:code`) for this block.
293	///
294	/// Retrieves the WASM runtime code from the storage layer, which handles
295	/// the layered lookup (local modifications → cache → remote).
296	///
297	/// # Returns
298	///
299	/// The runtime WASM code as bytes.
300	///
301	/// # Errors
302	///
303	/// Returns [`BlockError::RuntimeCodeNotFound`] if the `:code` key is not
304	/// found in storage.
305	///
306	/// # Example
307	///
308	/// ```ignore
309	/// let runtime_code = block.runtime_code().await?;
310	/// let executor = RuntimeExecutor::new(runtime_code)?;
311	/// ```
312	pub async fn runtime_code(&self) -> Result<Vec<u8>, BlockError> {
313		let code_key = sp_core::storage::well_known_keys::CODE;
314		self.storage()
315			.get(self.number, code_key)
316			.await?
317			.and_then(|v| v.value.clone())
318			.ok_or(BlockError::RuntimeCodeNotFound)
319	}
320}
321
322#[cfg(test)]
323mod tests {
324	use super::*;
325
326	#[test]
327	fn from_u32_creates_number_variant() {
328		let fork_point: BlockForkPoint = 42u32.into();
329		assert!(matches!(fork_point, BlockForkPoint::Number(42)));
330	}
331
332	#[test]
333	fn from_h256_creates_hash_variant() {
334		let hash = H256::from([0xab; 32]);
335		let fork_point: BlockForkPoint = hash.into();
336		assert!(matches!(fork_point, BlockForkPoint::Hash(h) if h == hash));
337	}
338}