// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
use super::*;
use crate::{
chain_head::{api::ChainHeadApiClient, event::MethodResponse, test_utils::ChainHeadMockClient},
common::events::{StorageQuery, StorageQueryType, StorageResultType},
hex_string,
};
use assert_matches::assert_matches;
use codec::{Decode, Encode};
use futures::Future;
use jsonrpsee::{
core::{
client::Subscription as RpcClientSubscription, server::Subscription as RpcSubscription,
},
rpc_params, MethodsError as Error, RpcModule,
};
use pezsc_block_builder::BlockBuilderBuilder;
use pezsc_client_api::ChildInfo;
use pezsc_rpc::testing::TokioTestExecutor;
use pezsc_service::client::new_with_backend;
use pezsp_blockchain::HeaderBackend;
use pezsp_consensus::BlockOrigin;
use pezsp_core::{
storage::well_known_keys::{self, CODE},
Blake2Hasher, Hasher,
};
use pezsp_runtime::traits::Block as BlockT;
use pezsp_version::RuntimeVersion;
use std::{
collections::{HashMap, HashSet, VecDeque},
fmt::Debug,
sync::Arc,
time::Duration,
};
use bizinikiwi_test_runtime::Transfer;
use bizinikiwi_test_runtime_client::{
prelude::*, runtime, runtime::RuntimeApi, Backend, BlockBuilderExt, Client,
ClientBlockImportExt, GenesisInit,
};
type Header = bizinikiwi_test_runtime_client::runtime::Header;
type Block = bizinikiwi_test_runtime_client::runtime::Block;
const MAX_PINNED_BLOCKS: usize = 32;
const MAX_PINNED_SECS: u64 = 60;
const MAX_OPERATIONS: usize = 16;
const MAX_LAGGING_DISTANCE: usize = 128;
const MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION: usize = 4;
const INVALID_HASH: [u8; 32] = [1; 32];
const KEY: &[u8] = b":mock";
const VALUE: &[u8] = b"hello world";
const CHILD_STORAGE_KEY: &[u8] = b"child";
const CHILD_VALUE: &[u8] = b"child value";
const DOES_NOT_PRODUCE_EVENTS_SECONDS: u64 = 10;
/// Start an RPC server with the chainHead module.
pub async fn run_server() -> std::net::SocketAddr {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let api = ChainHead::new(
client,
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_follow_subscriptions_per_connection: 1,
max_lagging_distance: MAX_LAGGING_DISTANCE,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let server = jsonrpsee::server::ServerBuilder::default().build("127.0.0.1:0").await.unwrap();
let addr = server.local_addr().unwrap();
let handle = server.start(api);
tokio::spawn(handle.stopped());
addr
}
async fn get_next_event(sub: &mut RpcSubscription) -> T {
let (event, _sub_id) = tokio::time::timeout(std::time::Duration::from_secs(60), sub.next())
.await
.unwrap()
.unwrap()
.unwrap();
event
}
async fn does_not_produce_event(
sub: &mut RpcSubscription,
duration: std::time::Duration,
) {
tokio::time::timeout(duration, sub.next::()).await.unwrap_err();
}
async fn run_with_timeout(future: F) -> ::Output {
tokio::time::timeout(std::time::Duration::from_secs(60 * 10), future)
.await
.unwrap()
}
async fn setup_api() -> (
Arc>,
RpcModule>>,
RpcSubscription,
String,
Block,
) {
let child_info = ChildInfo::new_default(CHILD_STORAGE_KEY);
let builder = TestClientBuilder::new().add_extra_child_storage(
&child_info,
KEY.to_vec(),
CHILD_VALUE.to_vec(),
);
let backend = builder.backend();
let client = Arc::new(builder.build());
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [true]).await.unwrap();
let sub_id = sub.subscription_id();
let sub_id = serde_json::to_string(&sub_id).unwrap();
let block = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
// Ensure the imported block is propagated and pinned for this subscription.
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::Initialized(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
(client, api, sub, sub_id, block)
}
async fn import_block(
client: Arc>,
parent_hash: ::Hash,
parent_number: u64,
) -> Block {
let block = BlockBuilderBuilder::new(&*client)
.on_parent_block(parent_hash)
.with_parent_block_number(parent_number)
.build()
.unwrap()
.build()
.unwrap()
.block;
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
block
}
async fn import_best_block_with_tx(
client: Arc>,
parent_hash: ::Hash,
parent_number: u64,
tx: Transfer,
) -> Block {
let mut block_builder = BlockBuilderBuilder::new(&*client)
.on_parent_block(parent_hash)
.with_parent_block_number(parent_number)
.build()
.unwrap();
block_builder.push_transfer(tx).unwrap();
let block = block_builder.build().unwrap().block;
client.import_as_best(BlockOrigin::Own, block.clone()).await.unwrap();
block
}
/// Check the subscription produces a new block and a best block event.
///
/// The macro is used instead of a fn to preserve the lines of code in case of panics.
macro_rules! check_new_and_best_block_events {
($sub:expr, $block_hash:expr, $parent_hash:expr) => {
let event: FollowEvent = get_next_event($sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", $block_hash),
parent_block_hash: format!("{:?}", $parent_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event($sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", $block_hash),
});
assert_eq!(event, expected);
};
}
#[tokio::test]
async fn follow_subscription_produces_blocks() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let finalized_hash = client.info().finalized_hash;
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [false]).await.unwrap();
// Initialized must always be reported first.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Initialized(Initialized {
finalized_block_hashes: vec![format!("{:?}", finalized_hash)],
finalized_block_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let block = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
let best_hash = block.header.hash();
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", best_hash),
parent_block_hash: format!("{:?}", finalized_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", best_hash),
});
assert_eq!(event, expected);
client.finalize_block(best_hash, None).unwrap();
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Finalized(Finalized {
finalized_block_hashes: vec![format!("{:?}", best_hash)],
pruned_block_hashes: vec![],
});
assert_eq!(event, expected);
}
#[tokio::test]
async fn follow_with_runtime() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let finalized_hash = client.info().finalized_hash;
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [true]).await.unwrap();
// Initialized must always be reported first.
let event: FollowEvent = get_next_event(&mut sub).await;
// it is basically json-encoded bizinikiwi_test_runtime_client::runtime::VERSION
let runtime_str = "{\"specName\":\"test\",\"implName\":\"parity-test\",\"authoringVersion\":1,\
\"specVersion\":2,\"implVersion\":2,\"apis\":[[\"0xdf6acb689907609b\",5],\
[\"0x37e397fc7c91f5e4\",2],[\"0xd2bc9897eed08f15\",3],[\"0x40fe3ad401f8959a\",6],\
[\"0xbc9d89904f5b923f\",1],[\"0xc6e9a76309f39b09\",2],[\"0xdd718d5cc53262d4\",1],\
[\"0xcbca25e39f142387\",2],[\"0xf78b278be53f454c\",2],[\"0xab3c0572291feb8b\",1],\
[\"0xed99c5acb25eedf5\",3],[\"0xfbc577b9d747efd6\",1]],\"transactionVersion\":1,\"systemVersion\":1}";
let runtime: RuntimeVersion = serde_json::from_str(runtime_str).unwrap();
let finalized_block_runtime =
Some(RuntimeEvent::Valid(RuntimeVersionEvent { spec: runtime.clone().into() }));
// Runtime must always be reported with the first event.
let expected = FollowEvent::Initialized(Initialized {
finalized_block_hashes: vec![format!("{:?}", finalized_hash)],
finalized_block_runtime,
with_runtime: false,
});
pretty_assertions::assert_eq!(event, expected);
// Import a new block without runtime changes.
// The runtime field must be None in this case.
let block = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
let best_hash = block.hash();
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", best_hash),
parent_block_hash: format!("{:?}", finalized_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", best_hash),
});
assert_eq!(event, expected);
client.finalize_block(best_hash, None).unwrap();
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Finalized(Finalized {
finalized_block_hashes: vec![format!("{:?}", best_hash)],
pruned_block_hashes: vec![],
});
assert_eq!(event, expected);
let finalized_hash = best_hash;
// The `RuntimeVersion` is embedded into the WASM blob at the `runtime_version`
// section. Modify the `RuntimeVersion` and commit the changes to a new block.
// The RPC must notify the runtime event change.
let wasm = pezsp_maybe_compressed_blob::decompress(
runtime::wasm_binary_unwrap(),
pezsp_maybe_compressed_blob::CODE_BLOB_BOMB_LIMIT,
)
.unwrap();
// Update the runtime spec version.
let mut runtime = runtime;
runtime.spec_version += 1;
let embedded = pezsp_version::embed::embed_runtime_version(&wasm, runtime.clone()).unwrap();
let wasm = pezsp_maybe_compressed_blob::compress_strongly(
&embedded,
pezsp_maybe_compressed_blob::CODE_BLOB_BOMB_LIMIT,
)
.unwrap();
let mut builder = BlockBuilderBuilder::new(&*client)
.on_parent_block(best_hash)
.with_parent_block_number(1)
.build()
.unwrap();
builder.push_storage_change(CODE.to_vec(), Some(wasm)).unwrap();
let block = builder.build().unwrap().block;
let best_hash = block.header.hash();
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
let new_runtime =
Some(RuntimeEvent::Valid(RuntimeVersionEvent { spec: runtime.clone().into() }));
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", best_hash),
parent_block_hash: format!("{:?}", finalized_hash),
new_runtime,
with_runtime: false,
});
assert_eq!(event, expected);
}
#[tokio::test]
async fn get_header() {
let (_client, api, _sub, sub_id, block) = setup_api().await;
let block_hash = format!("{:?}", block.header.hash());
let invalid_hash = hex_string(&INVALID_HASH);
// Invalid subscription ID must produce no results.
let res: Option = api
.call("chainHead_v1_header", ["invalid_sub_id", &invalid_hash])
.await
.unwrap();
assert!(res.is_none());
// Valid subscription with invalid block hash will error.
let err = api
.call::<_, serde_json::Value>("chainHead_v1_header", [&sub_id, &invalid_hash])
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(ref err) if err.code() == super::error::rpc_spec_v2::INVALID_BLOCK_ERROR && err.message() == "Invalid block hash"
);
// Obtain the valid header.
let res: String = api.call("chainHead_v1_header", [&sub_id, &block_hash]).await.unwrap();
let bytes = array_bytes::hex2bytes(&res).unwrap();
let header: Header = Decode::decode(&mut &bytes[..]).unwrap();
assert_eq!(header, block.header);
}
#[tokio::test]
async fn get_body() {
let (client, api, mut block_sub, sub_id, block) = setup_api().await;
let block_hash = format!("{:?}", block.header.hash());
let invalid_hash = hex_string(&INVALID_HASH);
// Subscription ID is invalid.
let response: MethodResponse =
api.call("chainHead_v1_body", ["invalid_sub_id", &invalid_hash]).await.unwrap();
assert_matches!(response, MethodResponse::LimitReached);
// Block hash is invalid.
let err = api
.call::<_, serde_json::Value>("chainHead_v1_body", [&sub_id, &invalid_hash])
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(ref err) if err.code() == super::error::rpc_spec_v2::INVALID_BLOCK_ERROR && err.message() == "Invalid block hash"
);
// Valid call.
let response: MethodResponse =
api.call("chainHead_v1_body", [&sub_id, &block_hash]).await.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// Response propagated to `chainHead_follow`.
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationBodyDone(done) if done.operation_id == operation_id && done.value.is_empty()
);
// Import a block with extrinsics.
let mut builder = BlockBuilderBuilder::new(&*client)
.on_parent_block(block.hash())
.with_parent_block_number(1)
.build()
.unwrap();
builder
.push_transfer(runtime::Transfer {
from: Sr25519Keyring::Alice.into(),
to: Sr25519Keyring::Ferdie.into(),
amount: 42,
nonce: 0,
})
.unwrap();
let block = builder.build().unwrap().block;
let block_hash = format!("{:?}", block.header.hash());
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
// Ensure the imported block is propagated and pinned for this subscription.
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::BestBlockChanged(_)
);
// Valid call to a block with extrinsics.
let response: MethodResponse =
api.call("chainHead_v1_body", [&sub_id, &block_hash]).await.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// Response propagated to `chainHead_follow`.
let expected_tx = hex_string(&block.extrinsics[0].encode());
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationBodyDone(done) if done.operation_id == operation_id && done.value == vec![expected_tx]
);
}
#[tokio::test]
async fn call_runtime() {
let (_client, api, mut block_sub, sub_id, block) = setup_api().await;
let block_hash = format!("{:?}", block.header.hash());
let invalid_hash = hex_string(&INVALID_HASH);
// Subscription ID is invalid.
let response: MethodResponse = api
.call("chainHead_v1_call", ["invalid_sub_id", &block_hash, "BabeApi_current_epoch", "0x00"])
.await
.unwrap();
assert_matches!(response, MethodResponse::LimitReached);
// Block hash is invalid.
let err = api
.call::<_, serde_json::Value>(
"chainHead_v1_call",
[&sub_id, &invalid_hash, "BabeApi_current_epoch", "0x00"],
)
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(ref err) if err.code() == super::error::rpc_spec_v2::INVALID_BLOCK_ERROR && err.message() == "Invalid block hash"
);
// Pass an invalid parameters that cannot be decode.
let err = api
.call::<_, serde_json::Value>(
"chainHead_v1_call",
// 0x0 is invalid.
[&sub_id, &block_hash, "BabeApi_current_epoch", "0x0"],
)
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(err) if err.code() == super::error::json_rpc_spec::INVALID_PARAM_ERROR && err.message().contains("Invalid parameter")
);
// Valid call.
let alice_id = Sr25519Keyring::Alice.to_account_id();
// Hex encoded scale encoded bytes representing the call parameters.
let call_parameters = hex_string(&alice_id.encode());
let response: MethodResponse = api
.call(
"chainHead_v1_call",
[&sub_id, &block_hash, "AccountNonceApi_account_nonce", &call_parameters],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// Response propagated to `chainHead_follow`.
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationCallDone(done) if done.operation_id == operation_id && done.output == "0x0000000000000000"
);
// The `current_epoch` takes no parameters and not draining the input buffer
// will cause the execution to fail.
let response: MethodResponse = api
.call("chainHead_v1_call", [&sub_id, &block_hash, "BabeApi_current_epoch", "0x00"])
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// Error propagated to `chainHead_follow`.
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationError(error) if error.operation_id == operation_id && error.error.contains("Execution failed")
);
}
#[tokio::test]
async fn call_runtime_without_flag() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [false]).await.unwrap();
let sub_id = sub.subscription_id();
let sub_id = serde_json::to_string(&sub_id).unwrap();
let block = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_hash = format!("{:?}", block.header.hash());
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
// Ensure the imported block is propagated and pinned for this subscription.
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::Initialized(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
// Valid runtime call on a subscription started with `with_runtime` false.
let alice_id = Sr25519Keyring::Alice.to_account_id();
let call_parameters = hex_string(&alice_id.encode());
let err = api
.call::<_, serde_json::Value>(
"chainHead_v1_call",
[&sub_id, &block_hash, "AccountNonceApi_account_nonce", &call_parameters],
)
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(ref err) if err.code() == super::error::rpc_spec_v2::INVALID_RUNTIME_CALL && err.message().contains("subscription was started with `withRuntime` set to `false`")
);
}
#[tokio::test]
async fn get_storage_hash() {
let (client, api, mut block_sub, sub_id, block) = setup_api().await;
let block_hash = format!("{:?}", block.header.hash());
let invalid_hash = hex_string(&INVALID_HASH);
let key = hex_string(&KEY);
// Subscription ID is invalid.
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
"invalid_sub_id",
&invalid_hash,
vec![StorageQuery {
key: key.clone(),
query_type: StorageQueryType::Hash,
pagination_start_key: None
}]
],
)
.await
.unwrap();
assert_matches!(response, MethodResponse::LimitReached);
// Block hash is invalid.
let err = api
.call::<_, serde_json::Value>(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&invalid_hash,
vec![StorageQuery {
key: key.clone(),
query_type: StorageQueryType::Hash,
pagination_start_key: None
}]
],
)
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(ref err) if err.code() == super::error::rpc_spec_v2::INVALID_BLOCK_ERROR && err.message() == "Invalid block hash"
);
// Valid call without storage at the key.
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&block_hash,
vec![StorageQuery {
key: key.clone(),
query_type: StorageQueryType::Hash,
pagination_start_key: None
}]
],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// The `Done` event is generated directly since the key does not have any value associated.
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageDone(done) if done.operation_id == operation_id
);
// Import a new block with storage changes.
let mut builder = BlockBuilderBuilder::new(&*client)
.on_parent_block(block.hash())
.with_parent_block_number(1)
.build()
.unwrap();
builder.push_storage_change(KEY.to_vec(), Some(VALUE.to_vec())).unwrap();
let block = builder.build().unwrap().block;
let block_hash = format!("{:?}", block.header.hash());
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
// Ensure the imported block is propagated and pinned for this subscription.
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::BestBlockChanged(_)
);
// Valid call with storage at the key.
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&block_hash,
vec![StorageQuery {
key: key.clone(),
query_type: StorageQueryType::Hash,
pagination_start_key: None
}]
],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
let expected_hash = format!("{:?}", Blake2Hasher::hash(&VALUE));
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id &&
res.items.len() == 1 &&
res.items[0].key == key && res.items[0].result == StorageResultType::Hash(expected_hash)
);
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageDone(done) if done.operation_id == operation_id
);
// Child value set in `setup_api`.
let child_info = hex_string(&CHILD_STORAGE_KEY);
let genesis_hash = format!("{:?}", client.genesis_hash());
// Valid call with storage at the key.
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&genesis_hash,
vec![StorageQuery {
key: key.clone(),
query_type: StorageQueryType::Hash,
pagination_start_key: None
}],
&child_info
],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
let expected_hash = format!("{:?}", Blake2Hasher::hash(&CHILD_VALUE));
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id &&
res.items.len() == 1 &&
res.items[0].key == key && res.items[0].result == StorageResultType::Hash(expected_hash)
);
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageDone(done) if done.operation_id == operation_id
);
}
#[tokio::test]
async fn get_storage_multi_query_iter() {
let (client, api, mut block_sub, sub_id, block) = setup_api().await;
let key = hex_string(&KEY);
// Import a new block with storage changes.
let mut builder = BlockBuilderBuilder::new(&*client)
.on_parent_block(block.hash())
.with_parent_block_number(1)
.build()
.unwrap();
builder.push_storage_change(KEY.to_vec(), Some(VALUE.to_vec())).unwrap();
let block = builder.build().unwrap().block;
let block_hash = format!("{:?}", block.header.hash());
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
// Ensure the imported block is propagated and pinned for this subscription.
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::BestBlockChanged(_)
);
// Valid call with storage at the key.
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&block_hash,
vec![
StorageQuery {
key: key.clone(),
query_type: StorageQueryType::DescendantsHashes,
pagination_start_key: None
},
StorageQuery {
key: key.clone(),
query_type: StorageQueryType::DescendantsValues,
pagination_start_key: None
}
]
],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
let expected_hash = format!("{:?}", Blake2Hasher::hash(&VALUE));
let expected_value = hex_string(&VALUE);
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id &&
res.items.len() == 1 &&
res.items[0].key == key &&
res.items[0].result == StorageResultType::Hash(expected_hash)
);
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id &&
res.items.len() == 1 &&
res.items[0].key == key &&
res.items[0].result == StorageResultType::Value(expected_value)
);
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageDone(done) if done.operation_id == operation_id
);
// Child value set in `setup_api`.
let child_info = hex_string(&CHILD_STORAGE_KEY);
let genesis_hash = format!("{:?}", client.genesis_hash());
let expected_hash = format!("{:?}", Blake2Hasher::hash(&CHILD_VALUE));
let expected_value = hex_string(&CHILD_VALUE);
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&genesis_hash,
vec![
StorageQuery {
key: key.clone(),
query_type: StorageQueryType::DescendantsHashes,
pagination_start_key: None
},
StorageQuery {
key: key.clone(),
query_type: StorageQueryType::DescendantsValues,
pagination_start_key: None
}
],
&child_info
],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id &&
res.items.len() == 1 &&
res.items[0].key == key &&
res.items[0].result == StorageResultType::Hash(expected_hash)
);
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id &&
res.items.len() == 1 &&
res.items[0].key == key &&
res.items[0].result == StorageResultType::Value(expected_value)
);
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageDone(done) if done.operation_id == operation_id
);
}
#[tokio::test]
async fn get_storage_value() {
let (client, api, mut block_sub, sub_id, block) = setup_api().await;
let block_hash = format!("{:?}", block.hash());
let invalid_hash = hex_string(&INVALID_HASH);
let key = hex_string(&KEY);
// Subscription ID is invalid.
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
"invalid_sub_id",
&invalid_hash,
vec![StorageQuery {
key: key.clone(),
query_type: StorageQueryType::Value,
pagination_start_key: None
}]
],
)
.await
.unwrap();
assert_matches!(response, MethodResponse::LimitReached);
// Block hash is invalid.
let err = api
.call::<_, serde_json::Value>(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&invalid_hash,
vec![StorageQuery {
key: key.clone(),
query_type: StorageQueryType::Value,
pagination_start_key: None
}]
],
)
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(ref err) if err.code() == super::error::rpc_spec_v2::INVALID_BLOCK_ERROR && err.message() == "Invalid block hash"
);
// Valid call without storage at the key.
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&block_hash,
vec![StorageQuery {
key: key.clone(),
query_type: StorageQueryType::Value,
pagination_start_key: None
}]
],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// The `Done` event is generated directly since the key does not have any value associated.
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageDone(done) if done.operation_id == operation_id
);
// Import a new block with storage changes.
let mut builder = BlockBuilderBuilder::new(&*client)
.on_parent_block(block.hash())
.with_parent_block_number(1)
.build()
.unwrap();
builder.push_storage_change(KEY.to_vec(), Some(VALUE.to_vec())).unwrap();
let block = builder.build().unwrap().block;
let block_hash = format!("{:?}", block.hash());
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
// Ensure the imported block is propagated and pinned for this subscription.
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::BestBlockChanged(_)
);
// Valid call with storage at the key.
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&block_hash,
vec![StorageQuery {
key: key.clone(),
query_type: StorageQueryType::Value,
pagination_start_key: None
}]
],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
let expected_value = hex_string(&VALUE);
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id &&
res.items.len() == 1 &&
res.items[0].key == key && res.items[0].result == StorageResultType::Value(expected_value)
);
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageDone(done) if done.operation_id == operation_id
);
// Child value set in `setup_api`.
let child_info = hex_string(&CHILD_STORAGE_KEY);
let genesis_hash = format!("{:?}", client.genesis_hash());
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&genesis_hash,
vec![StorageQuery {
key: key.clone(),
query_type: StorageQueryType::Value,
pagination_start_key: None
}],
&child_info
],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
let expected_value = hex_string(&CHILD_VALUE);
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id &&
res.items.len() == 1 &&
res.items[0].key == key && res.items[0].result == StorageResultType::Value(expected_value)
);
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageDone(done) if done.operation_id == operation_id
);
}
#[tokio::test]
async fn get_storage_non_queryable_key() {
let (mut _client, api, mut block_sub, sub_id, block) = setup_api().await;
let block_hash = format!("{:?}", block.header.hash());
let key = hex_string(&KEY);
// Key is prefixed by CHILD_STORAGE_KEY_PREFIX.
let mut prefixed_key = well_known_keys::CHILD_STORAGE_KEY_PREFIX.to_vec();
prefixed_key.extend_from_slice(&KEY);
let prefixed_key = hex_string(&prefixed_key);
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&block_hash,
vec![StorageQuery {
key: prefixed_key,
query_type: StorageQueryType::Value,
pagination_start_key: None
}]
],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// The `Done` event is generated directly since the key is not queryable.
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageDone(done) if done.operation_id == operation_id
);
// Key is prefixed by DEFAULT_CHILD_STORAGE_KEY_PREFIX.
let mut prefixed_key = well_known_keys::DEFAULT_CHILD_STORAGE_KEY_PREFIX.to_vec();
prefixed_key.extend_from_slice(&KEY);
let prefixed_key = hex_string(&prefixed_key);
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&block_hash,
vec![StorageQuery {
key: prefixed_key,
query_type: StorageQueryType::Value,
pagination_start_key: None
}]
],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// The `Done` event is generated directly since the key is not queryable.
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageDone(done) if done.operation_id == operation_id
);
// Child key is prefixed by CHILD_STORAGE_KEY_PREFIX.
let mut prefixed_key = well_known_keys::CHILD_STORAGE_KEY_PREFIX.to_vec();
prefixed_key.extend_from_slice(CHILD_STORAGE_KEY);
let prefixed_key = hex_string(&prefixed_key);
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&block_hash,
vec![StorageQuery {
key: key.clone(),
query_type: StorageQueryType::Value,
pagination_start_key: None
}],
&prefixed_key
],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// The `Done` event is generated directly since the key is not queryable.
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageDone(done) if done.operation_id == operation_id
);
// Child key is prefixed by DEFAULT_CHILD_STORAGE_KEY_PREFIX.
let mut prefixed_key = well_known_keys::DEFAULT_CHILD_STORAGE_KEY_PREFIX.to_vec();
prefixed_key.extend_from_slice(CHILD_STORAGE_KEY);
let prefixed_key = hex_string(&prefixed_key);
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&block_hash,
vec![StorageQuery {
key,
query_type: StorageQueryType::Value,
pagination_start_key: None
}],
&prefixed_key
],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// The `Done` event is generated directly since the key is not queryable.
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageDone(done) if done.operation_id == operation_id
);
}
#[tokio::test]
async fn unique_operation_ids() {
let (mut _client, api, mut block_sub, sub_id, block) = setup_api().await;
let block_hash = format!("{:?}", block.header.hash());
let mut op_ids = HashSet::new();
// Ensure that operation IDs are unique for multiple method calls.
for _ in 0..5 {
// Valid `chainHead_v1_body` call.
let response: MethodResponse =
api.call("chainHead_v1_body", [&sub_id, &block_hash]).await.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationBodyDone(done) if done.operation_id == operation_id && done.value.is_empty()
);
// Ensure uniqueness.
assert!(op_ids.insert(operation_id));
// Valid `chainHead_v1_storage` call.
let key = hex_string(&KEY);
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&block_hash,
vec![StorageQuery {
key: key.clone(),
query_type: StorageQueryType::Value,
pagination_start_key: None
}]
],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// The `Done` event is generated directly since the key does not have any value associated.
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationStorageDone(done) if done.operation_id == operation_id
);
// Ensure uniqueness.
assert!(op_ids.insert(operation_id));
// Valid `chainHead_v1_call` call.
let alice_id = Sr25519Keyring::Alice.to_account_id();
let call_parameters = hex_string(&alice_id.encode());
let response: MethodResponse = api
.call(
"chainHead_v1_call",
[&sub_id, &block_hash, "AccountNonceApi_account_nonce", &call_parameters],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// Response propagated to `chainHead_follow`.
assert_matches!(
get_next_event::>(&mut block_sub).await,
FollowEvent::OperationCallDone(done) if done.operation_id == operation_id && done.output == "0x0000000000000000"
);
// Ensure uniqueness.
assert!(op_ids.insert(operation_id));
}
}
#[tokio::test]
async fn separate_operation_ids_for_subscriptions() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
// Create two separate subscriptions.
let mut sub_first = api.subscribe_unbounded("chainHead_v1_follow", [true]).await.unwrap();
let sub_id_first = sub_first.subscription_id();
let sub_id_first = serde_json::to_string(&sub_id_first).unwrap();
let mut sub_second = api.subscribe_unbounded("chainHead_v1_follow", [true]).await.unwrap();
let sub_id_second = sub_second.subscription_id();
let sub_id_second = serde_json::to_string(&sub_id_second).unwrap();
let block = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
let block_hash = format!("{:?}", block.header.hash());
// Ensure the imported block is propagated and pinned.
assert_matches!(
get_next_event::>(&mut sub_first).await,
FollowEvent::Initialized(_)
);
assert_matches!(
get_next_event::>(&mut sub_first).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub_first).await,
FollowEvent::BestBlockChanged(_)
);
assert_matches!(
get_next_event::>(&mut sub_second).await,
FollowEvent::Initialized(_)
);
assert_matches!(
get_next_event::>(&mut sub_second).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub_second).await,
FollowEvent::BestBlockChanged(_)
);
// Each `chainHead_follow` subscription receives a separate operation ID.
let response: MethodResponse =
api.call("chainHead_v1_body", [&sub_id_first, &block_hash]).await.unwrap();
let operation_id: String = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
assert_eq!(operation_id, "0");
let response: MethodResponse =
api.call("chainHead_v1_body", [&sub_id_second, &block_hash]).await.unwrap();
let operation_id_second: String = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// The second subscription does not increment the operation ID of the first one.
assert_eq!(operation_id_second, "0");
}
#[tokio::test]
async fn follow_generates_initial_blocks() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let finalized_hash = client.info().finalized_hash;
// Block tree:
// finalized -> block 1 -> block 2 -> block 3
// -> block 1 -> block 2_f
let block_1 = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_1_hash = block_1.header.hash();
client.import(BlockOrigin::Own, block_1.clone()).await.unwrap();
let block_2 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_1_hash)
.with_parent_block_number(1)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_2_hash = block_2.header.hash();
client.import(BlockOrigin::Own, block_2.clone()).await.unwrap();
let mut block_builder = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_1_hash)
.with_parent_block_number(1)
.build()
.unwrap();
// This push is required as otherwise block 3 has the same hash as block 2 and won't get
// imported
block_builder
.push_transfer(Transfer {
from: Sr25519Keyring::Alice.into(),
to: Sr25519Keyring::Ferdie.into(),
amount: 41,
nonce: 0,
})
.unwrap();
let block_2_f = block_builder.build().unwrap().block;
let block_2_f_hash = block_2_f.header.hash();
client.import(BlockOrigin::Own, block_2_f.clone()).await.unwrap();
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [false]).await.unwrap();
// Initialized must always be reported first.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Initialized(Initialized {
finalized_block_hashes: vec![format!("{:?}", finalized_hash)],
finalized_block_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
// Check block 1.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_1_hash),
parent_block_hash: format!("{:?}", finalized_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
// Check block 2.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_2_hash),
parent_block_hash: format!("{:?}", block_1_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
// Check block 3.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_2_f_hash),
parent_block_hash: format!("{:?}", block_1_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_2_hash),
});
assert_eq!(event, expected);
// Import block 3.
let block_3 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_2_hash)
.with_parent_block_number(2)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_3_hash = block_3.hash();
client.import(BlockOrigin::Own, block_3.clone()).await.unwrap();
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_3_hash),
parent_block_hash: format!("{:?}", block_2_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_3_hash),
});
assert_eq!(event, expected);
// Check the finalized event:
// - blocks 1, 2, 4 from canonical chain are finalized
// - block 3 from the fork is pruned
client.finalize_block(block_3_hash, None).unwrap();
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Finalized(Finalized {
finalized_block_hashes: vec![
format!("{:?}", block_1_hash),
format!("{:?}", block_2_hash),
format!("{:?}", block_3_hash),
],
pruned_block_hashes: vec![format!("{:?}", block_2_f_hash)],
});
assert_eq!(event, expected);
}
#[tokio::test]
async fn follow_exceeding_pinned_blocks() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: 2,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [false]).await.unwrap();
let block = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
// Ensure the imported block is propagated and pinned for this subscription.
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::Initialized(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
// Block tree:
// finalized_block -> block -> block2
// The first 2 blocks are pinned into the subscription, but the block2 will exceed the limit (2
// blocks).
let block2 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block.hash())
.with_parent_block_number(1)
.build()
.unwrap()
.build()
.unwrap()
.block;
client.import(BlockOrigin::Own, block2.clone()).await.unwrap();
assert_matches!(get_next_event::>(&mut sub).await, FollowEvent::Stop);
// Subscription will not produce any more event for further blocks.
let block3 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block2.hash())
.with_parent_block_number(2)
.build()
.unwrap()
.build()
.unwrap()
.block;
client.import(BlockOrigin::Own, block3.clone()).await.unwrap();
assert!(sub.next::>().await.is_none());
}
#[tokio::test]
async fn follow_with_unpin() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: 2,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [false]).await.unwrap();
let sub_id = sub.subscription_id();
let sub_id = serde_json::to_string(&sub_id).unwrap();
let block = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_hash = format!("{:?}", block.header.hash());
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
// Ensure the imported block is propagated and pinned for this subscription.
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::Initialized(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
// Unpin an invalid subscription ID must return Ok(()).
let invalid_hash = hex_string(&INVALID_HASH);
let _res: () = api
.call("chainHead_v1_unpin", rpc_params!["invalid_sub_id", &invalid_hash])
.await
.unwrap();
// Valid subscription with invalid block hash.
let invalid_hash = hex_string(&INVALID_HASH);
let err = api
.call::<_, serde_json::Value>("chainHead_v1_unpin", rpc_params![&sub_id, &invalid_hash])
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(ref err) if err.code() == super::error::rpc_spec_v2::INVALID_BLOCK_ERROR && err.message() == "Invalid block hash"
);
// To not exceed the number of pinned blocks, we need to unpin before the next import.
let _res: () = api.call("chainHead_v1_unpin", rpc_params![&sub_id, &block_hash]).await.unwrap();
// Block tree:
// finalized_block -> block -> block2
// ^ has been unpinned
let block2 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block.hash())
.with_parent_block_number(1)
.build()
.unwrap()
.build()
.unwrap()
.block;
client.import(BlockOrigin::Own, block2.clone()).await.unwrap();
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
let block3 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block2.hash())
.with_parent_block_number(2)
.build()
.unwrap()
.build()
.unwrap()
.block;
client.import(BlockOrigin::Own, block3.clone()).await.unwrap();
assert_matches!(get_next_event::>(&mut sub).await, FollowEvent::Stop);
assert!(sub.next::>().await.is_none());
}
#[tokio::test]
async fn unpin_duplicate_hashes() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: 3,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [false]).await.unwrap();
let sub_id = sub.subscription_id();
let sub_id = serde_json::to_string(&sub_id).unwrap();
let block = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_hash = format!("{:?}", block.header.hash());
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
// Ensure the imported block is propagated and pinned for this subscription.
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::Initialized(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
// Try to unpin duplicate hashes.
let err = api
.call::<_, serde_json::Value>(
"chainHead_v1_unpin",
rpc_params![&sub_id, vec![&block_hash, &block_hash]],
)
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(err) if err.code() == super::error::rpc_spec_v2::INVALID_DUPLICATE_HASHES && err.message() == "Received duplicate hashes for the `chainHead_unpin` method"
);
// Block tree:
// finalized_block -> block -> block2
let block2 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block.hash())
.with_parent_block_number(1)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_hash_2 = format!("{:?}", block2.header.hash());
client.import(BlockOrigin::Own, block2.clone()).await.unwrap();
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
// Try to unpin duplicate hashes.
let err = api
.call::<_, serde_json::Value>(
"chainHead_v1_unpin",
rpc_params![&sub_id, vec![&block_hash, &block_hash_2, &block_hash]],
)
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(err) if err.code() == super::error::rpc_spec_v2::INVALID_DUPLICATE_HASHES && err.message() == "Received duplicate hashes for the `chainHead_unpin` method"
);
// Can unpin blocks.
let _res: () = api
.call("chainHead_v1_unpin", rpc_params![&sub_id, vec![&block_hash, &block_hash_2]])
.await
.unwrap();
}
#[tokio::test]
async fn follow_with_multiple_unpin_hashes() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [false]).await.unwrap();
let sub_id = sub.subscription_id();
let sub_id = serde_json::to_string(&sub_id).unwrap();
// Import 3 blocks.
let block_1 = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_1_hash = block_1.header.hash();
client.import(BlockOrigin::Own, block_1.clone()).await.unwrap();
let block_2 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_1.hash())
.with_parent_block_number(1)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_2_hash = block_2.header.hash();
client.import(BlockOrigin::Own, block_2.clone()).await.unwrap();
let block_3 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_2.hash())
.with_parent_block_number(2)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_3_hash = block_3.header.hash();
client.import(BlockOrigin::Own, block_3.clone()).await.unwrap();
// Ensure the imported block is propagated and pinned for this subscription.
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::Initialized(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
// Unpin an invalid subscription ID must return Ok(()).
let invalid_hash = hex_string(&INVALID_HASH);
let _res: () = api
.call("chainHead_v1_unpin", rpc_params!["invalid_sub_id", &invalid_hash])
.await
.unwrap();
// Valid subscription with invalid block hash.
let err = api
.call::<_, serde_json::Value>("chainHead_v1_unpin", rpc_params![&sub_id, &invalid_hash])
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(ref err) if err.code() == super::error::rpc_spec_v2::INVALID_BLOCK_ERROR && err.message() == "Invalid block hash"
);
let _res: () = api
.call("chainHead_v1_unpin", rpc_params![&sub_id, &block_1_hash])
.await
.unwrap();
// One block hash is invalid. Block 1 is already unpinned.
let err = api
.call::<_, serde_json::Value>(
"chainHead_v1_unpin",
rpc_params![&sub_id, vec![&block_1_hash, &block_2_hash, &block_3_hash]],
)
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(ref err) if err.code() == super::error::rpc_spec_v2::INVALID_BLOCK_ERROR && err.message() == "Invalid block hash"
);
// Unpin multiple blocks.
let _res: () = api
.call("chainHead_v1_unpin", rpc_params![&sub_id, vec![&block_2_hash, &block_3_hash]])
.await
.unwrap();
// Check block 2 and 3 are unpinned.
let err = api
.call::<_, serde_json::Value>("chainHead_v1_unpin", rpc_params![&sub_id, &block_2_hash])
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(ref err) if err.code() == super::error::rpc_spec_v2::INVALID_BLOCK_ERROR && err.message() == "Invalid block hash"
);
let err = api
.call::<_, serde_json::Value>("chainHead_v1_unpin", rpc_params![&sub_id, &block_3_hash])
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(ref err) if err.code() == super::error::rpc_spec_v2::INVALID_BLOCK_ERROR && err.message() == "Invalid block hash"
);
}
#[tokio::test]
async fn follow_prune_best_block() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let finalized_hash = client.info().finalized_hash;
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [false]).await.unwrap();
// Initialized must always be reported first.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Initialized(Initialized {
finalized_block_hashes: vec![format!("{:?}", finalized_hash)],
finalized_block_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
// Block tree:
//
// finalized -> block 1 -> block 2
// ^^^ best block reported
//
// -> block 1 -> block 2_f -> block 4
// ^^^ finalized
//
// The block 4 is needed on the longest chain because we want the
// best block 2 to be reported as pruned. Pruning is happening at
// height (N - 1), where N is the finalized block number.
let block_1 = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_1_hash = block_1.hash();
client.import(BlockOrigin::Own, block_1.clone()).await.unwrap();
let block_2_f = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_1_hash)
.with_parent_block_number(1)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_2_f_hash = block_2_f.hash();
client.import(BlockOrigin::Own, block_2_f.clone()).await.unwrap();
let block_4 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_2_f_hash)
.with_parent_block_number(2)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_4_hash = block_4.hash();
client.import(BlockOrigin::Own, block_4.clone()).await.unwrap();
// Import block 2 as best on the fork.
let mut block_builder = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_1_hash)
.with_parent_block_number(1)
.build()
.unwrap();
// This push is required as otherwise block 3 has the same hash as block 2 and won't get
// imported
block_builder
.push_transfer(Transfer {
from: Sr25519Keyring::Alice.into(),
to: Sr25519Keyring::Ferdie.into(),
amount: 41,
nonce: 0,
})
.unwrap();
let block_2 = block_builder.build().unwrap().block;
let block_2_hash = block_2.header.hash();
client.import_as_best(BlockOrigin::Own, block_2.clone()).await.unwrap();
// Check block 1.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_1_hash),
parent_block_hash: format!("{:?}", finalized_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_1_hash),
});
assert_eq!(event, expected);
// Check block 3.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_2_f_hash),
parent_block_hash: format!("{:?}", block_1_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_2_f_hash),
});
assert_eq!(event, expected);
// Check block 4.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_4_hash),
parent_block_hash: format!("{:?}", block_2_f_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_4_hash),
});
assert_eq!(event, expected);
// Check block 2, that we imported as custom best.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_2_hash),
parent_block_hash: format!("{:?}", block_1_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_2_hash),
});
assert_eq!(event, expected);
// Finalize the block 4 from the fork.
client.finalize_block(block_4_hash, None).unwrap();
// Expect to report the best block changed before the finalized event.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_4_hash),
});
assert_eq!(event, expected);
// Block 2 must be reported as pruned, even if it was the previous best.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Finalized(Finalized {
finalized_block_hashes: vec![
format!("{:?}", block_1_hash),
format!("{:?}", block_2_f_hash),
format!("{:?}", block_4_hash),
],
pruned_block_hashes: vec![format!("{:?}", block_2_hash)],
});
assert_eq!(event, expected);
// Pruned hash can be unpinned.
let sub_id = sub.subscription_id();
let sub_id = serde_json::to_string(&sub_id).unwrap();
let hash = format!("{:?}", block_2_hash);
let _res: () = api.call("chainHead_v1_unpin", rpc_params![&sub_id, &hash]).await.unwrap();
}
#[tokio::test]
async fn follow_forks_pruned_block() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
// Block tree before the subscription:
//
// finalized -> block 1 -> block 2 -> block 3
// ^^^ finalized
// -> block 1 -> block 2_f -> block 3_f
//
let finalized_hash = client.info().finalized_hash;
let block_1 = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_1_hash = block_1.header.hash();
client.import(BlockOrigin::Own, block_1.clone()).await.unwrap();
let block_2 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_1.hash())
.with_parent_block_number(1)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_2_hash = block_2.header.hash();
client.import(BlockOrigin::Own, block_2.clone()).await.unwrap();
let block_3 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_2.hash())
.with_parent_block_number(2)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_3_hash = block_3.header.hash();
client.import(BlockOrigin::Own, block_3.clone()).await.unwrap();
// Block 4 with parent Block 1 is not the best imported.
let mut block_builder = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_1.hash())
.with_parent_block_number(1)
.build()
.unwrap();
// This push is required as otherwise block 4 has the same hash as block 2 and won't get
// imported
block_builder
.push_transfer(Transfer {
from: Sr25519Keyring::Alice.into(),
to: Sr25519Keyring::Ferdie.into(),
amount: 41,
nonce: 0,
})
.unwrap();
let block_2_f = block_builder.build().unwrap().block;
client.import(BlockOrigin::Own, block_2_f.clone()).await.unwrap();
let mut block_builder = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_2_f.hash())
.with_parent_block_number(2)
.build()
.unwrap();
block_builder
.push_transfer(Transfer {
from: Sr25519Keyring::Bob.into(),
to: Sr25519Keyring::Ferdie.into(),
amount: 41,
nonce: 0,
})
.unwrap();
let block_3_f = block_builder.build().unwrap().block;
client.import(BlockOrigin::Own, block_3_f.clone()).await.unwrap();
// Block 2_f and 3_f are not pruned, pruning happens at height (N - 1).
client.finalize_block(block_3_hash, None).unwrap();
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [false]).await.unwrap();
// Initialized must always be reported first.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Initialized(Initialized {
finalized_block_hashes: vec![
format!("{:?}", finalized_hash),
format!("{:?}", block_1_hash),
format!("{:?}", block_2_hash),
format!("{:?}", block_3_hash),
],
finalized_block_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
// Block tree:
//
// finalized -> block 1 -> block 2 -> block 3 -> block 4
// ^^^ finalized
// -> block 1 -> block 2_f -> block 3_f
//
// Mark block 4 as finalized to force block 2_f and 3_f to get pruned.
let block_4 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_3.hash())
.with_parent_block_number(3)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_4_hash = block_4.hash();
client.import(BlockOrigin::Own, block_4.clone()).await.unwrap();
client.finalize_block(block_4_hash, None).unwrap();
// Check block 6.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_4_hash),
parent_block_hash: format!("{:?}", block_3_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_4_hash),
});
assert_eq!(event, expected);
// Block 4 and 5 must not be reported as pruned.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Finalized(Finalized {
finalized_block_hashes: vec![format!("{:?}", block_4_hash)],
pruned_block_hashes: vec![],
});
assert_eq!(event, expected);
}
#[tokio::test]
async fn follow_report_multiple_pruned_block() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
// Block tree:
//
// finalized -> block 1 -> block 2 -> block 3
// ^^^ finalized after subscription
// -> block 1 -> block 2_f -> block 3_f
let finalized_hash = client.info().finalized_hash;
let block_1 = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_1_hash = block_1.hash();
client.import(BlockOrigin::Own, block_1.clone()).await.unwrap();
let block_2 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_1.hash())
.with_parent_block_number(1)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_2_hash = block_2.hash();
client.import(BlockOrigin::Own, block_2.clone()).await.unwrap();
let block_3 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_2.hash())
.with_parent_block_number(2)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_3_hash = block_3.hash();
client.import(BlockOrigin::Own, block_3.clone()).await.unwrap();
// Block 2_f with parent Block 1 is not the best imported.
let mut block_builder = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_1.hash())
.with_parent_block_number(1)
.build()
.unwrap();
// This push is required as otherwise block 2_f has the same hash as block 2 and won't get
// imported
block_builder
.push_transfer(Transfer {
from: Sr25519Keyring::Alice.into(),
to: Sr25519Keyring::Ferdie.into(),
amount: 41,
nonce: 0,
})
.unwrap();
let block_2_f = block_builder.build().unwrap().block;
let block_2_f_hash = block_2_f.hash();
client.import(BlockOrigin::Own, block_2_f.clone()).await.unwrap();
let mut block_builder = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_2_f.hash())
.with_parent_block_number(2)
.build()
.unwrap();
block_builder
.push_transfer(Transfer {
from: Sr25519Keyring::Bob.into(),
to: Sr25519Keyring::Ferdie.into(),
amount: 41,
nonce: 0,
})
.unwrap();
let block_3_f = block_builder.build().unwrap().block;
let block_3_f_hash = block_3_f.hash();
client.import(BlockOrigin::Own, block_3_f.clone()).await.unwrap();
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [false]).await.unwrap();
// Initialized must always be reported first.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Initialized(Initialized {
finalized_block_hashes: vec![format!("{:?}", finalized_hash)],
finalized_block_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_1_hash),
parent_block_hash: format!("{:?}", finalized_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_2_hash),
parent_block_hash: format!("{:?}", block_1_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_3_hash),
parent_block_hash: format!("{:?}", block_2_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
// The fork must also be reported.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_2_f_hash),
parent_block_hash: format!("{:?}", block_1_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_3_f_hash),
parent_block_hash: format!("{:?}", block_2_f_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
// The best block of the chain must also be reported.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_3_hash),
});
assert_eq!(event, expected);
// Block 4 and 5 are not pruned, pruning happens at height (N - 1).
client.finalize_block(block_3_hash, None).unwrap();
// Finalizing block 3 directly will also result in block 1 and 2 being finalized.
// It will also mark block 2 and block 3 from the fork as pruned.
let event: FollowEvent = get_next_event(&mut sub).await;
// Sort the hashes to prevent flaky tests.
let mut pruned_block_hashes =
vec![format!("{:?}", block_2_f_hash), format!("{:?}", block_3_f_hash)];
pruned_block_hashes.sort();
let expected = FollowEvent::Finalized(Finalized {
finalized_block_hashes: vec![
format!("{:?}", block_1_hash),
format!("{:?}", block_2_hash),
format!("{:?}", block_3_hash),
],
pruned_block_hashes,
});
assert_eq!(event, expected);
// Block tree:
//
// finalized -> block 1 -> block 2 -> block 3 -> block 4
// ^^^ finalized
// -> block 1 -> block 2_f -> block 3_f
//
let block_4 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_3.hash())
.with_parent_block_number(3)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_4_hash = block_4.hash();
client.import(BlockOrigin::Own, block_4.clone()).await.unwrap();
client.finalize_block(block_4_hash, None).unwrap();
// Check block 6.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_4_hash),
parent_block_hash: format!("{:?}", block_3_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_4_hash),
});
assert_eq!(event, expected);
// Blocks from the fork were pruned earlier.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Finalized(Finalized {
finalized_block_hashes: vec![format!("{:?}", block_4_hash)],
pruned_block_hashes: vec![],
});
assert_eq!(event, expected);
}
#[tokio::test]
async fn pin_block_references() {
// Manually construct an in-memory backend and client.
let backend = Arc::new(pezsc_client_api::in_mem::Backend::new());
let executor = bizinikiwi_test_runtime_client::WasmExecutor::default();
let client_config = pezsc_service::ClientConfig::default();
let genesis_block_builder = pezsc_service::GenesisBlockBuilder::new(
&bizinikiwi_test_runtime_client::GenesisParameters::default().genesis_storage(),
!client_config.no_genesis,
backend.clone(),
executor.clone(),
)
.unwrap();
let client = Arc::new(
new_with_backend::<_, _, Block, _, RuntimeApi>(
backend.clone(),
executor,
genesis_block_builder,
Box::new(TokioTestExecutor::default()),
None,
None,
client_config,
)
.unwrap(),
);
let api = ChainHead::new(
client.clone(),
backend.clone(),
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: 3,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
async fn wait_pinned_references(
backend: &Arc>,
hash: &Block::Hash,
target: i64,
) {
// Retry for at most 2 minutes.
let mut retries = 120;
while backend.pin_refs(hash).unwrap() != target {
if retries == 0 {
panic!("Expected target={} pinned references for hash={:?}", target, hash);
}
retries -= 1;
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
}
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [false]).await.unwrap();
let sub_id = sub.subscription_id();
let sub_id = serde_json::to_string(&sub_id).unwrap();
let block = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
let hash = block.header.hash();
let block_hash = format!("{:?}", hash);
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
// Ensure the imported block is propagated for this subscription.
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::Initialized(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
// We need to wait a bit for:
// 1. `NewBlock` and `BestBlockChanged` notifications to propagate to the chainHead
// subscription. (pin_refs == 2)
// 2. The chainHead to call `pin_blocks` only once for the `NewBlock`
// notification (pin_refs == 3)
// 3. Both notifications to go out of scope (pin_refs == 1 (total 3 - dropped 2)).
wait_pinned_references(&backend, &hash, 1).await;
// To not exceed the number of pinned blocks, we need to unpin before the next import.
let _res: () = api.call("chainHead_v1_unpin", rpc_params![&sub_id, &block_hash]).await.unwrap();
// Make sure unpin clears out the reference.
let refs = backend.pin_refs(&hash).unwrap();
assert_eq!(refs, 0);
// Add another 2 blocks and make sure we drop the subscription with the blocks pinned.
let mut hashes = Vec::new();
for _ in 0..2 {
let block = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().best_hash)
.with_parent_block_number(client.chain_info().best_number)
.build()
.unwrap()
.build()
.unwrap()
.block;
let hash = block.hash();
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
// Ensure the imported block is propagated for this subscription.
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
hashes.push(hash);
}
// Make sure the pin was propagated.
for hash in &hashes {
wait_pinned_references(&backend, hash, 1).await;
}
// Drop the subscription and expect the pinned blocks to be released.
drop(sub);
// The `chainHead` detects the subscription was terminated when it tries
// to send another block.
let block = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().best_hash)
.with_parent_block_number(client.chain_info().best_number)
.build()
.unwrap()
.build()
.unwrap()
.block;
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
for hash in &hashes {
wait_pinned_references(&backend, &hash, 0).await;
}
}
#[tokio::test]
async fn follow_finalized_before_new_block() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let client_mock = Arc::new(ChainHeadMockClient::new(client.clone()));
let api = ChainHead::new(
client_mock.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
// Make sure the block is imported for it to be pinned.
let block_1 = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_1_hash = block_1.header.hash();
client.import(BlockOrigin::Own, block_1.clone()).await.unwrap();
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [false]).await.unwrap();
// Trigger the `FinalizedNotification` for block 1 before the `BlockImportNotification`, and
// expect for the `chainHead` to generate `NewBlock`, `BestBlock` and `Finalized` events.
// Trigger the Finalized notification before the NewBlock one.
run_with_timeout(client_mock.trigger_finality_stream(block_1.header.clone(), vec![])).await;
// Initialized must always be reported first.
let finalized_hash = client.info().finalized_hash;
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Initialized(Initialized {
finalized_block_hashes: vec![format!("{:?}", finalized_hash)],
finalized_block_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
// Block 1 must be reported because we triggered the finalized notification.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_1_hash),
parent_block_hash: format!("{:?}", finalized_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_1_hash),
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Finalized(Finalized {
finalized_block_hashes: vec![format!("{:?}", block_1_hash)],
pruned_block_hashes: vec![],
});
assert_eq!(event, expected);
let block_2 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_1.hash())
.with_parent_block_number(1)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_2_hash = block_2.hash();
client.import(BlockOrigin::Own, block_2.clone()).await.unwrap();
// Triggering the `BlockImportNotification` notification for block 1 should have no effect
// on the notification because the events were handled by the `FinalizedNotification`.
// Also trigger the `BlockImportNotification` notification for block 2 to ensure
// `NewBlock and `BestBlock` events are generated.
// Trigger NewBlock notification for block 1 and block 2.
run_with_timeout(client_mock.trigger_import_stream(block_1.header)).await;
run_with_timeout(client_mock.trigger_import_stream(block_2.header)).await;
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_2_hash),
parent_block_hash: format!("{:?}", block_1_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_2_hash),
});
assert_eq!(event, expected);
}
#[tokio::test]
async fn ensure_operation_limits_works() {
let child_info = ChildInfo::new_default(CHILD_STORAGE_KEY);
let builder = TestClientBuilder::new().add_extra_child_storage(
&child_info,
KEY.to_vec(),
CHILD_VALUE.to_vec(),
);
let backend = builder.backend();
let client = Arc::new(builder.build());
// Configure the chainHead with maximum 1 ongoing operations.
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: 1,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [true]).await.unwrap();
let sub_id = sub.subscription_id();
let sub_id = serde_json::to_string(&sub_id).unwrap();
let block = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
// Ensure the imported block is propagated and pinned for this subscription.
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::Initialized(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
let block_hash = format!("{:?}", block.header.hash());
let key = hex_string(&KEY);
let items = vec![
StorageQuery {
key: key.clone(),
query_type: StorageQueryType::DescendantsHashes,
pagination_start_key: None,
},
StorageQuery {
key: key.clone(),
query_type: StorageQueryType::DescendantsHashes,
pagination_start_key: None,
},
StorageQuery {
key: key.clone(),
query_type: StorageQueryType::DescendantsValues,
pagination_start_key: None,
},
StorageQuery {
key: key.clone(),
query_type: StorageQueryType::DescendantsValues,
pagination_start_key: None,
},
];
let response: MethodResponse = api
.call("chainHead_v1_storage", rpc_params![&sub_id, &block_hash, items])
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => {
// Check discarded items.
assert_eq!(started.discarded_items, Some(0));
started.operation_id
},
MethodResponse::LimitReached => panic!("Expected started response"),
};
// No value associated with the provided key.
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::OperationStorageDone(done) if done.operation_id == operation_id
);
// The storage is finished and capacity must be released.
let alice_id = Sr25519Keyring::Alice.to_account_id();
// Hex encoded scale encoded bytes representing the call parameters.
let call_parameters = hex_string(&alice_id.encode());
let response: MethodResponse = api
.call(
"chainHead_v1_call",
[&sub_id, &block_hash, "AccountNonceApi_account_nonce", &call_parameters],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// Response propagated to `chainHead_follow`.
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::OperationCallDone(done) if done.operation_id == operation_id && done.output == "0x0000000000000000"
);
}
#[tokio::test]
async fn storage_is_backpressured() {
let child_info = ChildInfo::new_default(CHILD_STORAGE_KEY);
let builder = TestClientBuilder::new().add_extra_child_storage(
&child_info,
KEY.to_vec(),
CHILD_VALUE.to_vec(),
);
let backend = builder.backend();
let client = Arc::new(builder.build());
// Configure the chainHead with maximum 1 item before asking for pagination.
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [true]).await.unwrap();
let sub_id = sub.subscription_id();
let sub_id = serde_json::to_string(&sub_id).unwrap();
// Import a new block with storage changes.
let mut builder = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap();
builder.push_storage_change(b":m".to_vec(), Some(b"a".to_vec())).unwrap();
builder.push_storage_change(b":mo".to_vec(), Some(b"ab".to_vec())).unwrap();
builder.push_storage_change(b":moc".to_vec(), Some(b"abc".to_vec())).unwrap();
builder.push_storage_change(b":moD".to_vec(), Some(b"abcmoD".to_vec())).unwrap();
builder.push_storage_change(b":mock".to_vec(), Some(b"abcd".to_vec())).unwrap();
let block = builder.build().unwrap().block;
let block_hash = format!("{:?}", block.header.hash());
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
// Ensure the imported block is propagated and pinned for this subscription.
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::Initialized(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
let invalid_hash = hex_string(&INVALID_HASH);
// Invalid subscription ID must produce no results.
let _res: () = api
.call("chainHead_v1_continue", ["invalid_sub_id", &invalid_hash])
.await
.unwrap();
// Invalid operation ID must produce no results.
let _res: () = api.call("chainHead_v1_continue", [&sub_id, &invalid_hash]).await.unwrap();
// Valid call with storage at the key.
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&block_hash,
vec![StorageQuery {
key: hex_string(b":m"),
query_type: StorageQueryType::DescendantsValues,
pagination_start_key: None
}]
],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id &&
res.items.len() == 1 &&
res.items[0].key == hex_string(b":m") &&
res.items[0].result == StorageResultType::Value(hex_string(b"a"))
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id &&
res.items.len() == 1 &&
res.items[0].key == hex_string(b":mo") &&
res.items[0].result == StorageResultType::Value(hex_string(b"ab"))
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id &&
res.items.len() == 1 &&
res.items[0].key == hex_string(b":moD") &&
res.items[0].result == StorageResultType::Value(hex_string(b"abcmoD"))
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id &&
res.items.len() == 1 &&
res.items[0].key == hex_string(b":moc") &&
res.items[0].result == StorageResultType::Value(hex_string(b"abc"))
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id &&
res.items.len() == 1 &&
res.items[0].key == hex_string(b":mock") &&
res.items[0].result == StorageResultType::Value(hex_string(b"abcd"))
);
// Finished.
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::OperationStorageDone(done) if done.operation_id == operation_id
);
}
#[tokio::test]
async fn stop_storage_operation() {
let child_info = ChildInfo::new_default(CHILD_STORAGE_KEY);
let builder = TestClientBuilder::new().add_extra_child_storage(
&child_info,
KEY.to_vec(),
CHILD_VALUE.to_vec(),
);
let backend = builder.backend();
let client = Arc::new(builder.build());
// Configure the chainHead with maximum 1 item before asking for pagination.
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [true]).await.unwrap();
let sub_id = sub.subscription_id();
let sub_id = serde_json::to_string(&sub_id).unwrap();
// Import a new block with storage changes.
let mut builder = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap();
builder.push_storage_change(b":m".to_vec(), Some(b"a".to_vec())).unwrap();
builder.push_storage_change(b":mo".to_vec(), Some(b"ab".to_vec())).unwrap();
let block = builder.build().unwrap().block;
let block_hash = format!("{:?}", block.header.hash());
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
// Ensure the imported block is propagated and pinned for this subscription.
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::Initialized(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
let invalid_hash = hex_string(&INVALID_HASH);
// Invalid subscription ID must produce no results.
let _res: () = api
.call("chainHead_v1_stopOperation", ["invalid_sub_id", &invalid_hash])
.await
.unwrap();
// Invalid operation ID must produce no results.
let _res: () = api.call("chainHead_v1_stopOperation", [&sub_id, &invalid_hash]).await.unwrap();
// Valid call with storage at the key.
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&block_hash,
vec![StorageQuery {
key: hex_string(b":m"),
query_type: StorageQueryType::DescendantsValues,
pagination_start_key: None
}]
],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id &&
res.items.len() == 1 &&
res.items[0].key == hex_string(b":m") &&
res.items[0].result == StorageResultType::Value(hex_string(b"a"))
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id &&
res.items.len() == 1 &&
res.items[0].key == hex_string(b":mo") &&
res.items[0].result == StorageResultType::Value(hex_string(b"ab"))
);
// Stop the operation.
let _res: () = api.call("chainHead_v1_stopOperation", [&sub_id, &operation_id]).await.unwrap();
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::OperationStorageDone(done) if done.operation_id == operation_id
);
does_not_produce_event::>(
&mut sub,
std::time::Duration::from_secs(DOES_NOT_PRODUCE_EVENTS_SECONDS),
)
.await;
}
#[tokio::test]
async fn storage_closest_merkle_value() {
let (client, api, mut sub, sub_id, block) = setup_api().await;
/// The core of this test.
///
/// Checks keys that are exact match, keys with descendant and keys that should not return
/// values.
///
/// Returns (key, merkle value) pairs.
async fn expect_merkle_request(
api: &RpcModule>>,
mut sub: &mut RpcSubscription,
sub_id: String,
block_hash: String,
) -> HashMap {
// Valid call with storage at the keys.
let response: MethodResponse = api
.call(
"chainHead_v1_storage",
rpc_params![
&sub_id,
&block_hash,
vec![
StorageQuery {
key: hex_string(b":AAAA"),
query_type: StorageQueryType::ClosestDescendantMerkleValue,
pagination_start_key: None
},
StorageQuery {
key: hex_string(b":AAAB"),
query_type: StorageQueryType::ClosestDescendantMerkleValue,
pagination_start_key: None
},
// Key with descendant.
StorageQuery {
key: hex_string(b":A"),
query_type: StorageQueryType::ClosestDescendantMerkleValue,
pagination_start_key: None
},
StorageQuery {
key: hex_string(b":AA"),
query_type: StorageQueryType::ClosestDescendantMerkleValue,
pagination_start_key: None
},
// Keys below this comment do not produce a result.
// Key that exceed the keyspace of the trie.
StorageQuery {
key: hex_string(b":AAAAX"),
query_type: StorageQueryType::ClosestDescendantMerkleValue,
pagination_start_key: None
},
StorageQuery {
key: hex_string(b":AAABX"),
query_type: StorageQueryType::ClosestDescendantMerkleValue,
pagination_start_key: None
},
// Key that are not part of the trie.
StorageQuery {
key: hex_string(b":AAX"),
query_type: StorageQueryType::ClosestDescendantMerkleValue,
pagination_start_key: None
},
StorageQuery {
key: hex_string(b":AAAX"),
query_type: StorageQueryType::ClosestDescendantMerkleValue,
pagination_start_key: None
},
]
],
)
.await
.unwrap();
let operation_id = match response {
MethodResponse::Started(started) => {
assert_eq!(started.discarded_items, Some(0));
started.operation_id
},
MethodResponse::LimitReached => panic!("Expected started response"),
};
let mut merkle_values = HashMap::new();
loop {
match get_next_event::>(&mut sub).await {
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id =>
for res in res.items {
let value = match res.result {
StorageResultType::ClosestDescendantMerkleValue(value) => value,
_ => panic!("Unexpected StorageResultType"),
};
merkle_values.insert(res.key, value);
},
FollowEvent::OperationStorageDone(done) if done.operation_id == operation_id =>
break,
_ => panic!("Unexpected event"),
}
}
// Response for AAAA, AAAB, A and AA.
assert_eq!(merkle_values.len(), 4);
// While checking for expected merkle values to align,
// the following will check that the returned keys are
// expected.
// Values for AAAA and AAAB are different.
assert_ne!(
merkle_values.get(&hex_string(b":AAAA")).unwrap(),
merkle_values.get(&hex_string(b":AAAB")).unwrap()
);
// Values for A and AA should be on the same branch node.
assert_eq!(
merkle_values.get(&hex_string(b":A")).unwrap(),
merkle_values.get(&hex_string(b":AA")).unwrap()
);
// The branch node value must be different than the leaf of either
// AAAA and AAAB.
assert_ne!(
merkle_values.get(&hex_string(b":A")).unwrap(),
merkle_values.get(&hex_string(b":AAAA")).unwrap()
);
assert_ne!(
merkle_values.get(&hex_string(b":A")).unwrap(),
merkle_values.get(&hex_string(b":AAAB")).unwrap()
);
merkle_values
}
// Import a new block with storage changes.
let mut builder = BlockBuilderBuilder::new(&*client)
.on_parent_block(block.hash())
.with_parent_block_number(1)
.build()
.unwrap();
builder.push_storage_change(b":AAAA".to_vec(), Some(vec![1; 64])).unwrap();
builder.push_storage_change(b":AAAB".to_vec(), Some(vec![2; 64])).unwrap();
let block = builder.build().unwrap().block;
let block_hash = format!("{:?}", block.header.hash());
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
// Ensure the imported block is propagated and pinned for this subscription.
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
let merkle_values_lhs = expect_merkle_request(&api, &mut sub, sub_id.clone(), block_hash).await;
// Import a new block with and change AAAB value.
let mut builder = BlockBuilderBuilder::new(&*client)
.on_parent_block(block.hash())
.with_parent_block_number(2)
.build()
.unwrap();
builder.push_storage_change(b":AAAA".to_vec(), Some(vec![1; 64])).unwrap();
builder.push_storage_change(b":AAAB".to_vec(), Some(vec![3; 64])).unwrap();
let block = builder.build().unwrap().block;
let block_hash = format!("{:?}", block.header.hash());
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
// Ensure the imported block is propagated and pinned for this subscription.
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
let merkle_values_rhs = expect_merkle_request(&api, &mut sub, sub_id.clone(), block_hash).await;
// Change propagated to the root.
assert_ne!(
merkle_values_lhs.get(&hex_string(b":A")).unwrap(),
merkle_values_rhs.get(&hex_string(b":A")).unwrap()
);
assert_ne!(
merkle_values_lhs.get(&hex_string(b":AAAB")).unwrap(),
merkle_values_rhs.get(&hex_string(b":AAAB")).unwrap()
);
// However the AAAA branch leaf remains unchanged.
assert_eq!(
merkle_values_lhs.get(&hex_string(b":AAAA")).unwrap(),
merkle_values_rhs.get(&hex_string(b":AAAA")).unwrap()
);
}
#[tokio::test]
async fn chain_head_stop_all_subscriptions() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
// Configure the chainHead to stop all subscriptions on lagging distance of 5 blocks.
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: 5,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [true]).await.unwrap();
// Ensure the imported block is propagated and pinned for this subscription.
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::Initialized(_)
);
// Import 6 blocks in total to trigger the suspension distance.
let mut parent_hash = client.chain_info().genesis_hash;
for i in 0..6 {
let block = BlockBuilderBuilder::new(&*client)
.on_parent_block(parent_hash)
.with_parent_block_number(i)
.build()
.unwrap()
.build()
.unwrap()
.block;
let hash = block.hash();
parent_hash = hash;
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::NewBlock(_)
);
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::BestBlockChanged(_)
);
}
let mut second_sub = api.subscribe_unbounded("chainHead_v1_follow", [true]).await.unwrap();
// Lagging detected, the stop event is delivered immediately.
assert_matches!(
get_next_event::>(&mut second_sub).await,
FollowEvent::Stop
);
// Ensure that all subscriptions are stopped.
assert_matches!(get_next_event::>(&mut sub).await, FollowEvent::Stop);
// Other subscriptions cannot be started until the suspension period is over.
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [true]).await.unwrap();
// Should receive the stop event immediately.
assert_matches!(get_next_event::>(&mut sub).await, FollowEvent::Stop);
// For the next subscription, lagging distance must be smaller.
client.finalize_block(parent_hash, None).unwrap();
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [true]).await.unwrap();
assert_matches!(
get_next_event::>(&mut sub).await,
FollowEvent::Initialized(_)
);
}
#[tokio::test]
async fn chain_head_single_connection_context() {
let server_addr = run_server().await;
let server_url = format!("ws://{}", server_addr);
let client = jsonrpsee::ws_client::WsClientBuilder::default()
.build(&server_url)
.await
.unwrap();
// Calls cannot be made from a different connection context.
let second_client = jsonrpsee::ws_client::WsClientBuilder::default()
.build(&server_url)
.await
.unwrap();
let mut sub: RpcClientSubscription> =
ChainHeadApiClient::::chain_head_unstable_follow(&client, true)
.await
.unwrap();
let event = tokio::time::timeout(std::time::Duration::from_secs(60), sub.next())
.await
.unwrap()
.unwrap()
.unwrap();
let finalized_hash = match event {
FollowEvent::Initialized(init) => init.finalized_block_hashes.into_iter().last().unwrap(),
_ => panic!("Expected FollowEvent::Initialized"),
};
let first_sub_id = match sub.kind() {
jsonrpsee::core::client::SubscriptionKind::Subscription(id) => match id {
jsonrpsee::types::SubscriptionId::Num(num) => num.to_string(),
jsonrpsee::types::SubscriptionId::Str(s) => s.to_string(),
},
_ => panic!("Unexpected subscription ID"),
};
// Trying to unpin from a different connection will have no effect.
let _response = ChainHeadApiClient::::chain_head_unstable_unpin(
&second_client,
first_sub_id.clone(),
crate::chain_head::api::ListOrValue::Value(finalized_hash.clone()),
)
.await
.unwrap();
// Body can still be fetched from the first subscription.
let response: MethodResponse = ChainHeadApiClient::::chain_head_unstable_body(
&client,
first_sub_id.clone(),
finalized_hash.clone(),
)
.await
.unwrap();
assert_matches!(response, MethodResponse::Started(_started));
// Cannot make a call from a different connection context.
let response: MethodResponse = ChainHeadApiClient::::chain_head_unstable_body(
&second_client,
first_sub_id.clone(),
finalized_hash.clone(),
)
.await
.unwrap();
assert_matches!(response, MethodResponse::LimitReached);
let response: Option = ChainHeadApiClient::::chain_head_unstable_header(
&client,
first_sub_id.clone(),
finalized_hash.clone(),
)
.await
.unwrap();
assert!(response.is_some());
// Cannot make a call from a different connection context.
let response: Option = ChainHeadApiClient::::chain_head_unstable_header(
&second_client,
first_sub_id.clone(),
finalized_hash.clone(),
)
.await
.unwrap();
assert!(response.is_none());
let key = hex_string(&KEY);
let response: MethodResponse = ChainHeadApiClient::::chain_head_unstable_storage(
&client,
first_sub_id.clone(),
finalized_hash.clone(),
vec![StorageQuery {
key: key.clone(),
query_type: StorageQueryType::Hash,
pagination_start_key: None,
}],
None,
)
.await
.unwrap();
assert_matches!(response, MethodResponse::Started(_started));
// Cannot make a call from a different connection context.
let response: MethodResponse = ChainHeadApiClient::::chain_head_unstable_storage(
&second_client,
first_sub_id.clone(),
finalized_hash.clone(),
vec![StorageQuery {
key: key.clone(),
query_type: StorageQueryType::Hash,
pagination_start_key: None,
}],
None,
)
.await
.unwrap();
assert_matches!(response, MethodResponse::LimitReached);
let alice_id = Sr25519Keyring::Alice.to_account_id();
// Hex encoded scale encoded bytes representing the call parameters.
let call_parameters = hex_string(&alice_id.encode());
let response: MethodResponse = ChainHeadApiClient::::chain_head_unstable_call(
&client,
first_sub_id.clone(),
finalized_hash.clone(),
"AccountNonceApi_account_nonce".into(),
call_parameters.clone(),
)
.await
.unwrap();
assert_matches!(response, MethodResponse::Started(_started));
// Cannot make a call from a different connection context.
let response: MethodResponse = ChainHeadApiClient::::chain_head_unstable_call(
&second_client,
first_sub_id.clone(),
finalized_hash.clone(),
"AccountNonceApi_account_nonce".into(),
call_parameters.clone(),
)
.await
.unwrap();
assert_matches!(response, MethodResponse::LimitReached);
}
#[tokio::test]
async fn chain_head_limit_reached() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
// Maximum of 1 chainHead_follow subscription.
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: 1,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [true]).await.unwrap();
// Initialized must always be reported first.
let _event: FollowEvent = get_next_event(&mut sub).await;
let error = api.subscribe_unbounded("chainHead_v1_follow", [true]).await.unwrap_err();
assert!(error
.to_string()
.contains("Maximum number of chainHead_follow has been reached"));
// After dropping the subscription, other subscriptions are allowed to be created.
drop(sub);
// Ensure the `chainHead_unfollow` is propagated to the server.
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [true]).await.unwrap();
// Initialized must always be reported first.
let _event: FollowEvent = get_next_event(&mut sub).await;
}
#[tokio::test]
async fn follow_unique_pruned_blocks() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
max_lagging_distance: MAX_LAGGING_DISTANCE,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let finalized_hash = client.info().finalized_hash;
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [false]).await.unwrap();
// Initialized must always be reported first.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Initialized(Initialized {
finalized_block_hashes: vec![format!("{:?}", finalized_hash)],
finalized_block_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
// Block tree:
//
// finalized -> block 1 -> block 2 -> block 3
//
// -> block 2 -> block 4 -> block 5
//
// -> block 1 -> block 2_f -> block 6
// ^^^ finalized
// -> block 7
// ^^^ finalized
// -> block 8
// ^^^ finalized
// The chainHead will see block 5 as the best block. However, the
// client will finalize the block 6, which is on another fork.
//
// When the block 6 is finalized all blocks from the stale forks (2, 3, 4, 5) are pruned.
//
// Initial setup steps:
let block_1_hash =
import_block(client.clone(), client.chain_info().genesis_hash, 0).await.hash();
let block_2_f_hash = import_block(client.clone(), block_1_hash, 1).await.hash();
let block_6_hash = import_block(client.clone(), block_2_f_hash, 2).await.hash();
// Import block 2 as best on the fork.
let mut tx_alice_ferdie = Transfer {
from: Sr25519Keyring::Alice.into(),
to: Sr25519Keyring::Ferdie.into(),
amount: 41,
nonce: 0,
};
let block_2_hash =
import_best_block_with_tx(client.clone(), block_1_hash, 1, tx_alice_ferdie.clone())
.await
.hash();
let block_3_hash = import_block(client.clone(), block_2_hash, 2).await.hash();
// Fork block 4.
tx_alice_ferdie.nonce = 1;
let block_4_hash = import_best_block_with_tx(client.clone(), block_2_hash, 2, tx_alice_ferdie)
.await
.hash();
let block_5_hash = import_block(client.clone(), block_4_hash, 3).await.hash();
// Check expected events generated by the setup.
{
// Check block 1 -> block 2f -> block 6.
check_new_and_best_block_events!(&mut sub, block_1_hash, finalized_hash);
check_new_and_best_block_events!(&mut sub, block_2_f_hash, block_1_hash);
check_new_and_best_block_events!(&mut sub, block_6_hash, block_2_f_hash);
// Check (block 1 ->) block 2 -> block 3.
check_new_and_best_block_events!(&mut sub, block_2_hash, block_1_hash);
check_new_and_best_block_events!(&mut sub, block_3_hash, block_2_hash);
// Check (block 1 -> block 2 ->) block 4 -> block 5.
check_new_and_best_block_events!(&mut sub, block_4_hash, block_2_hash);
check_new_and_best_block_events!(&mut sub, block_5_hash, block_4_hash);
}
// Finalize the block 6 from the fork.
client.finalize_block(block_6_hash, None).unwrap();
// Expect to report the best block changed before the finalized event.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_6_hash),
});
assert_eq!(event, expected);
// All blocks from stale forks are pruned when we finalize block 6.
let mut event: FollowEvent = get_next_event(&mut sub).await;
// Sort pruned block hashes to counter flaky test caused by event generation (get_pruned_hashes)
if let FollowEvent::Finalized(Finalized { pruned_block_hashes, .. }) = &mut event {
pruned_block_hashes.sort();
}
let expected_pruned_block_hashes = {
let mut hashes = vec![
format!("{:?}", block_2_hash),
format!("{:?}", block_3_hash),
format!("{:?}", block_4_hash),
format!("{:?}", block_5_hash),
];
hashes.sort();
hashes
};
let expected = FollowEvent::Finalized(Finalized {
finalized_block_hashes: vec![
format!("{:?}", block_1_hash),
format!("{:?}", block_2_f_hash),
format!("{:?}", block_6_hash),
],
pruned_block_hashes: expected_pruned_block_hashes,
});
assert_eq!(event, expected);
// Pruned hash can be unpinned.
let sub_id = sub.subscription_id();
let sub_id = serde_json::to_string(&sub_id).unwrap();
let hash = format!("{:?}", block_2_hash);
let _res: () = api.call("chainHead_v1_unpin", rpc_params![&sub_id, &hash]).await.unwrap();
// Import block 7 and check it.
let block_7_hash = import_block(client.clone(), block_6_hash, 3).await.hash();
check_new_and_best_block_events!(&mut sub, block_7_hash, block_6_hash);
// Finalize the block 7.
client.finalize_block(block_7_hash, None).unwrap();
let event: FollowEvent = get_next_event(&mut sub).await;
// All necessary blocks were pruned on block 6 finalization.
let expected = FollowEvent::Finalized(Finalized {
finalized_block_hashes: vec![format!("{:?}", block_7_hash)],
pruned_block_hashes: vec![],
});
assert_eq!(event, expected);
// Check block 8.
let block_8_hash = import_block(client.clone(), block_7_hash, 4).await.hash();
check_new_and_best_block_events!(&mut sub, block_8_hash, block_7_hash);
// Finalize the block 8.
client.finalize_block(block_8_hash, None).unwrap();
// All necessary blocks were pruned on block 6 finalization.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Finalized(Finalized {
finalized_block_hashes: vec![format!("{:?}", block_8_hash)],
pruned_block_hashes: vec![],
});
assert_eq!(event, expected);
}
#[tokio::test]
async fn follow_report_best_block_of_a_known_block() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let client_mock = Arc::new(ChainHeadMockClient::new(client.clone()));
let api = ChainHead::new(
client_mock.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let finalized_hash = client.info().finalized_hash;
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [false]).await.unwrap();
// Initialized must always be reported first.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Initialized(Initialized {
finalized_block_hashes: vec![format!("{:?}", finalized_hash)],
finalized_block_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
// Block tree:
//
// finalized -> block 1 -> block 2
// ^^^ best block reported
//
// -> block 1 -> block 2_f -> block 3 (best)
// ^^^ finalized
let block_1 = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_1_hash = block_1.hash();
client.import(BlockOrigin::Own, block_1.clone()).await.unwrap();
let block_2_f = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_1_hash)
.with_parent_block_number(1)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_2_f_hash = block_2_f.hash();
client.import(BlockOrigin::Own, block_2_f.clone()).await.unwrap();
// Import block 2 as best on the fork.
let mut block_builder = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_1_hash)
.with_parent_block_number(1)
.build()
.unwrap();
// This push is required as otherwise block 3 has the same hash as block 2 and won't get
// imported
block_builder
.push_transfer(Transfer {
from: Sr25519Keyring::Alice.into(),
to: Sr25519Keyring::Ferdie.into(),
amount: 41,
nonce: 0,
})
.unwrap();
let block_2 = block_builder.build().unwrap().block;
let block_2_hash = block_2.header.hash();
client.import_as_best(BlockOrigin::Own, block_2.clone()).await.unwrap();
run_with_timeout(client_mock.trigger_import_stream(block_1.header.clone())).await;
run_with_timeout(client_mock.trigger_import_stream(block_2_f.header.clone())).await;
run_with_timeout(client_mock.trigger_import_stream(block_2.header.clone())).await;
// Check block 1.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_1_hash),
parent_block_hash: format!("{:?}", finalized_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_1_hash),
});
assert_eq!(event, expected);
// Check block 2.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_2_f_hash),
parent_block_hash: format!("{:?}", block_1_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_2_f_hash),
});
assert_eq!(event, expected);
// Check block 2, that we imported as custom best.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_2_hash),
parent_block_hash: format!("{:?}", block_1_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_2_hash),
});
assert_eq!(event, expected);
// Craft block 3 and import it later to simulate a race condition.
let block_3 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_2_f_hash)
.with_parent_block_number(2)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_3_hash = block_3.hash();
// Set best block info to block 3, that is not announced yet.
//
// This simulates the following edge-case:
// - The client imports a new block as best block.
// - The finality stream is triggered before the block is announced.
//
// This generated in the past a `BestBlock` event for the block that was not announced
// by `NewBlock` events.
//
// This happened because the chainHead was using the `client.info()` without verifying
// if the block was announced or not. This was fixed by using the latest finalized
// block instead as fallback. For more info see: https://github.com/pezkuwichain/pezkuwi-sdk/issues/142.
client_mock.set_best_block(block_3_hash, 3);
// Finalize the block 2 from the fork.
client.finalize_block(block_2_f_hash, None).unwrap();
run_with_timeout(
client_mock.trigger_finality_stream(block_2_f.header.clone(), vec![block_2_hash]),
)
.await;
// Block 2f is now the best block, not the block 3 that is not announced yet.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_2_f_hash),
});
assert_eq!(event, expected);
// Block 2 must be reported as pruned, even if it was the previous best.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Finalized(Finalized {
finalized_block_hashes: vec![
// Note: the client mock is only reporting one block at a time.
// format!("{:?}", block_1_hash),
format!("{:?}", block_2_f_hash),
],
pruned_block_hashes: vec![format!("{:?}", block_2_hash)],
});
assert_eq!(event, expected);
// Block 3 is now imported as best.
client.import_as_best(BlockOrigin::Own, block_3.clone()).await.unwrap();
run_with_timeout(client_mock.trigger_import_stream(block_3.header.clone())).await;
// Check block 3.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::NewBlock(NewBlock {
block_hash: format!("{:?}", block_3_hash),
parent_block_hash: format!("{:?}", block_2_f_hash),
new_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: format!("{:?}", block_3_hash),
});
assert_eq!(event, expected);
// Pruned hash can be unpinned.
let sub_id = sub.subscription_id();
let sub_id = serde_json::to_string(&sub_id).unwrap();
let hash = format!("{:?}", block_2_hash);
let _res: () = api.call("chainHead_v1_unpin", rpc_params![&sub_id, &hash]).await.unwrap();
// Finalize the block 3.
client.finalize_block(block_3_hash, None).unwrap();
run_with_timeout(client_mock.trigger_finality_stream(block_3.header.clone(), vec![])).await;
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Finalized(Finalized {
finalized_block_hashes: vec![format!("{:?}", block_3_hash)],
pruned_block_hashes: vec![],
});
assert_eq!(event, expected);
}
#[tokio::test]
async fn follow_event_with_unknown_parent() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let client_mock = Arc::new(ChainHeadMockClient::new(client.clone()));
let api = ChainHead::new(
client_mock.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
max_lagging_distance: MAX_LAGGING_DISTANCE,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
},
)
.into_rpc();
let finalized_hash = client.info().finalized_hash;
let mut sub = api.subscribe_unbounded("chainHead_v1_follow", [false]).await.unwrap();
// Initialized must always be reported first.
let event: FollowEvent = get_next_event(&mut sub).await;
let expected = FollowEvent::Initialized(Initialized {
finalized_block_hashes: vec![format!("{:?}", finalized_hash)],
finalized_block_runtime: None,
with_runtime: false,
});
assert_eq!(event, expected);
// Block tree:
//
// finalized -> (gap: block 1) -> block 2
//
// Block 1 is not announced yet. ChainHead should report the stop
// event when encountering an unknown parent of block 2.
// Note: `client` is used just for constructing the blocks.
// The blocks are imported to chainHead using the `client_mock`.
let block_1 = BlockBuilderBuilder::new(&*client)
.on_parent_block(client.chain_info().genesis_hash)
.with_parent_block_number(0)
.build()
.unwrap()
.build()
.unwrap()
.block;
let block_1_hash = block_1.hash();
client.import(BlockOrigin::Own, block_1.clone()).await.unwrap();
let block_2 = BlockBuilderBuilder::new(&*client)
.on_parent_block(block_1_hash)
.with_parent_block_number(1)
.build()
.unwrap()
.build()
.unwrap()
.block;
client.import(BlockOrigin::Own, block_2.clone()).await.unwrap();
run_with_timeout(client_mock.trigger_import_stream(block_2.header)).await;
// When importing the block 2, chainHead detects a gap in our blocks and stops.
assert_matches!(get_next_event::>(&mut sub).await, FollowEvent::Stop);
}
#[tokio::test]
async fn events_are_backpressured() {
let builder = TestClientBuilder::new();
let backend = builder.backend();
let client = Arc::new(builder.build());
let api = ChainHead::new(
client.clone(),
backend,
Arc::new(TokioTestExecutor::default()),
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: Duration::from_secs(MAX_PINNED_SECS),
subscription_max_ongoing_operations: MAX_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: 10,
},
)
.into_rpc();
let mut parent_hash = client.chain_info().genesis_hash;
let mut header = VecDeque::new();
let mut sub = api.subscribe("chainHead_v1_follow", [false], 1).await.unwrap();
// insert more events than the user can consume
for i in 0..=5 {
let block = BlockBuilderBuilder::new(&*client)
.on_parent_block(parent_hash)
.with_parent_block_number(i)
.build()
.unwrap()
.build()
.unwrap()
.block;
header.push_front(block.header().clone());
parent_hash = block.hash();
client.import(BlockOrigin::Own, block.clone()).await.unwrap();
}
let mut events = Vec::new();
while let Some(event) = sub.next::>().await {
events.push(event);
}
assert_eq!(events.len(), 2);
assert_matches!(events.pop().unwrap().map(|x| x.0), Ok(FollowEvent::Stop));
}