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}