use crate::{BlockError, ForkRpcClient, LocalStorageLayer, RemoteStorageLayer, StorageCache};
use std::sync::Arc;
use subxt::{Metadata, config::substrate::H256, ext::codec::Encode};
use url::Url;
#[derive(Clone, Debug)]
pub struct Block {
pub number: u32,
pub hash: H256,
pub parent_hash: H256,
pub header: Vec<u8>,
pub extrinsics: Vec<Vec<u8>>,
storage: LocalStorageLayer,
pub parent: Option<Box<Block>>,
}
#[derive(Clone, Copy)]
pub enum BlockForkPoint {
Number(u32),
Hash(H256),
}
impl From<u32> for BlockForkPoint {
fn from(number: u32) -> Self {
Self::Number(number)
}
}
impl From<H256> for BlockForkPoint {
fn from(hash: H256) -> Self {
Self::Hash(hash)
}
}
impl Block {
pub async fn fork_point(
endpoint: &Url,
cache: StorageCache,
block_fork_point: BlockForkPoint,
) -> Result<Self, BlockError> {
let rpc = ForkRpcClient::connect(endpoint).await?;
let (block_hash, header) = match block_fork_point {
BlockForkPoint::Number(block_number) => {
let (block_hash, block) =
if let Some(block_by_number) = rpc.block_by_number(block_number).await? {
block_by_number
} else {
return Err(BlockError::BlockNumberNotFound(block_number));
};
(block_hash, block.header)
},
BlockForkPoint::Hash(block_hash) => (
block_hash,
rpc.header(block_hash)
.await
.map_err(|_| BlockError::BlockHashNotFound(block_hash))?,
),
};
let block_number = header.number;
let parent_hash = header.parent_hash;
let extrinsics = rpc
.block_by_hash(block_hash)
.await?
.map(|block| block.extrinsics.into_iter().map(|ext| ext.0.to_vec()).collect::<Vec<_>>())
.unwrap_or_default();
let metadata = rpc.metadata(block_hash).await?;
let remote = RemoteStorageLayer::new(rpc, cache);
let storage = LocalStorageLayer::new(remote, block_number, block_hash, metadata);
let header_encoded = header.encode();
Ok(Self {
number: block_number,
hash: block_hash,
parent_hash,
header: header_encoded,
extrinsics, storage,
parent: None,
})
}
pub async fn child(
&mut self,
hash: H256,
header: Vec<u8>,
extrinsics: Vec<Vec<u8>>,
) -> Result<Self, BlockError> {
self.storage.commit().await?;
Ok(Self {
number: self.number + 1,
hash,
parent_hash: self.hash,
header,
extrinsics,
storage: self.storage.clone(),
parent: Some(Box::new(self.clone())),
})
}
pub fn mocked_for_call(hash: H256, number: u32, storage: LocalStorageLayer) -> Self {
Self {
number,
hash,
parent_hash: H256::zero(),
header: vec![],
extrinsics: vec![],
storage,
parent: None,
}
}
pub fn storage(&self) -> &LocalStorageLayer {
&self.storage
}
pub fn storage_mut(&mut self) -> &mut LocalStorageLayer {
&mut self.storage
}
pub async fn metadata(&self) -> Result<Arc<Metadata>, BlockError> {
Ok(self.storage.metadata_at(self.number).await?)
}
pub async fn runtime_code(&self) -> Result<Vec<u8>, BlockError> {
let code_key = sp_core::storage::well_known_keys::CODE;
self.storage()
.get(self.number, code_key)
.await?
.and_then(|v| v.value.clone())
.ok_or(BlockError::RuntimeCodeNotFound)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_u32_creates_number_variant() {
let fork_point: BlockForkPoint = 42u32.into();
assert!(matches!(fork_point, BlockForkPoint::Number(42)));
}
#[test]
fn from_h256_creates_hash_variant() {
let hash = H256::from([0xab; 32]);
let fork_point: BlockForkPoint = hash.into();
assert!(matches!(fork_point, BlockForkPoint::Hash(h) if h == hash));
}
mod sequential {
use super::*;
use crate::testing::TestContext;
#[tokio::test(flavor = "multi_thread")]
async fn fork_point_with_hash_creates_block_with_correct_metadata() {
let ctx = TestContext::for_storage().await;
let expected_parent_hash =
ctx.rpc().header(ctx.block_hash()).await.unwrap().parent_hash;
let block =
Block::fork_point(&ctx.endpoint, ctx.cache().clone(), ctx.block_hash().into())
.await
.unwrap();
assert_eq!(block.number, ctx.block_number());
assert_eq!(block.hash, ctx.block_hash());
assert_eq!(block.parent_hash, expected_parent_hash);
assert!(!block.header.is_empty());
assert!(block.parent.is_none());
}
#[tokio::test(flavor = "multi_thread")]
async fn fork_point_with_non_existent_hash_returns_error() {
let ctx = TestContext::for_storage().await;
let non_existent_hash = H256::from([0xde; 32]);
let result =
Block::fork_point(&ctx.endpoint, ctx.cache().clone(), non_existent_hash.into())
.await;
assert!(
matches!(result, Err(BlockError::BlockHashNotFound(h)) if h == non_existent_hash)
);
}
#[tokio::test(flavor = "multi_thread")]
async fn fork_point_with_number_creates_block_with_correct_metadata() {
let ctx = TestContext::for_storage().await;
let expected_parent_hash =
ctx.rpc().header(ctx.block_hash()).await.unwrap().parent_hash;
let block =
Block::fork_point(&ctx.endpoint, ctx.cache().clone(), ctx.block_number().into())
.await
.unwrap();
assert_eq!(block.number, ctx.block_number());
assert_eq!(block.hash, ctx.block_hash());
assert_eq!(block.parent_hash, expected_parent_hash);
assert!(!block.header.is_empty());
assert!(block.parent.is_none());
}
#[tokio::test(flavor = "multi_thread")]
async fn fork_point_with_non_existent_number_returns_error() {
let ctx = TestContext::for_storage().await;
let non_existent_number = u32::MAX;
let result =
Block::fork_point(&ctx.endpoint, ctx.cache().clone(), non_existent_number.into())
.await;
assert!(
matches!(result, Err(BlockError::BlockNumberNotFound(n)) if n == non_existent_number)
);
}
#[tokio::test(flavor = "multi_thread")]
async fn child_creates_block_with_correct_metadata() {
let ctx = TestContext::for_storage().await;
let mut parent =
Block::fork_point(&ctx.endpoint, ctx.cache().clone(), ctx.block_hash().into())
.await
.unwrap();
let child_hash = H256::from([0x42; 32]);
let child_header = vec![1, 2, 3, 4];
let child_extrinsics = vec![vec![5, 6, 7]];
let child = parent
.child(child_hash, child_header.clone(), child_extrinsics.clone())
.await
.unwrap();
assert_eq!(child.number, parent.number + 1);
assert_eq!(child.hash, child_hash);
assert_eq!(child.parent_hash, parent.hash);
assert_eq!(child.header, child_header);
assert_eq!(child.extrinsics, child_extrinsics);
assert_eq!(child.parent.unwrap().number, parent.number);
}
async fn get_storage_value(block: Block, number: u32, key: &[u8]) -> Vec<u8> {
block
.storage()
.get(number, key)
.await
.unwrap()
.as_deref()
.unwrap()
.value
.clone()
.unwrap()
}
#[tokio::test(flavor = "multi_thread")]
async fn child_commits_parent_storage() {
let ctx = TestContext::for_storage().await;
let mut parent =
Block::fork_point(&ctx.endpoint, ctx.cache().clone(), ctx.block_hash().into())
.await
.unwrap();
let key = b"committed_key";
let value = b"committed_value";
parent.storage_mut().set(key, Some(value)).unwrap();
let mut child = parent.child(H256::from([0x42; 32]), vec![], vec![]).await.unwrap();
let value2 = b"committed_value2";
child.storage_mut().set(key, Some(value2)).unwrap();
assert_eq!(get_storage_value(child.clone(), child.number + 1, key).await, value2);
assert_eq!(get_storage_value(child.clone(), child.number, key).await, value);
}
#[tokio::test(flavor = "multi_thread")]
async fn child_storage_inherits_parent_modifications() {
let ctx = TestContext::for_storage().await;
let mut parent =
Block::fork_point(&ctx.endpoint, ctx.cache().clone(), ctx.block_hash().into())
.await
.unwrap();
let key = b"inherited_key";
let value = b"inherited_value";
parent.storage_mut().set(key, Some(value)).unwrap();
let child = parent.child(H256::from([0x42; 32]), vec![], vec![]).await.unwrap();
assert_eq!(get_storage_value(child.clone(), child.number, key).await, value);
}
}
}