feat: Rebrand Polkadot/Substrate references to PezkuwiChain

This commit systematically rebrands various references from Parity Technologies'
Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk.

Key changes include:
- Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks.
- Modified internal documentation and code comments to reflect PezkuwiChain naming and structure.
- Replaced direct references to  with  or specific paths within the  for XCM, Pezkuwi, and other modules.
- Cleaned up deprecated  issue and PR references in various  and  files, particularly in  and  modules.
- Adjusted image and logo URLs in documentation to point to PezkuwiChain assets.
- Removed or rephrased comments related to external Polkadot/Substrate PRs and issues.

This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
2025-12-14 00:04:10 +03:00
parent 286de54384
commit 1c0e57d984
9084 changed files with 997839 additions and 997557 deletions
@@ -0,0 +1,133 @@
// 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 <https://www.gnu.org/licenses/>.
//! API trait of the archive methods.
use crate::{
archive::{
error::{Error, Infallible},
types::MethodResult,
},
common::events::{
ArchiveStorageDiffEvent, ArchiveStorageDiffItem, ArchiveStorageEvent, StorageQuery,
},
};
use jsonrpsee::proc_macros::rpc;
#[rpc(client, server)]
pub trait ArchiveApi<Hash> {
/// Retrieves the body (list of transactions) of a given block hash.
///
/// Returns an array of strings containing the hexadecimal-encoded SCALE-codec-encoded
/// transactions in that block. If no block with that hash is found, null.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "archive_v1_body")]
fn archive_v1_body(&self, hash: Hash) -> Result<Option<Vec<String>>, Infallible>;
/// Get the chain's genesis hash.
///
/// Returns a string containing the hexadecimal-encoded hash of the genesis block of the chain.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "archive_v1_genesisHash")]
fn archive_v1_genesis_hash(&self) -> Result<String, Infallible>;
/// Get the block's header.
///
/// Returns a string containing the hexadecimal-encoded SCALE-codec encoding header of the
/// block.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "archive_v1_header")]
fn archive_v1_header(&self, hash: Hash) -> Result<Option<String>, Infallible>;
/// Get the height of the current finalized block.
///
/// Returns an integer height of the current finalized block of the chain.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "archive_v1_finalizedHeight")]
fn archive_v1_finalized_height(&self) -> Result<u64, Infallible>;
/// Get the hashes of blocks from the given height.
///
/// Returns an array (possibly empty) of strings containing an hexadecimal-encoded hash of a
/// block header.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "archive_v1_hashByHeight")]
fn archive_v1_hash_by_height(&self, height: u64) -> Result<Vec<String>, Error>;
/// Call into the Runtime API at a specified block's state.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "archive_v1_call")]
fn archive_v1_call(
&self,
hash: Hash,
function: String,
call_parameters: String,
) -> Result<MethodResult, Error>;
/// Returns storage entries at a specific block's state.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[subscription(
name = "archive_v1_storage" => "archive_v1_storageEvent",
unsubscribe = "archive_v1_stopStorage",
item = ArchiveStorageEvent,
)]
fn archive_v1_storage(
&self,
hash: Hash,
items: Vec<StorageQuery<String>>,
child_trie: Option<String>,
);
/// Returns the storage difference between two blocks.
///
/// # Unstable
///
/// This method is unstable and can change in minor or patch releases.
#[subscription(
name = "archive_v1_storageDiff" => "archive_v1_storageDiffEvent",
unsubscribe = "archive_v1_storageDiff_stopStorageDiff",
item = ArchiveStorageDiffEvent,
)]
fn archive_v1_storage_diff(
&self,
hash: Hash,
items: Vec<ArchiveStorageDiffItem<String>>,
previous_hash: Option<Hash>,
);
}
@@ -0,0 +1,388 @@
// 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 <https://www.gnu.org/licenses/>.
//! API implementation for `archive`.
use crate::{
archive::{
archive_storage::ArchiveStorageDiff,
error::{Error as ArchiveError, Infallible},
types::MethodResult,
ArchiveApiServer,
},
common::{
events::{
ArchiveStorageDiffEvent, ArchiveStorageDiffItem, ArchiveStorageEvent, StorageQuery,
},
storage::{QueryResult, StorageSubscriptionClient},
},
hex_string, SubscriptionTaskExecutor,
};
use codec::Encode;
use futures::FutureExt;
use jsonrpsee::{core::async_trait, PendingSubscriptionSink};
use pezsc_client_api::{
Backend, BlockBackend, BlockchainEvents, CallExecutor, ChildInfo, ExecutorProvider, StorageKey,
StorageProvider,
};
use pezsc_rpc::utils::Subscription;
use pezsp_api::{CallApiAt, CallContext};
use pezsp_blockchain::{
Backend as BlockChainBackend, Error as BlockChainError, HeaderBackend, HeaderMetadata,
};
use pezsp_core::{Bytes, U256};
use pezsp_runtime::{
traits::{Block as BlockT, Header as HeaderT, NumberFor},
SaturatedConversion,
};
use std::{collections::HashSet, marker::PhantomData, sync::Arc};
use tokio::sync::mpsc;
pub(crate) const LOG_TARGET: &str = "rpc-spec-v2::archive";
/// The buffer capacity for each storage query.
///
/// This is small because the underlying JSON-RPC server has
/// its down buffer capacity per connection as well.
const STORAGE_QUERY_BUF: usize = 16;
/// An API for archive RPC calls.
pub struct Archive<BE: Backend<Block>, Block: BlockT, Client> {
/// Bizinikiwi client.
client: Arc<Client>,
/// Backend of the chain.
backend: Arc<BE>,
/// Executor to spawn subscriptions.
executor: SubscriptionTaskExecutor,
/// The hexadecimal encoded hash of the genesis block.
genesis_hash: String,
/// Phantom member to pin the block type.
_phantom: PhantomData<Block>,
}
impl<BE: Backend<Block>, Block: BlockT, Client> Archive<BE, Block, Client> {
/// Create a new [`Archive`].
pub fn new<GenesisHash: AsRef<[u8]>>(
client: Arc<Client>,
backend: Arc<BE>,
genesis_hash: GenesisHash,
executor: SubscriptionTaskExecutor,
) -> Self {
let genesis_hash = hex_string(&genesis_hash.as_ref());
Self { client, backend, executor, genesis_hash, _phantom: PhantomData }
}
}
/// Parse hex-encoded string parameter as raw bytes.
///
/// If the parsing fails, returns an error propagated to the RPC method.
fn parse_hex_param(param: String) -> Result<Vec<u8>, ArchiveError> {
// Methods can accept empty parameters.
if param.is_empty() {
return Ok(Default::default());
}
array_bytes::hex2bytes(&param).map_err(|_| ArchiveError::InvalidParam(param))
}
#[async_trait]
impl<BE, Block, Client> ArchiveApiServer<Block::Hash> for Archive<BE, Block, Client>
where
Block: BlockT + 'static,
Block::Header: Unpin,
BE: Backend<Block> + 'static,
Client: BlockBackend<Block>
+ ExecutorProvider<Block>
+ HeaderBackend<Block>
+ HeaderMetadata<Block, Error = BlockChainError>
+ BlockchainEvents<Block>
+ CallApiAt<Block>
+ StorageProvider<Block, BE>
+ 'static,
{
fn archive_v1_body(&self, hash: Block::Hash) -> Result<Option<Vec<String>>, Infallible> {
let Ok(Some(signed_block)) = self.client.block(hash) else { return Ok(None) };
let extrinsics = signed_block
.block
.extrinsics()
.iter()
.map(|extrinsic| hex_string(&extrinsic.encode()))
.collect();
Ok(Some(extrinsics))
}
fn archive_v1_genesis_hash(&self) -> Result<String, Infallible> {
Ok(self.genesis_hash.clone())
}
fn archive_v1_header(&self, hash: Block::Hash) -> Result<Option<String>, Infallible> {
let Ok(Some(header)) = self.client.header(hash) else { return Ok(None) };
Ok(Some(hex_string(&header.encode())))
}
fn archive_v1_finalized_height(&self) -> Result<u64, Infallible> {
Ok(self.client.info().finalized_number.saturated_into())
}
fn archive_v1_hash_by_height(&self, height: u64) -> Result<Vec<String>, ArchiveError> {
let height: NumberFor<Block> = U256::from(height)
.try_into()
.map_err(|_| ArchiveError::InvalidParam(format!("Invalid block height: {}", height)))?;
let finalized_num = self.client.info().finalized_number;
if finalized_num >= height {
let Ok(Some(hash)) = self.client.block_hash(height) else { return Ok(vec![]) };
return Ok(vec![hex_string(&hash.as_ref())]);
}
let blockchain = self.backend.blockchain();
// Fetch all the leaves of the blockchain that are on a higher or equal height.
let mut headers: Vec<_> = blockchain
.leaves()
.map_err(|error| ArchiveError::FetchLeaves(error.to_string()))?
.into_iter()
.filter_map(|hash| {
let Ok(Some(header)) = self.client.header(hash) else { return None };
if header.number() < &height {
return None;
}
Some(header)
})
.collect();
let mut result = Vec::new();
let mut visited = HashSet::new();
while let Some(header) = headers.pop() {
if header.number() == &height {
result.push(hex_string(&header.hash().as_ref()));
continue;
}
let parent_hash = *header.parent_hash();
// Continue the iteration for unique hashes.
// Forks might intersect on a common chain that is not yet finalized.
if visited.insert(parent_hash) {
let Ok(Some(next_header)) = self.client.header(parent_hash) else { continue };
headers.push(next_header);
}
}
Ok(result)
}
fn archive_v1_call(
&self,
hash: Block::Hash,
function: String,
call_parameters: String,
) -> Result<MethodResult, ArchiveError> {
let call_parameters = Bytes::from(parse_hex_param(call_parameters)?);
let result =
self.client
.executor()
.call(hash, &function, &call_parameters, CallContext::Offchain);
Ok(match result {
Ok(result) => MethodResult::ok(hex_string(&result)),
Err(error) => MethodResult::err(error.to_string()),
})
}
fn archive_v1_storage(
&self,
pending: PendingSubscriptionSink,
hash: Block::Hash,
items: Vec<StorageQuery<String>>,
child_trie: Option<String>,
) {
let mut storage_client =
StorageSubscriptionClient::<Client, Block, BE>::new(self.client.clone());
let fut = async move {
let Ok(mut sink) = pending.accept().await.map(Subscription::from) else { return };
let items = match items
.into_iter()
.map(|query| {
let key = StorageKey(parse_hex_param(query.key)?);
// Validate that paginationStartKey is only used with descendant queries
if query.pagination_start_key.is_some() &&
!query.query_type.is_descendant_query()
{
return Err(ArchiveError::InvalidParam(
"paginationStartKey is only valid for descendantsValues and descendantsHashes query types"
.to_string(),
));
}
let pagination_start_key = query
.pagination_start_key
.map(|key| parse_hex_param(key).map(StorageKey))
.transpose()?;
Ok(StorageQuery { key, query_type: query.query_type, pagination_start_key })
})
.collect::<Result<Vec<_>, ArchiveError>>()
{
Ok(items) => items,
Err(error) => {
let _ = sink.send(&ArchiveStorageEvent::err(error.to_string())).await;
return;
},
};
let child_trie = child_trie.map(|child_trie| parse_hex_param(child_trie)).transpose();
let child_trie = match child_trie {
Ok(child_trie) => child_trie.map(ChildInfo::new_default_from_vec),
Err(error) => {
let _ = sink.send(&ArchiveStorageEvent::err(error.to_string())).await;
return;
},
};
let (tx, mut rx) = tokio::sync::mpsc::channel(STORAGE_QUERY_BUF);
let storage_fut = storage_client.generate_events(hash, items, child_trie, tx);
// We don't care about the return value of this join:
// - process_events might encounter an error (if the client disconnected)
// - storage_fut might encounter an error while processing a trie queries and
// the error is propagated via the sink.
let _ = futures::future::join(storage_fut, process_storage_events(&mut rx, &mut sink))
.await;
};
self.executor.spawn("bizinikiwi-rpc-subscription", Some("rpc"), fut.boxed());
}
fn archive_v1_storage_diff(
&self,
pending: PendingSubscriptionSink,
hash: Block::Hash,
items: Vec<ArchiveStorageDiffItem<String>>,
previous_hash: Option<Block::Hash>,
) {
let storage_client = ArchiveStorageDiff::new(self.client.clone());
let client = self.client.clone();
log::trace!(target: LOG_TARGET, "Storage diff subscription started");
let fut = async move {
let Ok(mut sink) = pending.accept().await.map(Subscription::from) else { return };
let previous_hash = if let Some(previous_hash) = previous_hash {
previous_hash
} else {
let Ok(Some(current_header)) = client.header(hash) else {
let message = format!("Block header is not present: {hash}");
let _ = sink.send(&ArchiveStorageDiffEvent::err(message)).await;
return;
};
*current_header.parent_hash()
};
let (tx, mut rx) = tokio::sync::mpsc::channel(STORAGE_QUERY_BUF);
let storage_fut =
storage_client.handle_trie_queries(hash, items, previous_hash, tx.clone());
// We don't care about the return value of this join:
// - process_events might encounter an error (if the client disconnected)
// - storage_fut might encounter an error while processing a trie queries and
// the error is propagated via the sink.
let _ =
futures::future::join(storage_fut, process_storage_diff_events(&mut rx, &mut sink))
.await;
};
self.executor.spawn("bizinikiwi-rpc-subscription", Some("rpc"), fut.boxed());
}
}
/// Sends all the events of the storage_diff method to the sink.
async fn process_storage_diff_events(
rx: &mut mpsc::Receiver<ArchiveStorageDiffEvent>,
sink: &mut Subscription,
) {
loop {
tokio::select! {
_ = sink.closed() => {
return
},
maybe_event = rx.recv() => {
let Some(event) = maybe_event else {
break;
};
if event.is_done() {
log::debug!(target: LOG_TARGET, "Finished processing partial trie query");
} else if event.is_err() {
log::debug!(target: LOG_TARGET, "Error encountered while processing partial trie query");
}
if sink.send(&event).await.is_err() {
return
}
}
}
}
}
/// Sends all the events of the storage method to the sink.
async fn process_storage_events(rx: &mut mpsc::Receiver<QueryResult>, sink: &mut Subscription) {
loop {
tokio::select! {
_ = sink.closed() => {
break
}
maybe_storage = rx.recv() => {
let Some(event) = maybe_storage else {
break;
};
match event {
Ok(None) => continue,
Ok(Some(event)) =>
if sink.send(&ArchiveStorageEvent::result(event)).await.is_err() {
return
},
Err(error) => {
let _ = sink.send(&ArchiveStorageEvent::err(error)).await;
return
}
}
}
}
}
let _ = sink.send(&ArchiveStorageEvent::StorageDone).await;
}
@@ -0,0 +1,850 @@
// 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 <https://www.gnu.org/licenses/>.
//! Implementation of the `archive_storage` method.
use std::{
collections::{hash_map::Entry, HashMap},
sync::Arc,
};
use itertools::Itertools;
use pezsc_client_api::{Backend, ChildInfo, StorageKey, StorageProvider};
use pezsp_runtime::traits::Block as BlockT;
use super::error::Error as ArchiveError;
use crate::{
archive::archive::LOG_TARGET,
common::{
events::{
ArchiveStorageDiffEvent, ArchiveStorageDiffItem, ArchiveStorageDiffOperationType,
ArchiveStorageDiffResult, ArchiveStorageDiffType, StorageResult,
},
storage::Storage,
},
};
use tokio::sync::mpsc;
/// Parse hex-encoded string parameter as raw bytes.
///
/// If the parsing fails, returns an error propagated to the RPC method.
pub fn parse_hex_param(param: String) -> Result<Vec<u8>, ArchiveError> {
// Methods can accept empty parameters.
if param.is_empty() {
return Ok(Default::default());
}
array_bytes::hex2bytes(&param).map_err(|_| ArchiveError::InvalidParam(param))
}
#[derive(Debug, PartialEq, Clone)]
pub struct DiffDetails {
key: StorageKey,
return_type: ArchiveStorageDiffType,
child_trie_key: Option<ChildInfo>,
child_trie_key_string: Option<String>,
}
/// The type of storage query.
#[derive(Debug, PartialEq, Clone, Copy)]
enum FetchStorageType {
/// Only fetch the value.
Value,
/// Only fetch the hash.
Hash,
/// Fetch both the value and the hash.
Both,
}
/// The return value of the `fetch_storage` method.
#[derive(Debug, PartialEq, Clone)]
enum FetchedStorage {
/// Storage value under a key.
Value(StorageResult),
/// Storage hash under a key.
Hash(StorageResult),
/// Both storage value and hash under a key.
Both { value: StorageResult, hash: StorageResult },
}
pub struct ArchiveStorageDiff<Client, Block, BE> {
client: Storage<Client, Block, BE>,
}
impl<Client, Block, BE> ArchiveStorageDiff<Client, Block, BE> {
pub fn new(client: Arc<Client>) -> Self {
Self { client: Storage::new(client) }
}
}
impl<Client, Block, BE> ArchiveStorageDiff<Client, Block, BE>
where
Block: BlockT + 'static,
BE: Backend<Block> + 'static,
Client: StorageProvider<Block, BE> + Send + Sync + 'static,
{
/// Fetch the storage from the given key.
fn fetch_storage(
&self,
hash: Block::Hash,
key: StorageKey,
maybe_child_trie: Option<ChildInfo>,
ty: FetchStorageType,
) -> Result<Option<FetchedStorage>, String> {
match ty {
FetchStorageType::Value => {
let result = self.client.query_value(hash, &key, maybe_child_trie.as_ref())?;
Ok(result.map(FetchedStorage::Value))
},
FetchStorageType::Hash => {
let result = self.client.query_hash(hash, &key, maybe_child_trie.as_ref())?;
Ok(result.map(FetchedStorage::Hash))
},
FetchStorageType::Both => {
let Some(value) = self.client.query_value(hash, &key, maybe_child_trie.as_ref())?
else {
return Ok(None);
};
let Some(hash) = self.client.query_hash(hash, &key, maybe_child_trie.as_ref())?
else {
return Ok(None);
};
Ok(Some(FetchedStorage::Both { value, hash }))
},
}
}
/// Check if the key belongs to the provided query items.
///
/// A key belongs to the query items when:
/// - the provided key is a prefix of the key in the query items.
/// - the query items are empty.
///
/// Returns an optional `FetchStorageType` based on the query items.
/// If the key does not belong to the query items, returns `None`.
fn belongs_to_query(key: &StorageKey, items: &[DiffDetails]) -> Option<FetchStorageType> {
// User has requested all keys, by default this fallbacks to fetching the value.
if items.is_empty() {
return Some(FetchStorageType::Value);
}
let mut value = false;
let mut hash = false;
for item in items {
if key.as_ref().starts_with(&item.key.as_ref()) {
match item.return_type {
ArchiveStorageDiffType::Value => value = true,
ArchiveStorageDiffType::Hash => hash = true,
}
}
}
match (value, hash) {
(true, true) => Some(FetchStorageType::Both),
(true, false) => Some(FetchStorageType::Value),
(false, true) => Some(FetchStorageType::Hash),
(false, false) => None,
}
}
/// Send the provided result to the `tx` sender.
///
/// Returns `false` if the sender has been closed.
fn send_result(
tx: &mpsc::Sender<ArchiveStorageDiffEvent>,
result: FetchedStorage,
operation_type: ArchiveStorageDiffOperationType,
child_trie_key: Option<String>,
) -> bool {
let items = match result {
FetchedStorage::Value(storage_result) | FetchedStorage::Hash(storage_result) => {
vec![storage_result]
},
FetchedStorage::Both { value, hash } => vec![value, hash],
};
for item in items {
let res = ArchiveStorageDiffEvent::StorageDiff(ArchiveStorageDiffResult {
key: item.key,
result: item.result,
operation_type,
child_trie_key: child_trie_key.clone(),
});
if tx.blocking_send(res).is_err() {
return false;
}
}
true
}
fn handle_trie_queries_inner(
&self,
hash: Block::Hash,
previous_hash: Block::Hash,
items: Vec<DiffDetails>,
tx: &mpsc::Sender<ArchiveStorageDiffEvent>,
) -> Result<(), String> {
// Parse the child trie key as `ChildInfo` and `String`.
let maybe_child_trie = items.first().and_then(|item| item.child_trie_key.clone());
let maybe_child_trie_str =
items.first().and_then(|item| item.child_trie_key_string.clone());
// Iterator over the current block and previous block
// at the same time to compare the keys. This approach effectively
// leverages backpressure to avoid memory consumption.
let keys_iter = self.client.raw_keys_iter(hash, maybe_child_trie.clone())?;
let previous_keys_iter =
self.client.raw_keys_iter(previous_hash, maybe_child_trie.clone())?;
let mut diff_iter = lexicographic_diff(keys_iter, previous_keys_iter);
while let Some(item) = diff_iter.next() {
let (operation_type, key) = match item {
Diff::Added(key) => (ArchiveStorageDiffOperationType::Added, key),
Diff::Deleted(key) => (ArchiveStorageDiffOperationType::Deleted, key),
Diff::Equal(key) => (ArchiveStorageDiffOperationType::Modified, key),
};
let Some(fetch_type) = Self::belongs_to_query(&key, &items) else {
// The key does not belong the the query items.
continue;
};
let maybe_result = match operation_type {
ArchiveStorageDiffOperationType::Added =>
self.fetch_storage(hash, key.clone(), maybe_child_trie.clone(), fetch_type)?,
ArchiveStorageDiffOperationType::Deleted => self.fetch_storage(
previous_hash,
key.clone(),
maybe_child_trie.clone(),
fetch_type,
)?,
ArchiveStorageDiffOperationType::Modified => {
let Some(storage_result) = self.fetch_storage(
hash,
key.clone(),
maybe_child_trie.clone(),
fetch_type,
)?
else {
continue;
};
let Some(previous_storage_result) = self.fetch_storage(
previous_hash,
key.clone(),
maybe_child_trie.clone(),
fetch_type,
)?
else {
continue;
};
// For modified records we need to check the actual storage values.
if storage_result == previous_storage_result {
continue;
}
Some(storage_result)
},
};
if let Some(storage_result) = maybe_result {
if !Self::send_result(
&tx,
storage_result,
operation_type,
maybe_child_trie_str.clone(),
) {
return Ok(());
}
}
}
Ok(())
}
/// This method will iterate over the keys of the main trie or a child trie and fetch the
/// given keys. The fetched keys will be sent to the provided `tx` sender to leverage
/// the backpressure mechanism.
pub async fn handle_trie_queries(
&self,
hash: Block::Hash,
items: Vec<ArchiveStorageDiffItem<String>>,
previous_hash: Block::Hash,
tx: mpsc::Sender<ArchiveStorageDiffEvent>,
) -> Result<(), tokio::task::JoinError> {
let this = ArchiveStorageDiff { client: self.client.clone() };
tokio::task::spawn_blocking(move || {
// Deduplicate the items.
let mut trie_items = match deduplicate_storage_diff_items(items) {
Ok(items) => items,
Err(error) => {
let _ = tx.blocking_send(ArchiveStorageDiffEvent::err(error.to_string()));
return;
},
};
// Default to using the main storage trie if no items are provided.
if trie_items.is_empty() {
trie_items.push(Vec::new());
}
log::trace!(target: LOG_TARGET, "Storage diff deduplicated items: {:?}", trie_items);
for items in trie_items {
log::trace!(
target: LOG_TARGET,
"handle_trie_queries: hash={:?}, previous_hash={:?}, items={:?}",
hash,
previous_hash,
items
);
let result = this.handle_trie_queries_inner(hash, previous_hash, items, &tx);
if let Err(error) = result {
log::trace!(
target: LOG_TARGET,
"handle_trie_queries: sending error={:?}",
error,
);
let _ = tx.blocking_send(ArchiveStorageDiffEvent::err(error));
return;
} else {
log::trace!(
target: LOG_TARGET,
"handle_trie_queries: sending storage diff done",
);
}
}
let _ = tx.blocking_send(ArchiveStorageDiffEvent::StorageDiffDone);
})
.await?;
Ok(())
}
}
/// The result of the `lexicographic_diff` method.
#[derive(Debug, PartialEq)]
enum Diff<T> {
Added(T),
Deleted(T),
Equal(T),
}
/// Compare two iterators lexicographically and return the differences.
fn lexicographic_diff<T, LeftIter, RightIter>(
mut left: LeftIter,
mut right: RightIter,
) -> impl Iterator<Item = Diff<T>>
where
T: Ord,
LeftIter: Iterator<Item = T>,
RightIter: Iterator<Item = T>,
{
let mut a = left.next();
let mut b = right.next();
core::iter::from_fn(move || match (a.take(), b.take()) {
(Some(a_value), Some(b_value)) =>
if a_value < b_value {
b = Some(b_value);
a = left.next();
Some(Diff::Added(a_value))
} else if a_value > b_value {
a = Some(a_value);
b = right.next();
Some(Diff::Deleted(b_value))
} else {
a = left.next();
b = right.next();
Some(Diff::Equal(a_value))
},
(Some(a_value), None) => {
a = left.next();
Some(Diff::Added(a_value))
},
(None, Some(b_value)) => {
b = right.next();
Some(Diff::Deleted(b_value))
},
(None, None) => None,
})
}
/// Deduplicate the provided items and return a list of `DiffDetails`.
///
/// Each list corresponds to a single child trie or the main trie.
fn deduplicate_storage_diff_items(
items: Vec<ArchiveStorageDiffItem<String>>,
) -> Result<Vec<Vec<DiffDetails>>, ArchiveError> {
let mut deduplicated: HashMap<Option<ChildInfo>, Vec<DiffDetails>> = HashMap::new();
for diff_item in items {
// Ensure the provided hex keys are valid before deduplication.
let key = StorageKey(parse_hex_param(diff_item.key)?);
let child_trie_key_string = diff_item.child_trie_key.clone();
let child_trie_key = diff_item
.child_trie_key
.map(|child_trie_key| parse_hex_param(child_trie_key))
.transpose()?
.map(ChildInfo::new_default_from_vec);
let diff_item = DiffDetails {
key,
return_type: diff_item.return_type,
child_trie_key: child_trie_key.clone(),
child_trie_key_string,
};
match deduplicated.entry(child_trie_key.clone()) {
Entry::Occupied(mut entry) => {
let mut should_insert = true;
for existing in entry.get() {
// This points to a different return type.
if existing.return_type != diff_item.return_type {
continue;
}
// Keys and return types are identical.
if existing.key == diff_item.key {
should_insert = false;
break;
}
// The following two conditions ensure that we keep the shortest key.
// The current key is a longer prefix of the existing key.
if diff_item.key.as_ref().starts_with(&existing.key.as_ref()) {
should_insert = false;
break;
}
// The existing key is a longer prefix of the current key.
// We need to keep the current key and remove the existing one.
if existing.key.as_ref().starts_with(&diff_item.key.as_ref()) {
let to_remove = existing.clone();
entry.get_mut().retain(|item| item != &to_remove);
break;
}
}
if should_insert {
entry.get_mut().push(diff_item);
}
},
Entry::Vacant(entry) => {
entry.insert(vec![diff_item]);
},
}
}
Ok(deduplicated
.into_iter()
.sorted_by_key(|(child_trie_key, _)| child_trie_key.clone())
.map(|(_, values)| values)
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dedup_empty() {
let items = vec![];
let result = deduplicate_storage_diff_items(items).unwrap();
assert!(result.is_empty());
}
#[test]
fn dedup_single() {
let items = vec![ArchiveStorageDiffItem {
key: "0x01".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
}];
let result = deduplicate_storage_diff_items(items).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].len(), 1);
let expected = DiffDetails {
key: StorageKey(vec![1]),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
child_trie_key_string: None,
};
assert_eq!(result[0][0], expected);
}
#[test]
fn dedup_with_different_keys() {
let items = vec![
ArchiveStorageDiffItem {
key: "0x01".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
},
ArchiveStorageDiffItem {
key: "0x02".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
},
];
let result = deduplicate_storage_diff_items(items).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].len(), 2);
let expected = vec![
DiffDetails {
key: StorageKey(vec![1]),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
child_trie_key_string: None,
},
DiffDetails {
key: StorageKey(vec![2]),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
child_trie_key_string: None,
},
];
assert_eq!(result[0], expected);
}
#[test]
fn dedup_with_same_keys() {
// Identical keys.
let items = vec![
ArchiveStorageDiffItem {
key: "0x01".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
},
ArchiveStorageDiffItem {
key: "0x01".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
},
];
let result = deduplicate_storage_diff_items(items).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].len(), 1);
let expected = vec![DiffDetails {
key: StorageKey(vec![1]),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
child_trie_key_string: None,
}];
assert_eq!(result[0], expected);
}
#[test]
fn dedup_with_same_prefix() {
// Identical keys.
let items = vec![
ArchiveStorageDiffItem {
key: "0x01".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
},
ArchiveStorageDiffItem {
key: "0x01ff".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
},
];
let result = deduplicate_storage_diff_items(items).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].len(), 1);
let expected = vec![DiffDetails {
key: StorageKey(vec![1]),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
child_trie_key_string: None,
}];
assert_eq!(result[0], expected);
}
#[test]
fn dedup_with_different_return_types() {
let items = vec![
ArchiveStorageDiffItem {
key: "0x01".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
},
ArchiveStorageDiffItem {
key: "0x01".into(),
return_type: ArchiveStorageDiffType::Hash,
child_trie_key: None,
},
];
let result = deduplicate_storage_diff_items(items).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].len(), 2);
let expected = vec![
DiffDetails {
key: StorageKey(vec![1]),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
child_trie_key_string: None,
},
DiffDetails {
key: StorageKey(vec![1]),
return_type: ArchiveStorageDiffType::Hash,
child_trie_key: None,
child_trie_key_string: None,
},
];
assert_eq!(result[0], expected);
}
#[test]
fn dedup_with_different_child_tries() {
let items = vec![
ArchiveStorageDiffItem {
key: "0x01".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: Some("0x01".into()),
},
ArchiveStorageDiffItem {
key: "0x01".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: Some("0x02".into()),
},
];
let result = deduplicate_storage_diff_items(items).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].len(), 1);
assert_eq!(result[1].len(), 1);
let expected = vec![
vec![DiffDetails {
key: StorageKey(vec![1]),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: Some(ChildInfo::new_default_from_vec(vec![1])),
child_trie_key_string: Some("0x01".into()),
}],
vec![DiffDetails {
key: StorageKey(vec![1]),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: Some(ChildInfo::new_default_from_vec(vec![2])),
child_trie_key_string: Some("0x02".into()),
}],
];
assert_eq!(result, expected);
}
#[test]
fn dedup_with_same_child_tries() {
let items = vec![
ArchiveStorageDiffItem {
key: "0x01".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: Some("0x01".into()),
},
ArchiveStorageDiffItem {
key: "0x01".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: Some("0x01".into()),
},
];
let result = deduplicate_storage_diff_items(items).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].len(), 1);
let expected = vec![DiffDetails {
key: StorageKey(vec![1]),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: Some(ChildInfo::new_default_from_vec(vec![1])),
child_trie_key_string: Some("0x01".into()),
}];
assert_eq!(result[0], expected);
}
#[test]
fn dedup_with_shorter_key_reverse_order() {
let items = vec![
ArchiveStorageDiffItem {
key: "0x01ff".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
},
ArchiveStorageDiffItem {
key: "0x01".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
},
];
let result = deduplicate_storage_diff_items(items).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].len(), 1);
let expected = vec![DiffDetails {
key: StorageKey(vec![1]),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
child_trie_key_string: None,
}];
assert_eq!(result[0], expected);
}
#[test]
fn dedup_multiple_child_tries() {
let items = vec![
ArchiveStorageDiffItem {
key: "0x02".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
},
ArchiveStorageDiffItem {
key: "0x01".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: Some("0x01".into()),
},
ArchiveStorageDiffItem {
key: "0x02".into(),
return_type: ArchiveStorageDiffType::Hash,
child_trie_key: Some("0x01".into()),
},
ArchiveStorageDiffItem {
key: "0x01".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: Some("0x02".into()),
},
ArchiveStorageDiffItem {
key: "0x01".into(),
return_type: ArchiveStorageDiffType::Hash,
child_trie_key: Some("0x02".into()),
},
ArchiveStorageDiffItem {
key: "0x01ff".into(),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: Some("0x02".into()),
},
];
let result = deduplicate_storage_diff_items(items).unwrap();
let expected = vec![
vec![DiffDetails {
key: StorageKey(vec![2]),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
child_trie_key_string: None,
}],
vec![
DiffDetails {
key: StorageKey(vec![1]),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: Some(ChildInfo::new_default_from_vec(vec![1])),
child_trie_key_string: Some("0x01".into()),
},
DiffDetails {
key: StorageKey(vec![2]),
return_type: ArchiveStorageDiffType::Hash,
child_trie_key: Some(ChildInfo::new_default_from_vec(vec![1])),
child_trie_key_string: Some("0x01".into()),
},
],
vec![
DiffDetails {
key: StorageKey(vec![1]),
return_type: ArchiveStorageDiffType::Value,
child_trie_key: Some(ChildInfo::new_default_from_vec(vec![2])),
child_trie_key_string: Some("0x02".into()),
},
DiffDetails {
key: StorageKey(vec![1]),
return_type: ArchiveStorageDiffType::Hash,
child_trie_key: Some(ChildInfo::new_default_from_vec(vec![2])),
child_trie_key_string: Some("0x02".into()),
},
],
];
assert_eq!(result, expected);
}
#[test]
fn test_lexicographic_diff() {
let left = vec![1, 2, 3, 4, 5];
let right = vec![2, 3, 4, 5, 6];
let diff = lexicographic_diff(left.into_iter(), right.into_iter()).collect::<Vec<_>>();
let expected = vec![
Diff::Added(1),
Diff::Equal(2),
Diff::Equal(3),
Diff::Equal(4),
Diff::Equal(5),
Diff::Deleted(6),
];
assert_eq!(diff, expected);
}
#[test]
fn test_lexicographic_diff_one_side_empty() {
let left = vec![];
let right = vec![1, 2, 3, 4, 5, 6];
let diff = lexicographic_diff(left.into_iter(), right.into_iter()).collect::<Vec<_>>();
let expected = vec![
Diff::Deleted(1),
Diff::Deleted(2),
Diff::Deleted(3),
Diff::Deleted(4),
Diff::Deleted(5),
Diff::Deleted(6),
];
assert_eq!(diff, expected);
let left = vec![1, 2, 3, 4, 5, 6];
let right = vec![];
let diff = lexicographic_diff(left.into_iter(), right.into_iter()).collect::<Vec<_>>();
let expected = vec![
Diff::Added(1),
Diff::Added(2),
Diff::Added(3),
Diff::Added(4),
Diff::Added(5),
Diff::Added(6),
];
assert_eq!(diff, expected);
}
}
@@ -0,0 +1,68 @@
// 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 <https://www.gnu.org/licenses/>.
//! Error helpers for `archive` RPC module.
use jsonrpsee::types::error::ErrorObject;
/// ChainHead RPC errors.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// Invalid parameter provided to the RPC method.
#[error("Invalid parameter: {0}")]
InvalidParam(String),
/// Runtime call failed.
#[error("Runtime call: {0}")]
RuntimeCall(String),
/// Failed to fetch leaves.
#[error("Failed to fetch leaves of the chain: {0}")]
FetchLeaves(String),
}
// Base code for all `archive` errors.
const BASE_ERROR: i32 = 3000;
/// Invalid parameter error.
const INVALID_PARAM_ERROR: i32 = BASE_ERROR + 1;
/// Runtime call error.
const RUNTIME_CALL_ERROR: i32 = BASE_ERROR + 2;
/// Failed to fetch leaves.
const FETCH_LEAVES_ERROR: i32 = BASE_ERROR + 3;
impl From<Error> for ErrorObject<'static> {
fn from(e: Error) -> Self {
let msg = e.to_string();
match e {
Error::InvalidParam(_) => ErrorObject::owned(INVALID_PARAM_ERROR, msg, None::<()>),
Error::RuntimeCall(_) => ErrorObject::owned(RUNTIME_CALL_ERROR, msg, None::<()>),
Error::FetchLeaves(_) => ErrorObject::owned(FETCH_LEAVES_ERROR, msg, None::<()>),
}
.into()
}
}
/// The error type for errors that can never happen.
//
// NOTE: Can't use std::convert::Infallible because of the orphan-rule
pub enum Infallible {}
impl From<Infallible> for ErrorObject<'static> {
fn from(e: Infallible) -> Self {
match e {}
}
}
@@ -0,0 +1,37 @@
// 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 <https://www.gnu.org/licenses/>.
//! Bizinikiwi archive API.
//!
//! # Note
//!
//! Methods are prefixed by `archive`.
#[cfg(test)]
mod tests;
mod archive_storage;
mod types;
pub mod api;
pub mod archive;
pub mod error;
pub use api::ArchiveApiServer;
pub use archive::Archive;
pub use types::{MethodResult, MethodResultErr, MethodResultOk};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,90 @@
// 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 <https://www.gnu.org/licenses/>.
use serde::{Deserialize, Serialize};
/// The result of an RPC method.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub enum MethodResult {
/// Method generated a result.
Ok(MethodResultOk),
/// Method encountered an error.
Err(MethodResultErr),
}
impl MethodResult {
/// Constructs a successful result.
pub fn ok(result: impl Into<String>) -> MethodResult {
MethodResult::Ok(MethodResultOk { success: true, value: result.into() })
}
/// Constructs an error result.
pub fn err(error: impl Into<String>) -> MethodResult {
MethodResult::Err(MethodResultErr { success: false, error: error.into() })
}
}
/// The successful result of an RPC method.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MethodResultOk {
/// Method was successful.
pub success: bool,
/// The result of the method.
pub value: String,
}
/// The error result of an RPC method.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MethodResultErr {
/// Method encountered an error.
pub success: bool,
/// The error of the method.
pub error: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn method_result_ok() {
let ok = MethodResult::ok("hello");
let ser = serde_json::to_string(&ok).unwrap();
let exp = r#"{"success":true,"value":"hello"}"#;
assert_eq!(ser, exp);
let ok_dec: MethodResult = serde_json::from_str(exp).unwrap();
assert_eq!(ok_dec, ok);
}
#[test]
fn method_result_error() {
let ok = MethodResult::err("hello");
let ser = serde_json::to_string(&ok).unwrap();
let exp = r#"{"success":false,"error":"hello"}"#;
assert_eq!(ser, exp);
let ok_dec: MethodResult = serde_json::from_str(exp).unwrap();
assert_eq!(ok_dec, ok);
}
}
@@ -0,0 +1,154 @@
// 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 <https://www.gnu.org/licenses/>.
#![allow(non_snake_case)]
//! API trait of the chain head.
use crate::{
chain_head::{
error::Error,
event::{FollowEvent, MethodResponse},
},
common::events::StorageQuery,
};
use jsonrpsee::{proc_macros::rpc, server::ResponsePayload};
pub use pezsp_rpc::list::ListOrValue;
#[rpc(client, server)]
pub trait ChainHeadApi<Hash> {
/// Track the state of the head of the chain: the finalized, non-finalized, and best blocks.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[subscription(
name = "chainHead_v1_follow" => "chainHead_v1_followEvent",
unsubscribe = "chainHead_v1_unfollow",
item = FollowEvent<Hash>,
)]
fn chain_head_unstable_follow(&self, with_runtime: bool);
/// Retrieves the body (list of transactions) of a pinned block.
///
/// This method should be seen as a complement to `chainHead_v1_follow`,
/// allowing the JSON-RPC client to retrieve more information about a block
/// that has been reported.
///
/// Use `archive_v1_body` if instead you want to retrieve the body of an arbitrary block.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "chainHead_v1_body", with_extensions)]
async fn chain_head_unstable_body(
&self,
follow_subscription: String,
hash: Hash,
) -> ResponsePayload<'static, MethodResponse>;
/// Retrieves the header of a pinned block.
///
/// This method should be seen as a complement to `chainHead_v1_follow`,
/// allowing the JSON-RPC client to retrieve more information about a block
/// that has been reported.
///
/// Use `archive_v1_header` if instead you want to retrieve the header of an arbitrary
/// block.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "chainHead_v1_header", with_extensions)]
async fn chain_head_unstable_header(
&self,
follow_subscription: String,
hash: Hash,
) -> Result<Option<String>, Error>;
/// Returns storage entries at a specific block's state.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "chainHead_v1_storage", with_extensions)]
async fn chain_head_unstable_storage(
&self,
follow_subscription: String,
hash: Hash,
items: Vec<StorageQuery<String>>,
child_trie: Option<String>,
) -> ResponsePayload<'static, MethodResponse>;
/// Call into the Runtime API at a specified block's state.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "chainHead_v1_call", with_extensions)]
async fn chain_head_unstable_call(
&self,
follow_subscription: String,
hash: Hash,
function: String,
call_parameters: String,
) -> ResponsePayload<'static, MethodResponse>;
/// Unpin a block or multiple blocks reported by the `follow` method.
///
/// Ongoing operations that require the provided block
/// will continue normally.
///
/// When this method returns an error, it is guaranteed that no blocks have been unpinned.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "chainHead_v1_unpin", with_extensions)]
async fn chain_head_unstable_unpin(
&self,
follow_subscription: String,
hash_or_hashes: ListOrValue<Hash>,
) -> Result<(), Error>;
/// Resumes a storage fetch started with `chainHead_storage` after it has generated an
/// `operationWaitingForContinue` event.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "chainHead_v1_continue", with_extensions)]
async fn chain_head_unstable_continue(
&self,
follow_subscription: String,
operation_id: String,
) -> Result<(), Error>;
/// Stops an operation started with chainHead_v1_body, chainHead_v1_call, or
/// chainHead_v1_storage. If the operation was still in progress, this interrupts it. If
/// the operation was already finished, this call has no effect.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "chainHead_v1_stopOperation", with_extensions)]
async fn chain_head_unstable_stop_operation(
&self,
follow_subscription: String,
operation_id: String,
) -> Result<(), Error>;
}
@@ -0,0 +1,704 @@
// 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 <https://www.gnu.org/licenses/>.
//! API implementation for `chainHead`.
use super::{
chain_head_storage::ChainHeadStorage,
event::{MethodResponseStarted, OperationBodyDone, OperationCallDone},
};
use crate::{
chain_head::{
api::ChainHeadApiServer,
chain_head_follow::ChainHeadFollower,
error::Error as ChainHeadRpcError,
event::{FollowEvent, MethodResponse, OperationError, OperationId, OperationStorageItems},
subscription::{StopHandle, SubscriptionManagement, SubscriptionManagementError},
FollowEventSendError, FollowEventSender,
},
common::{events::StorageQuery, storage::QueryResult},
hex_string, SubscriptionTaskExecutor,
};
use codec::Encode;
use futures::{channel::oneshot, future::FutureExt, SinkExt};
use jsonrpsee::{
core::async_trait, server::ResponsePayload, types::SubscriptionId, ConnectionId, Extensions,
MethodResponseFuture, PendingSubscriptionSink,
};
use log::debug;
use pezsc_client_api::{
Backend, BlockBackend, BlockchainEvents, CallExecutor, ChildInfo, ExecutorProvider, StorageKey,
StorageProvider,
};
use pezsc_rpc::utils::Subscription;
use pezsp_api::CallApiAt;
use pezsp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata};
use pezsp_core::{traits::CallContext, Bytes};
use pezsp_rpc::list::ListOrValue;
use pezsp_runtime::traits::Block as BlockT;
use std::{marker::PhantomData, sync::Arc, time::Duration};
use tokio::sync::mpsc;
pub(crate) const LOG_TARGET: &str = "rpc-spec-v2";
/// The buffer capacity for each storage query.
///
/// This is small because the underlying JSON-RPC server has
/// its down buffer capacity per connection as well.
const STORAGE_QUERY_BUF: usize = 16;
/// The configuration of [`ChainHead`].
pub struct ChainHeadConfig {
/// The maximum number of pinned blocks across all subscriptions.
pub global_max_pinned_blocks: usize,
/// The maximum duration that a block is allowed to be pinned per subscription.
pub subscription_max_pinned_duration: Duration,
/// The maximum number of ongoing operations per subscription.
pub subscription_max_ongoing_operations: usize,
/// Stop all subscriptions if the distance between the leaves and the current finalized
/// block is larger than this value.
pub max_lagging_distance: usize,
/// The maximum number of `chainHead_follow` subscriptions per connection.
pub max_follow_subscriptions_per_connection: usize,
/// The maximum number of pending messages per subscription.
pub subscription_buffer_cap: usize,
}
/// Maximum pinned blocks across all connections.
/// This number is large enough to consider immediate blocks.
/// Note: This should never exceed the `PINNING_CACHE_SIZE` from client/db.
pub(crate) const MAX_PINNED_BLOCKS: usize = 512;
/// Any block of any subscription should not be pinned more than
/// this constant. When a subscription contains a block older than this,
/// the subscription becomes subject to termination.
/// Note: This should be enough for immediate blocks.
const MAX_PINNED_DURATION: Duration = Duration::from_secs(60);
/// The maximum number of ongoing operations per subscription.
/// Note: The lower limit imposed by the spec is 16.
const MAX_ONGOING_OPERATIONS: usize = 16;
/// Stop all subscriptions if the distance between the leaves and the current finalized
/// block is larger than this value.
const MAX_LAGGING_DISTANCE: usize = 128;
/// The maximum number of `chainHead_follow` subscriptions per connection.
const MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION: usize = 4;
impl Default for ChainHeadConfig {
fn default() -> Self {
ChainHeadConfig {
global_max_pinned_blocks: MAX_PINNED_BLOCKS,
subscription_max_pinned_duration: MAX_PINNED_DURATION,
subscription_max_ongoing_operations: MAX_ONGOING_OPERATIONS,
max_lagging_distance: MAX_LAGGING_DISTANCE,
max_follow_subscriptions_per_connection: MAX_FOLLOW_SUBSCRIPTIONS_PER_CONNECTION,
subscription_buffer_cap: MAX_PINNED_BLOCKS,
}
}
}
/// An API for chain head RPC calls.
pub struct ChainHead<BE: Backend<Block>, Block: BlockT, Client> {
/// Bizinikiwi client.
client: Arc<Client>,
/// Backend of the chain.
backend: Arc<BE>,
/// Executor to spawn subscriptions.
executor: SubscriptionTaskExecutor,
/// Keep track of the pinned blocks for each subscription.
subscriptions: SubscriptionManagement<Block, BE>,
/// Stop all subscriptions if the distance between the leaves and the current finalized
/// block is larger than this value.
max_lagging_distance: usize,
/// Phantom member to pin the block type.
_phantom: PhantomData<Block>,
/// The maximum number of pending messages per subscription.
subscription_buffer_cap: usize,
}
impl<BE: Backend<Block>, Block: BlockT, Client> ChainHead<BE, Block, Client> {
/// Create a new [`ChainHead`].
pub fn new(
client: Arc<Client>,
backend: Arc<BE>,
executor: SubscriptionTaskExecutor,
config: ChainHeadConfig,
) -> Self {
Self {
client,
backend: backend.clone(),
executor,
subscriptions: SubscriptionManagement::new(
config.global_max_pinned_blocks,
config.subscription_max_pinned_duration,
config.subscription_max_ongoing_operations,
config.max_follow_subscriptions_per_connection,
backend,
),
max_lagging_distance: config.max_lagging_distance,
subscription_buffer_cap: config.subscription_buffer_cap,
_phantom: PhantomData,
}
}
}
/// Helper to convert the `subscription ID` to a string.
pub fn read_subscription_id_as_string(sink: &Subscription) -> String {
match sink.subscription_id() {
SubscriptionId::Num(n) => n.to_string(),
SubscriptionId::Str(s) => s.into_owned().into(),
}
}
/// Parse hex-encoded string parameter as raw bytes.
///
/// If the parsing fails, returns an error propagated to the RPC method.
fn parse_hex_param(param: String) -> Result<Vec<u8>, ChainHeadRpcError> {
// Methods can accept empty parameters.
if param.is_empty() {
return Ok(Default::default());
}
match array_bytes::hex2bytes(&param) {
Ok(bytes) => Ok(bytes),
Err(_) => Err(ChainHeadRpcError::InvalidParam(param)),
}
}
#[async_trait]
impl<BE, Block, Client> ChainHeadApiServer<Block::Hash> for ChainHead<BE, Block, Client>
where
Block: BlockT + 'static,
Block::Header: Unpin,
BE: Backend<Block> + 'static,
Client: BlockBackend<Block>
+ ExecutorProvider<Block>
+ HeaderBackend<Block>
+ HeaderMetadata<Block, Error = BlockChainError>
+ BlockchainEvents<Block>
+ CallApiAt<Block>
+ StorageProvider<Block, BE>
+ 'static,
{
fn chain_head_unstable_follow(&self, pending: PendingSubscriptionSink, with_runtime: bool) {
let subscriptions = self.subscriptions.clone();
let backend = self.backend.clone();
let client = self.client.clone();
let max_lagging_distance = self.max_lagging_distance;
let subscription_buffer_cap = self.subscription_buffer_cap;
let fut = async move {
// Ensure the current connection ID has enough space to accept a new subscription.
let connection_id = pending.connection_id();
// The RAII `reserved_subscription` will clean up resources on drop:
// - free the reserved subscription for the connection ID.
// - remove the subscription ID from the subscription management.
let Some(mut reserved_subscription) = subscriptions.reserve_subscription(connection_id)
else {
pending.reject(ChainHeadRpcError::ReachedLimits).await;
return;
};
let Ok(sink) = pending.accept().await.map(Subscription::from) else { return };
let sub_id = read_subscription_id_as_string(&sink);
// Keep track of the subscription.
let Some(sub_data) =
reserved_subscription.insert_subscription(sub_id.clone(), with_runtime)
else {
// Inserting the subscription can only fail if the JsonRPSee generated a duplicate
// subscription ID.
debug!(target: LOG_TARGET, "[follow][id={:?}] Subscription already accepted", sub_id);
let _ = sink.send(&FollowEvent::<String>::Stop).await;
return;
};
debug!(target: LOG_TARGET, "[follow][id={:?}] Subscription accepted", sub_id);
let mut chain_head_follow = ChainHeadFollower::new(
client,
backend,
subscriptions,
with_runtime,
sub_id.clone(),
max_lagging_distance,
subscription_buffer_cap,
);
let result = chain_head_follow.generate_events(sink, sub_data).await;
if let Err(SubscriptionManagementError::BlockDistanceTooLarge) = result {
debug!(target: LOG_TARGET, "[follow][id={:?}] All subscriptions are stopped", sub_id);
reserved_subscription.stop_all_subscriptions();
}
debug!(target: LOG_TARGET, "[follow][id={:?}] Subscription removed", sub_id);
};
self.executor.spawn("bizinikiwi-rpc-subscription", Some("rpc"), fut.boxed());
}
async fn chain_head_unstable_body(
&self,
ext: &Extensions,
follow_subscription: String,
hash: Block::Hash,
) -> ResponsePayload<'static, MethodResponse> {
let conn_id = ext
.get::<ConnectionId>()
.copied()
.expect("ConnectionId is always set by jsonrpsee; qed");
if !self.subscriptions.contains_subscription(conn_id, &follow_subscription) {
// The spec says to return `LimitReached` if the follow subscription is invalid or
// stale.
return ResponsePayload::success(MethodResponse::LimitReached);
}
let client = self.client.clone();
let subscriptions = self.subscriptions.clone();
let executor = self.executor.clone();
let result = spawn_blocking(&self.executor, async move {
let mut block_guard = match subscriptions.lock_block(&follow_subscription, hash, 1) {
Ok(block) => block,
Err(SubscriptionManagementError::SubscriptionAbsent) |
Err(SubscriptionManagementError::ExceededLimits) =>
return ResponsePayload::success(MethodResponse::LimitReached),
Err(SubscriptionManagementError::BlockHashAbsent) => {
// Block is not part of the subscription.
return ResponsePayload::error(ChainHeadRpcError::InvalidBlock);
},
Err(_) => return ResponsePayload::error(ChainHeadRpcError::InvalidBlock),
};
let operation_id = block_guard.operation().operation_id();
let event = match client.block(hash) {
Ok(Some(signed_block)) => {
let extrinsics = signed_block
.block
.extrinsics()
.iter()
.map(|extrinsic| hex_string(&extrinsic.encode()))
.collect();
FollowEvent::<Block::Hash>::OperationBodyDone(OperationBodyDone {
operation_id: operation_id.clone(),
value: extrinsics,
})
},
Ok(None) => {
// The block's body was pruned. This subscription ID has become invalid.
debug!(
target: LOG_TARGET,
"[body][id={:?}] Stopping subscription because hash={:?} was pruned",
&follow_subscription,
hash
);
subscriptions.remove_subscription(&follow_subscription);
return ResponsePayload::error(ChainHeadRpcError::InvalidBlock);
},
Err(error) => FollowEvent::<Block::Hash>::OperationError(OperationError {
operation_id: operation_id.clone(),
error: error.to_string(),
}),
};
let (rp, rp_fut) = method_started_response(operation_id, None);
let fut = async move {
// Wait for the server to send out the response and if it produces an error no event
// should be generated.
if rp_fut.await.is_err() {
return;
}
let _ = block_guard.response_sender().send(event).await;
};
executor.spawn_blocking("bizinikiwi-rpc-subscription", Some("rpc"), fut.boxed());
rp
});
result
.await
.unwrap_or_else(|_| ResponsePayload::success(MethodResponse::LimitReached))
}
async fn chain_head_unstable_header(
&self,
ext: &Extensions,
follow_subscription: String,
hash: Block::Hash,
) -> Result<Option<String>, ChainHeadRpcError> {
let conn_id = ext
.get::<ConnectionId>()
.copied()
.expect("ConnectionId is always set by jsonrpsee; qed");
if !self.subscriptions.contains_subscription(conn_id, &follow_subscription) {
return Ok(None);
}
let block_guard = match self.subscriptions.lock_block(&follow_subscription, hash, 1) {
Ok(block) => block,
Err(SubscriptionManagementError::SubscriptionAbsent) |
Err(SubscriptionManagementError::ExceededLimits) => return Ok(None),
Err(SubscriptionManagementError::BlockHashAbsent) => {
// Block is not part of the subscription.
return Err(ChainHeadRpcError::InvalidBlock.into());
},
Err(_) => return Err(ChainHeadRpcError::InvalidBlock.into()),
};
let client = self.client.clone();
let result = spawn_blocking(&self.executor, async move {
let _block_guard = block_guard;
client
.header(hash)
.map(|opt_header| opt_header.map(|h| hex_string(&h.encode())))
.map_err(|err| ChainHeadRpcError::InternalError(err.to_string()))
});
result.await.unwrap_or_else(|_| Ok(None))
}
async fn chain_head_unstable_storage(
&self,
ext: &Extensions,
follow_subscription: String,
hash: Block::Hash,
items: Vec<StorageQuery<String>>,
child_trie: Option<String>,
) -> ResponsePayload<'static, MethodResponse> {
let conn_id = ext
.get::<ConnectionId>()
.copied()
.expect("ConnectionId is always set by jsonrpsee; qed");
if !self.subscriptions.contains_subscription(conn_id, &follow_subscription) {
// The spec says to return `LimitReached` if the follow subscription is invalid or
// stale.
return ResponsePayload::success(MethodResponse::LimitReached);
}
// Gain control over parameter parsing and returned error.
let items = match items
.into_iter()
.map(|query| {
let key = StorageKey(parse_hex_param(query.key)?);
Ok(StorageQuery { key, query_type: query.query_type, pagination_start_key: None })
})
.collect::<Result<Vec<_>, ChainHeadRpcError>>()
{
Ok(items) => items,
Err(err) => {
return ResponsePayload::error(err);
},
};
let child_trie = match child_trie.map(|child_trie| parse_hex_param(child_trie)).transpose()
{
Ok(c) => c.map(ChildInfo::new_default_from_vec),
Err(e) => return ResponsePayload::error(e),
};
let mut block_guard =
match self.subscriptions.lock_block(&follow_subscription, hash, items.len()) {
Ok(block) => block,
Err(SubscriptionManagementError::SubscriptionAbsent) |
Err(SubscriptionManagementError::ExceededLimits) => {
return ResponsePayload::success(MethodResponse::LimitReached);
},
Err(SubscriptionManagementError::BlockHashAbsent) => {
// Block is not part of the subscription.
return ResponsePayload::error(ChainHeadRpcError::InvalidBlock);
},
Err(_) => return ResponsePayload::error(ChainHeadRpcError::InvalidBlock),
};
let mut storage_client = ChainHeadStorage::<Client, Block, BE>::new(self.client.clone());
// Storage items are never discarded.
let (rp, rp_fut) = method_started_response(block_guard.operation().operation_id(), Some(0));
let fut = async move {
// Wait for the server to send out the response and if it produces an error no event
// should be generated.
if rp_fut.await.is_err() {
return;
}
let (tx, rx) = tokio::sync::mpsc::channel(STORAGE_QUERY_BUF);
let operation_id = block_guard.operation().operation_id();
let stop_handle = block_guard.operation().stop_handle().clone();
let response_sender = block_guard.response_sender();
// May fail if the channel is closed or the connection is closed.
// which is okay to ignore.
let _ = futures::future::join(
storage_client.generate_events(hash, items, child_trie, tx),
process_storage_items(rx, response_sender, operation_id, &stop_handle),
)
.await;
};
self.executor.spawn("bizinikiwi-rpc-subscription", Some("rpc"), fut.boxed());
rp
}
async fn chain_head_unstable_call(
&self,
ext: &Extensions,
follow_subscription: String,
hash: Block::Hash,
function: String,
call_parameters: String,
) -> ResponsePayload<'static, MethodResponse> {
let call_parameters = match parse_hex_param(call_parameters) {
Ok(hex) => Bytes::from(hex),
Err(err) => return ResponsePayload::error(err),
};
let conn_id = ext
.get::<ConnectionId>()
.copied()
.expect("ConnectionId is always set by jsonrpsee; qed");
if !self.subscriptions.contains_subscription(conn_id, &follow_subscription) {
// The spec says to return `LimitReached` if the follow subscription is invalid or
// stale.
return ResponsePayload::success(MethodResponse::LimitReached);
}
let mut block_guard = match self.subscriptions.lock_block(&follow_subscription, hash, 1) {
Ok(block) => block,
Err(SubscriptionManagementError::SubscriptionAbsent) |
Err(SubscriptionManagementError::ExceededLimits) => {
// Invalid invalid subscription ID.
return ResponsePayload::success(MethodResponse::LimitReached);
},
Err(SubscriptionManagementError::BlockHashAbsent) => {
// Block is not part of the subscription.
return ResponsePayload::error(ChainHeadRpcError::InvalidBlock);
},
Err(_) => return ResponsePayload::error(ChainHeadRpcError::InvalidBlock),
};
// Reject subscription if with_runtime is false.
if !block_guard.has_runtime() {
return ResponsePayload::error(ChainHeadRpcError::InvalidRuntimeCall(
"The runtime updates flag must be set".to_string(),
));
}
let operation_id = block_guard.operation().operation_id();
let client = self.client.clone();
let (rp, rp_fut) = method_started_response(operation_id.clone(), None);
let fut = async move {
// Wait for the server to send out the response and if it produces an error no event
// should be generated.
if rp_fut.await.is_err() {
return;
}
let event = client
.executor()
.call(hash, &function, &call_parameters, CallContext::Offchain)
.map(|result| {
FollowEvent::<Block::Hash>::OperationCallDone(OperationCallDone {
operation_id: operation_id.clone(),
output: hex_string(&result),
})
})
.unwrap_or_else(|error| {
FollowEvent::<Block::Hash>::OperationError(OperationError {
operation_id: operation_id.clone(),
error: error.to_string(),
})
});
let _ = block_guard.response_sender().send(event).await;
};
self.executor
.spawn_blocking("bizinikiwi-rpc-subscription", Some("rpc"), fut.boxed());
rp
}
async fn chain_head_unstable_unpin(
&self,
ext: &Extensions,
follow_subscription: String,
hash_or_hashes: ListOrValue<Block::Hash>,
) -> Result<(), ChainHeadRpcError> {
let conn_id = ext
.get::<ConnectionId>()
.copied()
.expect("ConnectionId is always set by jsonrpsee; qed");
if !self.subscriptions.contains_subscription(conn_id, &follow_subscription) {
return Ok(());
}
let result = match hash_or_hashes {
ListOrValue::Value(hash) =>
self.subscriptions.unpin_blocks(&follow_subscription, [hash]),
ListOrValue::List(hashes) =>
self.subscriptions.unpin_blocks(&follow_subscription, hashes),
};
match result {
Ok(()) => Ok(()),
Err(SubscriptionManagementError::SubscriptionAbsent) => {
// Invalid invalid subscription ID.
Ok(())
},
Err(SubscriptionManagementError::BlockHashAbsent) => {
// Block is not part of the subscription.
Err(ChainHeadRpcError::InvalidBlock)
},
Err(SubscriptionManagementError::DuplicateHashes) =>
Err(ChainHeadRpcError::InvalidDuplicateHashes),
Err(_) => Err(ChainHeadRpcError::InvalidBlock),
}
}
async fn chain_head_unstable_continue(
&self,
ext: &Extensions,
follow_subscription: String,
operation_id: String,
) -> Result<(), ChainHeadRpcError> {
let conn_id = ext
.get::<ConnectionId>()
.copied()
.expect("ConnectionId is always set by jsonrpsee; qed");
if !self.subscriptions.contains_subscription(conn_id, &follow_subscription) {
return Ok(());
}
// WaitingForContinue event is never emitted, in such cases
// emit an `InvalidContinue error`.
if self.subscriptions.get_operation(&follow_subscription, &operation_id).is_some() {
Err(ChainHeadRpcError::InvalidContinue.into())
} else {
Ok(())
}
}
async fn chain_head_unstable_stop_operation(
&self,
ext: &Extensions,
follow_subscription: String,
operation_id: String,
) -> Result<(), ChainHeadRpcError> {
let conn_id = ext
.get::<ConnectionId>()
.copied()
.expect("ConnectionId is always set by jsonrpsee; qed");
if !self.subscriptions.contains_subscription(conn_id, &follow_subscription) {
return Ok(());
}
let Some(mut operation) =
self.subscriptions.get_operation(&follow_subscription, &operation_id)
else {
return Ok(());
};
operation.stop();
Ok(())
}
}
fn method_started_response(
operation_id: String,
discarded_items: Option<usize>,
) -> (ResponsePayload<'static, MethodResponse>, MethodResponseFuture) {
let rp = MethodResponse::Started(MethodResponseStarted { operation_id, discarded_items });
ResponsePayload::success(rp).notify_on_completion()
}
/// Spawn a blocking future on the provided executor and return the result on a oneshot channel.
///
/// This is a wrapper to extract the result of a `executor.spawn_blocking` future.
fn spawn_blocking<R>(
executor: &SubscriptionTaskExecutor,
fut: impl std::future::Future<Output = R> + Send + 'static,
) -> oneshot::Receiver<R>
where
R: Send + 'static,
{
let (tx, rx) = oneshot::channel();
let blocking_fut = async move {
let result = fut.await;
// Send the result back on the channel.
let _ = tx.send(result);
};
executor.spawn_blocking("bizinikiwi-rpc-subscription", Some("rpc"), blocking_fut.boxed());
rx
}
async fn process_storage_items<Hash>(
mut storage_query_stream: mpsc::Receiver<QueryResult>,
mut sender: FollowEventSender<Hash>,
operation_id: String,
stop_handle: &StopHandle,
) -> Result<(), FollowEventSendError> {
loop {
tokio::select! {
_ = stop_handle.stopped() => {
break;
},
maybe_storage = storage_query_stream.recv() => {
let Some(storage) = maybe_storage else {
break;
};
let item = match storage {
QueryResult::Err(error) => {
return sender
.send(FollowEvent::OperationError(OperationError { operation_id, error }))
.await
}
QueryResult::Ok(Some(v)) => v,
QueryResult::Ok(None) => continue,
};
sender
.send(FollowEvent::OperationStorageItems(OperationStorageItems {
operation_id: operation_id.clone(),
items: vec![item],
})).await?;
},
}
}
sender
.send(FollowEvent::OperationStorageDone(OperationId { operation_id }))
.await?;
Ok(())
}
@@ -0,0 +1,792 @@
// 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 <https://www.gnu.org/licenses/>.
//! Implementation of the `chainHead_follow` method.
use crate::chain_head::{
chain_head::{LOG_TARGET, MAX_PINNED_BLOCKS},
event::{
BestBlockChanged, Finalized, FollowEvent, Initialized, NewBlock, RuntimeEvent,
RuntimeVersionEvent,
},
subscription::{InsertedSubscriptionData, SubscriptionManagement, SubscriptionManagementError},
};
use futures::{
channel::oneshot,
stream::{self, Stream, StreamExt, TryStreamExt},
};
use log::debug;
use pezsc_client_api::{
Backend, BlockBackend, BlockImportNotification, BlockchainEvents, FinalityNotification,
StaleBlock,
};
use pezsc_rpc::utils::Subscription;
use schnellru::{ByLength, LruMap};
use pezsp_api::CallApiAt;
use pezsp_blockchain::{
Backend as BlockChainBackend, Error as BlockChainError, HeaderBackend, HeaderMetadata, Info,
};
use pezsp_runtime::{
traits::{Block as BlockT, Header as HeaderT, NumberFor},
SaturatedConversion, Saturating,
};
use std::{
collections::{HashSet, VecDeque},
sync::Arc,
};
/// The maximum number of finalized blocks provided by the
/// `Initialized` event.
const MAX_FINALIZED_BLOCKS: usize = 16;
/// Generates the events of the `chainHead_follow` method.
pub struct ChainHeadFollower<BE: Backend<Block>, Block: BlockT, Client> {
/// Bizinikiwi client.
client: Arc<Client>,
/// Backend of the chain.
backend: Arc<BE>,
/// Subscriptions handle.
sub_handle: SubscriptionManagement<Block, BE>,
/// Subscription was started with the runtime updates flag.
with_runtime: bool,
/// Subscription ID.
sub_id: String,
/// The best reported block by this subscription.
current_best_block: Option<Block::Hash>,
/// LRU cache of pruned blocks.
pruned_blocks: LruMap<Block::Hash, ()>,
/// LRU cache of announced blocks.
announced_blocks: AnnouncedBlocks<Block>,
/// Stop all subscriptions if the distance between the leaves and the current finalized
/// block is larger than this value.
max_lagging_distance: usize,
/// The maximum number of pending messages per subscription.
pub subscription_buffer_cap: usize,
}
struct AnnouncedBlocks<Block: BlockT> {
/// Unfinalized blocks.
blocks: LruMap<Block::Hash, ()>,
/// Finalized blocks.
finalized: MostRecentFinalizedBlocks<Block>,
}
/// Wrapper over LRU to efficiently lookup hashes and remove elements as FIFO queue.
///
/// For the finalized blocks we use `peek` to avoid moving the block counter to the front.
/// This effectively means that the LRU acts as a FIFO queue. Otherwise, we might
/// end up with scenarios where the "finalized block" in the end of LRU is overwritten which
/// may not necessarily be the oldest finalized block i.e, possible that "get" promotes an
/// older finalized block because it was accessed more recently.
struct MostRecentFinalizedBlocks<Block: BlockT>(LruMap<Block::Hash, ()>);
impl<Block: BlockT> MostRecentFinalizedBlocks<Block> {
/// Insert the finalized block hash into the LRU cache.
fn insert(&mut self, block: Block::Hash) {
self.0.insert(block, ());
}
/// Check if the block is contained in the LRU cache.
fn contains(&mut self, block: &Block::Hash) -> Option<&()> {
self.0.peek(block)
}
}
impl<Block: BlockT> AnnouncedBlocks<Block> {
/// Creates a new `AnnouncedBlocks`.
fn new() -> Self {
Self {
// The total number of pinned blocks is `MAX_PINNED_BLOCKS`, ensure we don't
// exceed the limit.
blocks: LruMap::new(ByLength::new((MAX_PINNED_BLOCKS - MAX_FINALIZED_BLOCKS) as u32)),
// We are keeping a smaller number of announced finalized blocks in memory.
// This is because the `Finalized` event might be triggered before the `NewBlock` event.
finalized: MostRecentFinalizedBlocks(LruMap::new(ByLength::new(
MAX_FINALIZED_BLOCKS as u32,
))),
}
}
/// Insert the block into the announced blocks.
fn insert(&mut self, block: Block::Hash, finalized: bool) {
if finalized {
// When a block is declared as finalized, it is removed from the unfinalized blocks.
//
// Given that the finalized blocks are bounded to `MAX_FINALIZED_BLOCKS`,
// this ensures we keep the minimum number of blocks in memory.
self.blocks.remove(&block);
self.finalized.insert(block);
} else {
self.blocks.insert(block, ());
}
}
/// Check if the block was previously announced.
fn was_announced(&mut self, block: &Block::Hash) -> bool {
self.blocks.get(block).is_some() || self.finalized.contains(block).is_some()
}
}
impl<BE: Backend<Block>, Block: BlockT, Client> ChainHeadFollower<BE, Block, Client> {
/// Create a new [`ChainHeadFollower`].
pub fn new(
client: Arc<Client>,
backend: Arc<BE>,
sub_handle: SubscriptionManagement<Block, BE>,
with_runtime: bool,
sub_id: String,
max_lagging_distance: usize,
subscription_buffer_cap: usize,
) -> Self {
Self {
client,
backend,
sub_handle,
with_runtime,
sub_id,
current_best_block: None,
pruned_blocks: LruMap::new(ByLength::new(
MAX_PINNED_BLOCKS.try_into().unwrap_or(u32::MAX),
)),
announced_blocks: AnnouncedBlocks::new(),
max_lagging_distance,
subscription_buffer_cap,
}
}
}
/// A block notification.
enum NotificationType<Block: BlockT> {
/// The initial events generated from the node's memory.
InitialEvents(Vec<FollowEvent<Block::Hash>>),
/// The new block notification obtained from `import_notification_stream`.
NewBlock(BlockImportNotification<Block>),
/// The finalized block notification obtained from `finality_notification_stream`.
Finalized(FinalityNotification<Block>),
/// The response of `chainHead` method calls.
MethodResponse(FollowEvent<Block::Hash>),
}
/// The initial blocks that should be reported or ignored by the chainHead.
#[derive(Clone, Debug)]
struct InitialBlocks<Block: BlockT> {
/// Children of the latest finalized block, for which the `NewBlock`
/// event must be generated.
///
/// It is a tuple of (block hash, parent hash).
finalized_block_descendants: Vec<(Block::Hash, Block::Hash)>,
/// Hashes of the last finalized blocks
finalized_block_hashes: VecDeque<Block::Hash>,
/// Blocks that should not be reported as pruned by the `Finalized` event.
///
/// Bizinikiwi database will perform the pruning of height N at
/// the finalization N + 1. We could have the following block tree
/// when the user subscribes to the `follow` method:
/// [A] - [A1] - [A2] - [A3]
/// ^^ finalized
/// - [A1] - [B1]
///
/// When the A3 block is finalized, B1 is reported as pruned, however
/// B1 was never reported as `NewBlock` (and as such was never pinned).
/// This is because the `NewBlock` events are generated for children of
/// the finalized hash.
pruned_forks: HashSet<Block::Hash>,
}
/// The startup point from which chainHead started to generate events.
struct StartupPoint<Block: BlockT> {
/// Best block hash.
pub best_hash: Block::Hash,
/// The head of the finalized chain.
pub finalized_hash: Block::Hash,
/// Last finalized block number.
pub finalized_number: NumberFor<Block>,
}
impl<Block: BlockT> From<Info<Block>> for StartupPoint<Block> {
fn from(info: Info<Block>) -> Self {
StartupPoint::<Block> {
best_hash: info.best_hash,
finalized_hash: info.finalized_hash,
finalized_number: info.finalized_number,
}
}
}
impl<BE, Block, Client> ChainHeadFollower<BE, Block, Client>
where
Block: BlockT + 'static,
BE: Backend<Block> + 'static,
Client: BlockBackend<Block>
+ HeaderBackend<Block>
+ HeaderMetadata<Block, Error = BlockChainError>
+ BlockchainEvents<Block>
+ CallApiAt<Block>
+ 'static,
{
/// Conditionally generate the runtime event of the given block.
fn generate_runtime_event(
&self,
block: Block::Hash,
parent: Option<Block::Hash>,
) -> Option<RuntimeEvent> {
// No runtime versions should be reported.
if !self.with_runtime {
return None;
}
let block_rt = match self.client.runtime_version_at(block) {
Ok(rt) => rt,
Err(err) => return Some(err.into()),
};
let parent = match parent {
Some(parent) => parent,
// Nothing to compare against, always report.
None => return Some(RuntimeEvent::Valid(RuntimeVersionEvent { spec: block_rt.into() })),
};
let parent_rt = match self.client.runtime_version_at(parent) {
Ok(rt) => rt,
Err(err) => return Some(err.into()),
};
// Report the runtime version change.
if block_rt != parent_rt {
Some(RuntimeEvent::Valid(RuntimeVersionEvent { spec: block_rt.into() }))
} else {
None
}
}
/// Check the distance between the provided blocks does not exceed a
/// a reasonable range.
///
/// When the blocks are too far apart (potentially millions of blocks):
/// - Tree route is expensive to calculate.
/// - The RPC layer will not be able to generate the `NewBlock` events for all blocks.
///
/// This edge-case can happen for teyrchains where the relay chain syncs slower to
/// the head of the chain than the teyrchain node that is synced already.
fn distance_within_reason(
&self,
block: Block::Hash,
finalized: Block::Hash,
) -> Result<(), SubscriptionManagementError> {
let Some(block_num) = self.client.number(block)? else {
return Err(SubscriptionManagementError::BlockHashAbsent);
};
let Some(finalized_num) = self.client.number(finalized)? else {
return Err(SubscriptionManagementError::BlockHashAbsent);
};
let distance: usize = block_num.saturating_sub(finalized_num).saturated_into();
if distance > self.max_lagging_distance {
return Err(SubscriptionManagementError::BlockDistanceTooLarge);
}
Ok(())
}
/// Get the in-memory blocks of the client, starting from the provided finalized hash.
///
/// The reported blocks are pinned by this function.
fn get_init_blocks_with_forks(
&self,
finalized: Block::Hash,
) -> Result<InitialBlocks<Block>, SubscriptionManagementError> {
let blockchain = self.backend.blockchain();
let leaves = blockchain.leaves()?;
let mut pruned_forks = HashSet::new();
let mut finalized_block_descendants = Vec::new();
let mut unique_descendants = HashSet::new();
// Ensure all leaves are within a reasonable distance from the finalized block,
// before traversing the tree.
for leaf in &leaves {
self.distance_within_reason(*leaf, finalized)?;
}
for leaf in leaves {
let tree_route = pezsp_blockchain::tree_route(blockchain, finalized, leaf)?;
let blocks = tree_route.enacted().iter().map(|block| block.hash);
if !tree_route.retracted().is_empty() {
pruned_forks.extend(blocks);
} else {
// Ensure a `NewBlock` event is generated for all children of the
// finalized block. Describe the tree route as (child_node, parent_node)
// Note: the order of elements matters here.
let mut parent = finalized;
for child in blocks {
let pair = (child, parent);
if unique_descendants.insert(pair) {
// The finalized block is pinned below.
self.sub_handle.pin_block(&self.sub_id, child)?;
finalized_block_descendants.push(pair);
}
parent = child;
}
}
}
let mut current_block = finalized;
// The header of the finalized block must not be pruned.
let Some(header) = blockchain.header(current_block)? else {
return Err(SubscriptionManagementError::BlockHeaderAbsent);
};
// Report at most `MAX_FINALIZED_BLOCKS`. Note: The node might not have that many blocks.
let mut finalized_block_hashes = VecDeque::with_capacity(MAX_FINALIZED_BLOCKS);
// Pin the finalized block.
self.sub_handle.pin_block(&self.sub_id, current_block)?;
finalized_block_hashes.push_front(current_block);
current_block = *header.parent_hash();
for _ in 0..MAX_FINALIZED_BLOCKS - 1 {
let Ok(Some(header)) = blockchain.header(current_block) else { break };
// Block cannot be reported if pinning fails.
if self.sub_handle.pin_block(&self.sub_id, current_block).is_err() {
break;
};
finalized_block_hashes.push_front(current_block);
current_block = *header.parent_hash();
}
Ok(InitialBlocks { finalized_block_descendants, finalized_block_hashes, pruned_forks })
}
/// Generate the initial events reported by the RPC `follow` method.
///
/// Returns the initial events that should be reported directly.
fn generate_init_events(
&mut self,
startup_point: &StartupPoint<Block>,
) -> Result<Vec<FollowEvent<Block::Hash>>, SubscriptionManagementError> {
let init = self.get_init_blocks_with_forks(startup_point.finalized_hash)?;
// The initialized event is the first one sent.
let initial_blocks = init.finalized_block_descendants;
let finalized_block_hashes = init.finalized_block_hashes;
// These are the pruned blocks that we should not report again.
for pruned in init.pruned_forks {
self.pruned_blocks.insert(pruned, ());
}
let finalized_block_hash = startup_point.finalized_hash;
let finalized_block_runtime = self.generate_runtime_event(finalized_block_hash, None);
for finalized in &finalized_block_hashes {
self.announced_blocks.insert(*finalized, true);
}
let initialized_event = FollowEvent::Initialized(Initialized {
finalized_block_hashes: finalized_block_hashes.into(),
finalized_block_runtime,
with_runtime: self.with_runtime,
});
let mut finalized_block_descendants = Vec::with_capacity(initial_blocks.len() + 1);
finalized_block_descendants.push(initialized_event);
for (child, parent) in initial_blocks.into_iter() {
// If the parent was not announced we have a gap currently.
// This can happen during a WarpSync.
if !self.announced_blocks.was_announced(&parent) {
return Err(SubscriptionManagementError::BlockHeaderAbsent);
}
self.announced_blocks.insert(child, false);
let new_runtime = self.generate_runtime_event(child, Some(parent));
let event = FollowEvent::NewBlock(NewBlock {
block_hash: child,
parent_block_hash: parent,
new_runtime,
with_runtime: self.with_runtime,
});
finalized_block_descendants.push(event);
}
// Generate a new best block event.
let best_block_hash = startup_point.best_hash;
if best_block_hash != finalized_block_hash {
if !self.announced_blocks.was_announced(&best_block_hash) {
return Err(SubscriptionManagementError::BlockHeaderAbsent);
}
self.announced_blocks.insert(best_block_hash, true);
let best_block = FollowEvent::BestBlockChanged(BestBlockChanged { best_block_hash });
self.current_best_block = Some(best_block_hash);
finalized_block_descendants.push(best_block);
};
Ok(finalized_block_descendants)
}
/// Generate the "NewBlock" event and potentially the "BestBlockChanged" event for the
/// given block hash.
fn generate_import_events(
&mut self,
block_hash: Block::Hash,
parent_block_hash: Block::Hash,
is_best_block: bool,
) -> Vec<FollowEvent<Block::Hash>> {
let new_runtime = self.generate_runtime_event(block_hash, Some(parent_block_hash));
let new_block = FollowEvent::NewBlock(NewBlock {
block_hash,
parent_block_hash,
new_runtime,
with_runtime: self.with_runtime,
});
if !is_best_block {
return vec![new_block];
}
// If this is the new best block, then we need to generate two events.
let best_block_event =
FollowEvent::BestBlockChanged(BestBlockChanged { best_block_hash: block_hash });
match self.current_best_block {
Some(block_cache) => {
// The RPC layer has not reported this block as best before.
// Note: This handles the race with the finalized branch.
if block_cache != block_hash {
self.current_best_block = Some(block_hash);
vec![new_block, best_block_event]
} else {
vec![new_block]
}
},
None => {
self.current_best_block = Some(block_hash);
vec![new_block, best_block_event]
},
}
}
/// Handle the import of new blocks by generating the appropriate events.
fn handle_import_blocks(
&mut self,
notification: BlockImportNotification<Block>,
startup_point: &StartupPoint<Block>,
) -> Result<Vec<FollowEvent<Block::Hash>>, SubscriptionManagementError> {
let block_hash = notification.hash;
// Ensure we are only reporting blocks after the starting point.
if *notification.header.number() < startup_point.finalized_number {
return Ok(Default::default());
}
// Ensure the block can be pinned before generating the events.
if !self.sub_handle.pin_block(&self.sub_id, block_hash)? {
// The block is already pinned, this is similar to the check above.
//
// The `SubscriptionManagement` ensures the block is tracked until (short lived):
// - 2 calls to `pin_block` are made (from `Finalized` and `NewBlock` branches).
// - the block is unpinned by the user
//
// This is rather a sanity checks for edge-cases (in theory), where
// [`MAX_FINALIZED_BLOCKS` + 1] finalized events are triggered before the `NewBlock`
// event of the first `Finalized` event.
return Ok(Default::default());
}
if self.announced_blocks.was_announced(&block_hash) {
// Block was already reported by the finalized branch.
return Ok(Default::default());
}
// Double check the parent hash. If the parent hash is not reported, we have a gap.
let parent_block_hash = *notification.header.parent_hash();
if !self.announced_blocks.was_announced(&parent_block_hash) {
// The parent block was not reported, we have a gap.
return Err(SubscriptionManagementError::Custom("Parent block was not reported".into()));
}
self.announced_blocks.insert(block_hash, false);
Ok(self.generate_import_events(block_hash, parent_block_hash, notification.is_new_best))
}
/// Generates new block events from the given finalized hashes.
///
/// It may be possible that the `Finalized` event fired before the `NewBlock`
/// event. Only in that case we generate:
/// - `NewBlock` event for all finalized hashes.
/// - `BestBlock` event for the last finalized hash.
///
/// This function returns an empty list if all finalized hashes were already reported
/// and are pinned.
fn generate_finalized_events(
&mut self,
finalized_block_hashes: &[Block::Hash],
) -> Result<Vec<FollowEvent<Block::Hash>>, SubscriptionManagementError> {
let mut events = Vec::new();
// Nothing to be done if no finalized hashes are provided.
let Some(first_hash) = finalized_block_hashes.get(0) else { return Ok(Default::default()) };
// Find the parent header.
let Some(first_header) = self.client.header(*first_hash)? else {
return Err(SubscriptionManagementError::BlockHeaderAbsent);
};
if !self.announced_blocks.was_announced(first_header.parent_hash()) {
return Err(SubscriptionManagementError::Custom(
"Parent block was not reported for a finalized block".into(),
));
}
let parents =
std::iter::once(first_header.parent_hash()).chain(finalized_block_hashes.iter());
for (i, (hash, parent)) in finalized_block_hashes.iter().zip(parents).enumerate() {
// Ensure the block is pinned before generating the events.
self.sub_handle.pin_block(&self.sub_id, *hash)?;
// Check if the block was already reported.
if self.announced_blocks.was_announced(hash) {
continue;
}
// Generate `NewBlock` events for all blocks beside the last block in the list
let is_last = i + 1 == finalized_block_hashes.len();
if !is_last {
// Generate only the `NewBlock` event for this block.
events.extend(self.generate_import_events(*hash, *parent, false));
self.announced_blocks.insert(*hash, true);
continue;
}
if let Some(best_block_hash) = self.current_best_block {
let ancestor =
pezsp_blockchain::lowest_common_ancestor(&*self.client, *hash, best_block_hash)?;
// If we end up here and the `best_block` is a descendent of the finalized block
// (last block in the list), it means that there were skipped notifications.
// Otherwise `pin_block` would had returned `false`.
//
// When the node falls out of sync and then syncs up to the tip of the chain, it can
// happen that we skip notifications. Then it is better to terminate the connection
// instead of trying to send notifications for all missed blocks.
if ancestor.hash == *hash {
return Err(SubscriptionManagementError::Custom(
"A descendent of the finalized block was already reported".into(),
));
}
}
// Let's generate the `NewBlock` and `NewBestBlock` events for the block.
events.extend(self.generate_import_events(*hash, *parent, true));
self.announced_blocks.insert(*hash, true);
}
Ok(events)
}
/// Get all pruned block hashes from the provided stale blocks.
fn get_pruned_hashes(
&mut self,
stale_blocks: &[Arc<StaleBlock<Block>>],
) -> Result<Vec<Block::Hash>, SubscriptionManagementError> {
Ok(stale_blocks
.iter()
.filter_map(|block| {
if self.pruned_blocks.get(&block.hash).is_some() {
// The block was already reported as pruned.
return None;
}
self.pruned_blocks.insert(block.hash, ());
Some(block.hash)
})
.collect())
}
/// Handle the finalization notification by generating the `Finalized` event.
///
/// If the block of the notification was not reported yet, this method also
/// generates the events similar to `handle_import_blocks`.
fn handle_finalized_blocks(
&mut self,
notification: FinalityNotification<Block>,
startup_point: &StartupPoint<Block>,
) -> Result<Vec<FollowEvent<Block::Hash>>, SubscriptionManagementError> {
let last_finalized = notification.hash;
// Ensure we are only reporting blocks after the starting point.
if *notification.header.number() < startup_point.finalized_number {
return Ok(Default::default());
}
// The tree route contains the exclusive path from the last finalized block to the block
// reported by the notification. Ensure the finalized block is also reported.
let mut finalized_block_hashes = notification.tree_route.to_vec();
finalized_block_hashes.push(last_finalized);
// If the finalized hashes were not reported yet, generate the `NewBlock` events.
let mut events = self.generate_finalized_events(&finalized_block_hashes)?;
// Report all pruned blocks from the notification that are not
// part of the fork we need to ignore.
let pruned_block_hashes = self.get_pruned_hashes(&notification.stale_blocks)?;
for finalized in &finalized_block_hashes {
self.announced_blocks.insert(*finalized, true);
}
let finalized_event = FollowEvent::Finalized(Finalized {
finalized_block_hashes,
pruned_block_hashes: pruned_block_hashes.clone(),
});
if let Some(current_best_block) = self.current_best_block {
// We need to generate a new best block if the best block is in the pruned list.
let is_in_pruned_list =
pruned_block_hashes.iter().any(|hash| *hash == current_best_block);
if is_in_pruned_list {
self.current_best_block = Some(last_finalized);
events.push(FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: last_finalized,
}));
} else {
// The pruning logic ensures that when the finalized block is announced,
// all blocks on forks that have the common ancestor lower or equal
// to the finalized block are reported.
//
// However, we double check if the best block is a descendant of the last finalized
// block to ensure we don't miss any events.
let ancestor = pezsp_blockchain::lowest_common_ancestor(
&*self.client,
last_finalized,
current_best_block,
)?;
let is_descendant = ancestor.hash == last_finalized;
if !is_descendant {
self.current_best_block = Some(last_finalized);
events.push(FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: last_finalized,
}));
}
}
}
events.push(finalized_event);
Ok(events)
}
/// Submit the events from the provided stream to the RPC client
/// for as long as the `rx_stop` event was not called.
async fn submit_events<EventStream>(
&mut self,
startup_point: &StartupPoint<Block>,
stream: EventStream,
sink: Subscription,
rx_stop: oneshot::Receiver<()>,
) -> Result<(), SubscriptionManagementError>
where
EventStream: Stream<Item = NotificationType<Block>> + Unpin + Send,
{
let buffer_cap = self.subscription_buffer_cap;
// create a channel to propagate error messages
let mut handle_events = |event| match event {
NotificationType::InitialEvents(events) => Ok(events),
NotificationType::NewBlock(notification) =>
self.handle_import_blocks(notification, &startup_point),
NotificationType::Finalized(notification) =>
self.handle_finalized_blocks(notification, &startup_point),
NotificationType::MethodResponse(notification) => Ok(vec![notification]),
};
let stream = stream
.map(|event| handle_events(event))
.map_ok(|items| stream::iter(items).map(Ok))
.try_flatten();
tokio::pin!(stream);
let sink_future =
sink.pipe_from_try_stream(stream, pezsc_rpc::utils::BoundedVecDeque::new(buffer_cap));
let result = tokio::select! {
_ = rx_stop => Ok(()),
result = sink_future => {
if let Err(ref e) = result {
debug!(
target: LOG_TARGET,
"[follow][id={:?}] Failed to handle stream notification {:?}",
&self.sub_id,
e
);
};
result
}
};
let _ = sink.send(&FollowEvent::<String>::Stop).await;
result
}
/// Generate the block events for the `chainHead_follow` method.
pub async fn generate_events(
&mut self,
sink: Subscription,
sub_data: InsertedSubscriptionData<Block>,
) -> Result<(), SubscriptionManagementError> {
// Register for the new block and finalized notifications.
let stream_import = self
.client
.import_notification_stream()
.map(|notification| NotificationType::NewBlock(notification));
let stream_finalized = self
.client
.finality_notification_stream()
.map(|notification| NotificationType::Finalized(notification));
let stream_responses = sub_data
.response_receiver
.map(|response| NotificationType::MethodResponse(response));
let startup_point = StartupPoint::from(self.client.info());
let initial_events = match self.generate_init_events(&startup_point) {
Ok(blocks) => blocks,
Err(err) => {
debug!(
target: LOG_TARGET,
"[follow][id={:?}] Failed to generate the initial events {:?}",
self.sub_id,
err
);
let _ = sink.send(&FollowEvent::<String>::Stop).await;
return Err(err);
},
};
let initial = NotificationType::InitialEvents(initial_events);
let merged = tokio_stream::StreamExt::merge(stream_import, stream_finalized);
let merged = tokio_stream::StreamExt::merge(merged, stream_responses);
let stream = stream::once(futures::future::ready(initial)).chain(merged);
self.submit_events(&startup_point, stream.boxed(), sink, sub_data.rx_stop).await
}
}
@@ -0,0 +1,123 @@
// 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 <https://www.gnu.org/licenses/>.
//! Implementation of the `chainHead_storage` method.
use std::{marker::PhantomData, sync::Arc};
use pezsc_client_api::{Backend, ChildInfo, StorageKey, StorageProvider};
use pezsp_runtime::traits::Block as BlockT;
use tokio::sync::mpsc;
use crate::common::{
events::{StorageQuery, StorageQueryType},
storage::{IterQueryType, QueryIter, QueryResult, Storage},
};
/// Generates the events of the `chainHead_storage` method.
pub struct ChainHeadStorage<Client, Block, BE> {
/// Storage client.
client: Storage<Client, Block, BE>,
_phandom: PhantomData<(BE, Block)>,
}
impl<Client, Block, BE> Clone for ChainHeadStorage<Client, Block, BE> {
fn clone(&self) -> Self {
Self { client: self.client.clone(), _phandom: PhantomData }
}
}
impl<Client, Block, BE> ChainHeadStorage<Client, Block, BE> {
/// Constructs a new [`ChainHeadStorage`].
pub fn new(client: Arc<Client>) -> Self {
Self { client: Storage::new(client), _phandom: PhantomData }
}
}
impl<Client, Block, BE> ChainHeadStorage<Client, Block, BE>
where
Block: BlockT + 'static,
BE: Backend<Block> + 'static,
Client: StorageProvider<Block, BE> + Send + Sync + 'static,
{
/// Generate the block events for the `chainHead_storage` method.
pub async fn generate_events(
&mut self,
hash: Block::Hash,
items: Vec<StorageQuery<StorageKey>>,
child_key: Option<ChildInfo>,
tx: mpsc::Sender<QueryResult>,
) -> Result<(), tokio::task::JoinError> {
let this = self.clone();
tokio::task::spawn_blocking(move || {
for item in items {
match item.query_type {
StorageQueryType::Value => {
let rp = this.client.query_value(hash, &item.key, child_key.as_ref());
if tx.blocking_send(rp).is_err() {
break;
}
},
StorageQueryType::Hash => {
let rp = this.client.query_hash(hash, &item.key, child_key.as_ref());
if tx.blocking_send(rp).is_err() {
break;
}
},
StorageQueryType::ClosestDescendantMerkleValue => {
let rp =
this.client.query_merkle_value(hash, &item.key, child_key.as_ref());
if tx.blocking_send(rp).is_err() {
break;
}
},
StorageQueryType::DescendantsValues => {
let query = QueryIter {
query_key: item.key,
ty: IterQueryType::Value,
pagination_start_key: None,
};
this.client.query_iter_pagination_with_producer(
query,
hash,
child_key.as_ref(),
&tx,
)
},
StorageQueryType::DescendantsHashes => {
let query = QueryIter {
query_key: item.key,
ty: IterQueryType::Hash,
pagination_start_key: None,
};
this.client.query_iter_pagination_with_producer(
query,
hash,
child_key.as_ref(),
&tx,
)
},
}
}
})
.await?;
Ok(())
}
}
@@ -0,0 +1,94 @@
// 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 <https://www.gnu.org/licenses/>.
//! Error helpers for `chainHead` RPC module.
use jsonrpsee::types::error::ErrorObject;
/// ChainHead RPC errors.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// Maximum number of chainHead_follow has been reached.
#[error("Maximum number of chainHead_follow has been reached")]
ReachedLimits,
/// The provided block hash is invalid.
#[error("Invalid block hash")]
InvalidBlock,
/// The follow subscription was started with `withRuntime` set to `false`.
#[error("The `chainHead_follow` subscription was started with `withRuntime` set to `false`")]
InvalidRuntimeCall(String),
/// Wait-for-continue event not generated.
#[error("Wait for continue event was not generated for the subscription")]
InvalidContinue,
/// Received duplicate hashes for the `chainHead_unpin` method.
#[error("Received duplicate hashes for the `chainHead_unpin` method")]
InvalidDuplicateHashes,
/// Invalid parameter provided to the RPC method.
#[error("Invalid parameter: {0}")]
InvalidParam(String),
/// Internal error.
#[error("Internal error: {0}")]
InternalError(String),
}
/// Errors for `chainHead` RPC module, as defined in
/// <https://github.com/paritytech/json-rpc-interface-spec>.
pub mod rpc_spec_v2 {
/// Maximum number of chainHead_follow has been reached.
pub const REACHED_LIMITS: i32 = -32800;
/// The provided block hash is invalid.
pub const INVALID_BLOCK_ERROR: i32 = -32801;
/// The follow subscription was started with `withRuntime` set to `false`.
pub const INVALID_RUNTIME_CALL: i32 = -32802;
/// Wait-for-continue event not generated.
pub const INVALID_CONTINUE: i32 = -32803;
/// Received duplicate hashes for the `chainHead_unpin` method.
pub const INVALID_DUPLICATE_HASHES: i32 = -32804;
}
/// General purpose errors, as defined in
/// <https://www.jsonrpc.org/specification#error_object>.
pub mod json_rpc_spec {
/// Invalid parameter error.
pub const INVALID_PARAM_ERROR: i32 = -32602;
/// Internal error.
pub const INTERNAL_ERROR: i32 = -32603;
}
impl From<Error> for ErrorObject<'static> {
fn from(e: Error) -> Self {
let msg = e.to_string();
match e {
Error::ReachedLimits =>
ErrorObject::owned(rpc_spec_v2::REACHED_LIMITS, msg, None::<()>),
Error::InvalidBlock =>
ErrorObject::owned(rpc_spec_v2::INVALID_BLOCK_ERROR, msg, None::<()>),
Error::InvalidRuntimeCall(_) =>
ErrorObject::owned(rpc_spec_v2::INVALID_RUNTIME_CALL, msg, None::<()>),
Error::InvalidContinue =>
ErrorObject::owned(rpc_spec_v2::INVALID_CONTINUE, msg, None::<()>),
Error::InvalidDuplicateHashes =>
ErrorObject::owned(rpc_spec_v2::INVALID_DUPLICATE_HASHES, msg, None::<()>),
Error::InvalidParam(_) =>
ErrorObject::owned(json_rpc_spec::INVALID_PARAM_ERROR, msg, None::<()>),
Error::InternalError(_) =>
ErrorObject::owned(json_rpc_spec::INTERNAL_ERROR, msg, None::<()>),
}
}
}
@@ -0,0 +1,655 @@
// 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 <https://www.gnu.org/licenses/>.
//! The chain head's event returned as json compatible object.
use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer};
use pezsp_api::ApiError;
use pezsp_version::RuntimeVersion;
use std::collections::BTreeMap;
use crate::common::events::StorageResult;
/// The operation could not be processed due to an error.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ErrorEvent {
/// Reason of the error.
pub error: String,
}
/// The runtime specification of the current block.
///
/// This event is generated for:
/// - the first announced block by the follow subscription
/// - blocks that suffered a change in runtime compared with their parents
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeVersionEvent {
/// The runtime version.
pub spec: ChainHeadRuntimeVersion,
}
/// Simplified type clone of `pezsp_version::RuntimeVersion`. Used instead of
/// `pezsp_version::RuntimeVersion` to conform to RPC spec V2.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChainHeadRuntimeVersion {
/// Identifies the different Bizinikiwi runtimes.
pub spec_name: String,
/// Name of the implementation of the spec.
pub impl_name: String,
/// Version of the runtime specification.
pub spec_version: u32,
/// Version of the implementation of the specification.
pub impl_version: u32,
/// Map of all supported API "features" and their versions.
pub apis: BTreeMap<String, u32>,
/// Transaction version.
pub transaction_version: u32,
}
impl From<RuntimeVersion> for ChainHeadRuntimeVersion {
fn from(val: RuntimeVersion) -> Self {
Self {
spec_name: val.spec_name.into(),
impl_name: val.impl_name.into(),
spec_version: val.spec_version,
impl_version: val.impl_version,
apis: val
.apis
.into_iter()
.map(|(api, version)| (pezsp_core::bytes::to_hex(api, false), *version))
.collect(),
transaction_version: val.transaction_version,
}
}
}
/// The runtime event generated if the `follow` subscription
/// has set the `with_runtime` flag.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum RuntimeEvent {
/// The runtime version of this block.
Valid(RuntimeVersionEvent),
/// The runtime could not be obtained due to an error.
Invalid(ErrorEvent),
}
impl From<ApiError> for RuntimeEvent {
fn from(err: ApiError) -> Self {
RuntimeEvent::Invalid(ErrorEvent { error: format!("Api error: {}", err) })
}
}
/// Contain information about the latest finalized block.
///
/// # Note
///
/// This is the first event generated by the `follow` subscription
/// and is submitted only once.
///
/// If the `with_runtime` flag is set, then this event contains
/// the `RuntimeEvent`, otherwise the `RuntimeEvent` is not present.
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Initialized<Hash> {
/// The hash of the latest finalized blocks.
pub finalized_block_hashes: Vec<Hash>,
/// The runtime version of the finalized block.
///
/// # Note
///
/// This is present only if the `with_runtime` flag is set for
/// the `follow` subscription.
pub finalized_block_runtime: Option<RuntimeEvent>,
/// Privately keep track if the `finalized_block_runtime` should be
/// serialized.
#[serde(default)]
pub(crate) with_runtime: bool,
}
impl<Hash: Serialize> Serialize for Initialized<Hash> {
/// Custom serialize implementation to include the `RuntimeEvent` depending
/// on the internal `with_runtime` flag.
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if self.with_runtime {
let mut state = serializer.serialize_struct("Initialized", 2)?;
state.serialize_field("finalizedBlockHashes", &self.finalized_block_hashes)?;
state.serialize_field("finalizedBlockRuntime", &self.finalized_block_runtime)?;
state.end()
} else {
let mut state = serializer.serialize_struct("Initialized", 1)?;
state.serialize_field("finalizedBlockHashes", &self.finalized_block_hashes)?;
state.end()
}
}
}
/// Indicate a new non-finalized block.
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NewBlock<Hash> {
/// The hash of the new block.
pub block_hash: Hash,
/// The parent hash of the new block.
pub parent_block_hash: Hash,
/// The runtime version of the new block.
///
/// # Note
///
/// This is present only if the `with_runtime` flag is set for
/// the `follow` subscription.
pub new_runtime: Option<RuntimeEvent>,
/// Privately keep track if the `finalized_block_runtime` should be
/// serialized.
#[serde(default)]
pub(crate) with_runtime: bool,
}
impl<Hash: Serialize> Serialize for NewBlock<Hash> {
/// Custom serialize implementation to include the `RuntimeEvent` depending
/// on the internal `with_runtime` flag.
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if self.with_runtime {
let mut state = serializer.serialize_struct("NewBlock", 3)?;
state.serialize_field("blockHash", &self.block_hash)?;
state.serialize_field("parentBlockHash", &self.parent_block_hash)?;
state.serialize_field("newRuntime", &self.new_runtime)?;
state.end()
} else {
let mut state = serializer.serialize_struct("NewBlock", 2)?;
state.serialize_field("blockHash", &self.block_hash)?;
state.serialize_field("parentBlockHash", &self.parent_block_hash)?;
state.end()
}
}
}
/// Indicate the block hash of the new best block.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BestBlockChanged<Hash> {
/// The block hash of the new best block.
pub best_block_hash: Hash,
}
/// Indicate the finalized and pruned block hashes.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Finalized<Hash> {
/// Block hashes that are finalized.
pub finalized_block_hashes: Vec<Hash>,
/// Block hashes that are pruned (removed).
pub pruned_block_hashes: Vec<Hash>,
}
/// Indicate the operation id of the event.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OperationId {
/// The operation id of the event.
pub operation_id: String,
}
/// The response of the `chainHead_body` method.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OperationBodyDone {
/// The operation id of the event.
pub operation_id: String,
/// Array of hexadecimal-encoded scale-encoded extrinsics found in the block.
pub value: Vec<String>,
}
/// The response of the `chainHead_call` method.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OperationCallDone {
/// The operation id of the event.
pub operation_id: String,
/// Hexadecimal-encoded output of the runtime function call.
pub output: String,
}
/// The response of the `chainHead_storage` method.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OperationStorageItems {
/// The operation id of the event.
pub operation_id: String,
/// The resulting items.
pub items: Vec<StorageResult>,
}
/// Indicate a problem during the operation.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OperationError {
/// The operation id of the event.
pub operation_id: String,
/// The reason of the error.
pub error: String,
}
/// The event generated by the `follow` method.
///
/// The block events are generated in the following order:
/// 1. Initialized - generated only once to signal the latest finalized block
/// 2. NewBlock - a new block was added.
/// 3. BestBlockChanged - indicate that the best block is now the one from this event. The block was
/// announced priorly with the `NewBlock` event.
/// 4. Finalized - State the finalized and pruned blocks.
///
/// The following events are related to operations:
/// - OperationBodyDone: The response of the `chianHead_body`
/// - OperationCallDone: The response of the `chianHead_call`
/// - OperationStorageItems: Items produced by the `chianHead_storage`
/// - OperationWaitingForContinue: Generated after OperationStorageItems and requires the user to
/// call `chainHead_continue`
/// - OperationStorageDone: The `chianHead_storage` method has produced all the results
/// - OperationInaccessible: The server was unable to provide the result, retries might succeed in
/// the future
/// - OperationError: The server encountered an error, retries will not succeed
///
/// The stop event indicates that the JSON-RPC server was unable to provide a consistent list of
/// the blocks at the head of the chain.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "event")]
pub enum FollowEvent<Hash> {
/// The latest finalized block.
///
/// This event is generated only once.
Initialized(Initialized<Hash>),
/// A new non-finalized block was added.
NewBlock(NewBlock<Hash>),
/// The best block of the chain.
BestBlockChanged(BestBlockChanged<Hash>),
/// A list of finalized and pruned blocks.
Finalized(Finalized<Hash>),
/// The response of the `chainHead_body` method.
OperationBodyDone(OperationBodyDone),
/// The response of the `chainHead_call` method.
OperationCallDone(OperationCallDone),
/// Yield one or more items found in the storage.
OperationStorageItems(OperationStorageItems),
/// Ask the user to call `chainHead_continue` to produce more events
/// regarding the operation id.
OperationWaitingForContinue(OperationId),
/// The responses of the `chainHead_storage` method have been produced.
OperationStorageDone(OperationId),
/// The RPC server was unable to provide the response of the following operation id.
///
/// Repeating the same operation in the future might succeed.
OperationInaccessible(OperationId),
/// The RPC server encountered an error while processing an operation id.
///
/// Repeating the same operation in the future will not succeed.
OperationError(OperationError),
/// The subscription is dropped and no further events
/// will be generated.
Stop,
}
/// The method response of `chainHead_body`, `chainHead_call` and `chainHead_storage`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "result")]
pub enum MethodResponse {
/// The method has started.
Started(MethodResponseStarted),
/// The RPC server cannot handle the request at the moment.
LimitReached,
}
/// The `started` result of a method.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MethodResponseStarted {
/// The operation id of the response.
pub operation_id: String,
/// The number of items from the back of the `chainHead_storage` that have been discarded.
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub discarded_items: Option<usize>,
}
#[cfg(test)]
mod tests {
use crate::common::events::StorageResultType;
use super::*;
#[test]
fn follow_initialized_event_no_updates() {
// Runtime flag is false.
let event: FollowEvent<String> = FollowEvent::Initialized(Initialized {
finalized_block_hashes: vec!["0x1".into()],
finalized_block_runtime: None,
with_runtime: false,
});
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"initialized","finalizedBlockHashes":["0x1"]}"#;
assert_eq!(ser, exp);
let event_dec: FollowEvent<String> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
#[test]
fn follow_initialized_event_with_updates() {
// Runtime flag is true, block runtime must always be reported for this event.
let runtime = RuntimeVersion {
spec_name: "ABC".into(),
impl_name: "Impl".into(),
spec_version: 1,
..Default::default()
};
let runtime_event = RuntimeEvent::Valid(RuntimeVersionEvent { spec: runtime.into() });
let mut initialized = Initialized {
finalized_block_hashes: vec!["0x1".into()],
finalized_block_runtime: Some(runtime_event),
with_runtime: true,
};
let event: FollowEvent<String> = FollowEvent::Initialized(initialized.clone());
let ser = serde_json::to_string(&event).unwrap();
let exp = concat!(
r#"{"event":"initialized","finalizedBlockHashes":["0x1"],"#,
r#""finalizedBlockRuntime":{"type":"valid","spec":{"specName":"ABC","implName":"Impl","#,
r#""specVersion":1,"implVersion":0,"apis":{},"transactionVersion":0}}}"#,
);
assert_eq!(ser, exp);
let event_dec: FollowEvent<String> = serde_json::from_str(exp).unwrap();
// The `with_runtime` field is used for serialization purposes.
initialized.with_runtime = false;
assert!(matches!(
event_dec, FollowEvent::Initialized(ref dec) if dec == &initialized
));
}
#[test]
fn follow_new_block_event_no_updates() {
// Runtime flag is false.
let event: FollowEvent<String> = FollowEvent::NewBlock(NewBlock {
block_hash: "0x1".into(),
parent_block_hash: "0x2".into(),
new_runtime: None,
with_runtime: false,
});
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"newBlock","blockHash":"0x1","parentBlockHash":"0x2"}"#;
assert_eq!(ser, exp);
let event_dec: FollowEvent<String> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
#[test]
fn follow_new_block_event_with_updates() {
// Runtime flag is true, block runtime must always be reported for this event.
let runtime = RuntimeVersion {
spec_name: "ABC".into(),
impl_name: "Impl".into(),
spec_version: 1,
apis: vec![([0, 0, 0, 0, 0, 0, 0, 0], 2), ([1, 0, 0, 0, 0, 0, 0, 0], 3)].into(),
..Default::default()
};
let runtime_event = RuntimeEvent::Valid(RuntimeVersionEvent { spec: runtime.into() });
let mut new_block = NewBlock {
block_hash: "0x1".into(),
parent_block_hash: "0x2".into(),
new_runtime: Some(runtime_event),
with_runtime: true,
};
let event: FollowEvent<String> = FollowEvent::NewBlock(new_block.clone());
let ser = serde_json::to_string(&event).unwrap();
let exp = concat!(
r#"{"event":"newBlock","blockHash":"0x1","parentBlockHash":"0x2","#,
r#""newRuntime":{"type":"valid","spec":{"specName":"ABC","implName":"Impl","#,
r#""specVersion":1,"implVersion":0,"apis":{"0x0000000000000000":2,"0x0100000000000000":3},"transactionVersion":0}}}"#,
);
assert_eq!(ser, exp);
let event_dec: FollowEvent<String> = serde_json::from_str(exp).unwrap();
// The `with_runtime` field is used for serialization purposes.
new_block.with_runtime = false;
assert!(matches!(
event_dec, FollowEvent::NewBlock(ref dec) if dec == &new_block
));
// Runtime flag is true, runtime didn't change compared to parent.
let mut new_block = NewBlock {
block_hash: "0x1".into(),
parent_block_hash: "0x2".into(),
new_runtime: None,
with_runtime: true,
};
let event: FollowEvent<String> = FollowEvent::NewBlock(new_block.clone());
let ser = serde_json::to_string(&event).unwrap();
let exp =
r#"{"event":"newBlock","blockHash":"0x1","parentBlockHash":"0x2","newRuntime":null}"#;
assert_eq!(ser, exp);
new_block.with_runtime = false;
let event_dec: FollowEvent<String> = serde_json::from_str(exp).unwrap();
assert!(matches!(
event_dec, FollowEvent::NewBlock(ref dec) if dec == &new_block
));
}
#[test]
fn follow_best_block_changed_event() {
let event: FollowEvent<String> =
FollowEvent::BestBlockChanged(BestBlockChanged { best_block_hash: "0x1".into() });
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"bestBlockChanged","bestBlockHash":"0x1"}"#;
assert_eq!(ser, exp);
let event_dec: FollowEvent<String> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
#[test]
fn follow_finalized_event() {
let event: FollowEvent<String> = FollowEvent::Finalized(Finalized {
finalized_block_hashes: vec!["0x1".into()],
pruned_block_hashes: vec!["0x2".into()],
});
let ser = serde_json::to_string(&event).unwrap();
let exp =
r#"{"event":"finalized","finalizedBlockHashes":["0x1"],"prunedBlockHashes":["0x2"]}"#;
assert_eq!(ser, exp);
let event_dec: FollowEvent<String> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
#[test]
fn follow_op_body_event() {
let event: FollowEvent<String> = FollowEvent::OperationBodyDone(OperationBodyDone {
operation_id: "123".into(),
value: vec!["0x1".into()],
});
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"operationBodyDone","operationId":"123","value":["0x1"]}"#;
assert_eq!(ser, exp);
let event_dec: FollowEvent<String> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
#[test]
fn follow_op_call_event() {
let event: FollowEvent<String> = FollowEvent::OperationCallDone(OperationCallDone {
operation_id: "123".into(),
output: "0x1".into(),
});
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"operationCallDone","operationId":"123","output":"0x1"}"#;
assert_eq!(ser, exp);
let event_dec: FollowEvent<String> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
#[test]
fn follow_op_storage_items_event() {
let event: FollowEvent<String> =
FollowEvent::OperationStorageItems(OperationStorageItems {
operation_id: "123".into(),
items: vec![StorageResult {
key: "0x1".into(),
result: StorageResultType::Value("0x123".to_string()),
child_trie_key: None,
}],
});
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"operationStorageItems","operationId":"123","items":[{"key":"0x1","value":"0x123"}]}"#;
assert_eq!(ser, exp);
let event_dec: FollowEvent<String> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
#[test]
fn follow_op_wait_event() {
let event: FollowEvent<String> =
FollowEvent::OperationWaitingForContinue(OperationId { operation_id: "123".into() });
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"operationWaitingForContinue","operationId":"123"}"#;
assert_eq!(ser, exp);
let event_dec: FollowEvent<String> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
#[test]
fn follow_op_storage_done_event() {
let event: FollowEvent<String> =
FollowEvent::OperationStorageDone(OperationId { operation_id: "123".into() });
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"operationStorageDone","operationId":"123"}"#;
assert_eq!(ser, exp);
let event_dec: FollowEvent<String> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
#[test]
fn follow_op_inaccessible_event() {
let event: FollowEvent<String> =
FollowEvent::OperationInaccessible(OperationId { operation_id: "123".into() });
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"operationInaccessible","operationId":"123"}"#;
assert_eq!(ser, exp);
let event_dec: FollowEvent<String> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
#[test]
fn follow_op_error_event() {
let event: FollowEvent<String> = FollowEvent::OperationError(OperationError {
operation_id: "123".into(),
error: "reason".into(),
});
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"operationError","operationId":"123","error":"reason"}"#;
assert_eq!(ser, exp);
let event_dec: FollowEvent<String> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
#[test]
fn follow_stop_event() {
let event: FollowEvent<String> = FollowEvent::Stop;
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"stop"}"#;
assert_eq!(ser, exp);
let event_dec: FollowEvent<String> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
#[test]
fn method_response() {
// Response of `call` and `body`
let event = MethodResponse::Started(MethodResponseStarted {
operation_id: "123".into(),
discarded_items: None,
});
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"result":"started","operationId":"123"}"#;
assert_eq!(ser, exp);
let event_dec: MethodResponse = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
// Response of `storage`
let event = MethodResponse::Started(MethodResponseStarted {
operation_id: "123".into(),
discarded_items: Some(1),
});
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"result":"started","operationId":"123","discardedItems":1}"#;
assert_eq!(ser, exp);
let event_dec: MethodResponse = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
// Limit reached.
let event = MethodResponse::LimitReached;
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"result":"limitReached"}"#;
assert_eq!(ser, exp);
let event_dec: MethodResponse = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
}
@@ -0,0 +1,51 @@
// 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 <https://www.gnu.org/licenses/>.
//! Bizinikiwi chain head API.
//!
//! # Note
//!
//! Methods are prefixed by `chainHead`.
#[cfg(test)]
pub mod test_utils;
#[cfg(test)]
mod tests;
pub mod api;
pub mod chain_head;
pub mod error;
pub mod event;
mod chain_head_follow;
mod chain_head_storage;
mod subscription;
pub use api::ChainHeadApiServer;
pub use chain_head::{ChainHead, ChainHeadConfig};
pub use event::{
BestBlockChanged, ErrorEvent, Finalized, FollowEvent, Initialized, NewBlock, RuntimeEvent,
RuntimeVersionEvent,
};
/// Follow event sender.
pub(crate) type FollowEventSender<Hash> = futures::channel::mpsc::Sender<FollowEvent<Hash>>;
/// Follow event receiver.
pub(crate) type FollowEventReceiver<Hash> = futures::channel::mpsc::Receiver<FollowEvent<Hash>>;
/// Follow event send error.
pub(crate) type FollowEventSendError = futures::channel::mpsc::SendError;
@@ -0,0 +1,74 @@
// 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 <https://www.gnu.org/licenses/>.
use pezsp_blockchain::Error;
/// Subscription management error.
#[derive(Debug, thiserror::Error)]
pub enum SubscriptionManagementError {
/// The subscription has exceeded the internal limits
/// regarding the number of pinned blocks in memory or
/// the number of ongoing operations.
#[error("Exceeded pinning or operation limits")]
ExceededLimits,
/// Error originated from the blockchain (client or backend).
#[error("Blockchain error {0}")]
Blockchain(Error),
/// The database does not contain a block hash.
#[error("Block hash is absent")]
BlockHashAbsent,
/// The database does not contain a block header.
#[error("Block header is absent")]
BlockHeaderAbsent,
/// The specified subscription ID is not present.
#[error("Subscription is absent")]
SubscriptionAbsent,
/// The unpin method was called with duplicate hashes.
#[error("Duplicate hashes")]
DuplicateHashes,
/// The distance between the leaves and the current finalized block is too large.
#[error("Distance too large")]
BlockDistanceTooLarge,
/// Custom error.
#[error("Subscription error {0}")]
Custom(String),
}
// Blockchain error does not implement `PartialEq` needed for testing.
impl PartialEq for SubscriptionManagementError {
fn eq(&self, other: &SubscriptionManagementError) -> bool {
match (self, other) {
(Self::ExceededLimits, Self::ExceededLimits) |
// Not needed for testing.
(Self::Blockchain(_), Self::Blockchain(_)) |
(Self::BlockHashAbsent, Self::BlockHashAbsent) |
(Self::BlockHeaderAbsent, Self::BlockHeaderAbsent) |
(Self::SubscriptionAbsent, Self::SubscriptionAbsent) |
(Self::DuplicateHashes, Self::DuplicateHashes) => true,
(Self::BlockDistanceTooLarge, Self::BlockDistanceTooLarge) => true,
(Self::Custom(lhs), Self::Custom(rhs)) => lhs == rhs,
_ => false,
}
}
}
impl From<Error> for SubscriptionManagementError {
fn from(err: Error) -> Self {
SubscriptionManagementError::Blockchain(err)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,253 @@
// 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 <https://www.gnu.org/licenses/>.
use jsonrpsee::ConnectionId;
use parking_lot::RwLock;
use pezsc_client_api::Backend;
use pezsp_runtime::traits::Block as BlockT;
use std::{sync::Arc, time::Duration};
mod error;
mod inner;
use crate::{
chain_head::chain_head::LOG_TARGET,
common::connections::{RegisteredConnection, ReservedConnection, RpcConnections},
};
use self::inner::SubscriptionsInner;
pub use self::inner::OperationState;
pub use error::SubscriptionManagementError;
pub use inner::{BlockGuard, InsertedSubscriptionData, StopHandle};
/// Manage block pinning / unpinning for subscription IDs.
pub struct SubscriptionManagement<Block: BlockT, BE: Backend<Block>> {
/// Manage subscription by mapping the subscription ID
/// to a set of block hashes.
inner: Arc<RwLock<SubscriptionsInner<Block, BE>>>,
/// Ensures that chainHead methods can be called from a single connection context.
///
/// For example, `chainHead_storage` cannot be called with a subscription ID that
/// was obtained from a different connection.
rpc_connections: RpcConnections,
}
impl<Block: BlockT, BE: Backend<Block>> Clone for SubscriptionManagement<Block, BE> {
fn clone(&self) -> Self {
SubscriptionManagement {
inner: self.inner.clone(),
rpc_connections: self.rpc_connections.clone(),
}
}
}
impl<Block: BlockT, BE: Backend<Block>> SubscriptionManagement<Block, BE> {
/// Construct a new [`SubscriptionManagement`].
pub fn new(
global_max_pinned_blocks: usize,
local_max_pin_duration: Duration,
max_ongoing_operations: usize,
max_follow_subscriptions_per_connection: usize,
backend: Arc<BE>,
) -> Self {
SubscriptionManagement {
inner: Arc::new(RwLock::new(SubscriptionsInner::new(
global_max_pinned_blocks,
local_max_pin_duration,
max_ongoing_operations,
backend,
))),
rpc_connections: RpcConnections::new(max_follow_subscriptions_per_connection),
}
}
/// Create a new instance from the inner state.
///
/// # Note
///
/// Used for testing.
#[cfg(test)]
pub(crate) fn _from_inner(
inner: Arc<RwLock<SubscriptionsInner<Block, BE>>>,
rpc_connections: RpcConnections,
) -> Self {
SubscriptionManagement { inner, rpc_connections }
}
/// Reserve space for a subscriptions.
///
/// Fails if the connection ID is has reached the maximum number of active subscriptions.
pub fn reserve_subscription(
&self,
connection_id: ConnectionId,
) -> Option<ReservedSubscription<Block, BE>> {
let reserved_token = self.rpc_connections.reserve_space(connection_id)?;
Some(ReservedSubscription {
state: ConnectionState::Reserved(reserved_token),
inner: self.inner.clone(),
})
}
/// Check if the given connection contains the given subscription.
pub fn contains_subscription(
&self,
connection_id: ConnectionId,
subscription_id: &str,
) -> bool {
self.rpc_connections.contains_identifier(connection_id, subscription_id)
}
/// Remove the subscription ID with associated pinned blocks.
pub fn remove_subscription(&self, sub_id: &str) {
let mut inner = self.inner.write();
inner.remove_subscription(sub_id)
}
/// The block is pinned in the backend only once when the block's hash is first encountered.
///
/// Each subscription is expected to call this method twice:
/// - once from the `NewBlock` import
/// - once from the `Finalized` import
///
/// Returns
/// - Ok(true) if the subscription did not previously contain this block
/// - Ok(false) if the subscription already contained this this
/// - Error if the backend failed to pin the block or the subscription ID is invalid
pub fn pin_block(
&self,
sub_id: &str,
hash: Block::Hash,
) -> Result<bool, SubscriptionManagementError> {
let mut inner = self.inner.write();
inner.pin_block(sub_id, hash)
}
/// Unpin the blocks from the subscription.
///
/// Blocks are reference counted and when the last subscription unpins a given block, the block
/// is also unpinned from the backend.
///
/// This method is called only once per subscription.
///
/// Returns an error if the subscription ID is invalid, or any of the blocks are not pinned
/// for the subscriptions. When an error is returned, it is guaranteed that no blocks have
/// been unpinned.
pub fn unpin_blocks(
&self,
sub_id: &str,
hashes: impl IntoIterator<Item = Block::Hash> + Clone,
) -> Result<(), SubscriptionManagementError> {
let mut inner = self.inner.write();
inner.unpin_blocks(sub_id, hashes)
}
/// Ensure the block remains pinned until the return object is dropped.
///
/// Returns a [`BlockGuard`] that pins and unpins the block hash in RAII manner
/// and reserves capacity for ogoing operations.
///
/// Returns an error if the block hash is not pinned for the subscription,
/// the subscription ID is invalid or the limit of ongoing operations was exceeded.
pub fn lock_block(
&self,
sub_id: &str,
hash: Block::Hash,
to_reserve: usize,
) -> Result<BlockGuard<Block, BE>, SubscriptionManagementError> {
let mut inner = self.inner.write();
inner.lock_block(sub_id, hash, to_reserve)
}
/// Get the operation state.
pub fn get_operation(&self, sub_id: &str, operation_id: &str) -> Option<OperationState> {
let mut inner = self.inner.write();
inner.get_operation(sub_id, operation_id)
}
}
/// The state of the connection.
///
/// The state starts in a [`ConnectionState::Reserved`] state and then transitions to
/// [`ConnectionState::Registered`] when the subscription is inserted.
enum ConnectionState {
Reserved(ReservedConnection),
Registered { _unregister_on_drop: RegisteredConnection, sub_id: String },
Empty,
}
/// RAII wrapper that removes the subscription from internal mappings and
/// gives back the reserved space for the connection.
pub struct ReservedSubscription<Block: BlockT, BE: Backend<Block>> {
state: ConnectionState,
inner: Arc<RwLock<SubscriptionsInner<Block, BE>>>,
}
impl<Block: BlockT, BE: Backend<Block>> ReservedSubscription<Block, BE> {
/// Insert a new subscription ID.
///
/// If the subscription was not previously inserted, returns the receiver that is
/// triggered upon the "Stop" event. Otherwise, if the subscription ID was already
/// inserted returns none.
///
/// # Note
///
/// This method should be called only once.
pub fn insert_subscription(
&mut self,
sub_id: String,
runtime_updates: bool,
) -> Option<InsertedSubscriptionData<Block>> {
match std::mem::replace(&mut self.state, ConnectionState::Empty) {
ConnectionState::Reserved(reserved) => {
let registered_token = reserved.register(sub_id.clone())?;
self.state = ConnectionState::Registered {
_unregister_on_drop: registered_token,
sub_id: sub_id.clone(),
};
let mut inner = self.inner.write();
inner.insert_subscription(sub_id, runtime_updates)
},
// Cannot insert multiple subscriptions into one single reserved space.
ConnectionState::Registered { .. } | ConnectionState::Empty => {
log::error!(target: LOG_TARGET, "Called insert_subscription on a connection that is not reserved");
None
},
}
}
/// Stop all active subscriptions.
///
/// For all active subscriptions, the internal data is discarded, blocks are unpinned and the
/// `Stop` event will be generated.
pub fn stop_all_subscriptions(&self) {
let mut inner = self.inner.write();
inner.stop_all_subscriptions()
}
}
impl<Block: BlockT, BE: Backend<Block>> Drop for ReservedSubscription<Block, BE> {
fn drop(&mut self) {
if let ConnectionState::Registered { sub_id, .. } = &self.state {
self.inner.write().remove_subscription(sub_id);
}
}
}
@@ -0,0 +1,370 @@
// 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 <https://www.gnu.org/licenses/>.
//! Test utilities.
use parking_lot::Mutex;
use pezsc_client_api::{
execution_extensions::ExecutionExtensions, BlockBackend, BlockImportNotification,
BlockchainEvents, CallExecutor, ChildInfo, ExecutorProvider, FinalityNotification,
FinalityNotifications, FinalizeSummary, ImportNotifications, KeysIter, MerkleValue, PairsIter,
StaleBlock, StorageData, StorageEventStream, StorageKey, StorageProvider,
};
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedSender};
use pezsp_api::{CallApiAt, CallApiAtParams};
use pezsp_blockchain::{BlockStatus, CachedHeaderMetadata, HeaderBackend, HeaderMetadata, Info};
use pezsp_consensus::BlockOrigin;
use pezsp_runtime::{
generic::SignedBlock,
traits::{Block as BlockT, Header as HeaderT, NumberFor},
Justifications,
};
use pezsp_version::RuntimeVersion;
use std::sync::Arc;
use bizinikiwi_test_runtime::{Block, Hash, Header, H256};
/// A mock client used for testing.
pub struct ChainHeadMockClient<Client> {
client: Arc<Client>,
import_sinks: Mutex<Vec<TracingUnboundedSender<BlockImportNotification<Block>>>>,
finality_sinks: Mutex<Vec<TracingUnboundedSender<FinalityNotification<Block>>>>,
best_block: Mutex<Option<(H256, u64)>>,
}
impl<Client> ChainHeadMockClient<Client> {
/// Create a new mock client.
pub fn new(client: Arc<Client>) -> Self {
ChainHeadMockClient {
client,
import_sinks: Default::default(),
finality_sinks: Default::default(),
best_block: Default::default(),
}
}
/// Trigger the import stram from a header.
pub async fn trigger_import_stream(&self, header: Header) {
// Ensure the client called the `import_notification_stream`.
while self.import_sinks.lock().is_empty() {
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
// Build the notification.
let (sink, _stream) = tracing_unbounded("test_sink", 100_000);
let notification =
BlockImportNotification::new(header.hash(), BlockOrigin::Own, header, true, None, sink);
for sink in self.import_sinks.lock().iter_mut() {
let _ = sink.unbounded_send(notification.clone());
}
}
/// Trigger the import stram from a header and a list of stale heads.
pub async fn trigger_finality_stream(&self, header: Header, stale_blocks: Vec<Hash>) {
// Ensure the client called the `finality_notification_stream`.
while self.finality_sinks.lock().is_empty() {
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
// Build the notification.
let (sink, _stream) = tracing_unbounded("test_sink", 100_000);
let summary = FinalizeSummary {
header: header.clone(),
finalized: vec![header.hash()],
stale_blocks: stale_blocks
.into_iter()
.map(|h| StaleBlock { hash: h, is_head: false })
.collect(),
};
let notification = FinalityNotification::from_summary(summary, sink);
for sink in self.finality_sinks.lock().iter_mut() {
let _ = sink.unbounded_send(notification.clone());
}
}
/// Set the best block hash and number that is reported by the `info` method.
pub fn set_best_block(&self, hash: H256, number: u64) {
*self.best_block.lock() = Some((hash, number));
}
}
// ChainHead calls `import_notification_stream` and `finality_notification_stream` in order to
// subscribe to block events.
impl<Client> BlockchainEvents<Block> for ChainHeadMockClient<Client> {
fn import_notification_stream(&self) -> ImportNotifications<Block> {
let (sink, stream) = tracing_unbounded("import_notification_stream", 1024);
self.import_sinks.lock().push(sink);
stream
}
fn every_import_notification_stream(&self) -> ImportNotifications<Block> {
unimplemented!()
}
fn finality_notification_stream(&self) -> FinalityNotifications<Block> {
let (sink, stream) = tracing_unbounded("finality_notification_stream", 1024);
self.finality_sinks.lock().push(sink);
stream
}
fn storage_changes_notification_stream(
&self,
_filter_keys: Option<&[StorageKey]>,
_child_filter_keys: Option<&[(StorageKey, Option<Vec<StorageKey>>)]>,
) -> pezsp_blockchain::Result<StorageEventStream<Hash>> {
unimplemented!()
}
}
// The following implementations are imposed by the `chainHead` trait bounds.
impl<Block: BlockT, E: CallExecutor<Block>, Client: ExecutorProvider<Block, Executor = E>>
ExecutorProvider<Block> for ChainHeadMockClient<Client>
{
type Executor = <Client as ExecutorProvider<Block>>::Executor;
fn executor(&self) -> &Self::Executor {
self.client.executor()
}
fn execution_extensions(&self) -> &ExecutionExtensions<Block> {
self.client.execution_extensions()
}
}
impl<
BE: pezsc_client_api::backend::Backend<Block> + Send + Sync + 'static,
Block: BlockT,
Client: StorageProvider<Block, BE>,
> StorageProvider<Block, BE> for ChainHeadMockClient<Client>
{
fn storage(
&self,
hash: Block::Hash,
key: &StorageKey,
) -> pezsp_blockchain::Result<Option<StorageData>> {
self.client.storage(hash, key)
}
fn storage_hash(
&self,
hash: Block::Hash,
key: &StorageKey,
) -> pezsp_blockchain::Result<Option<Block::Hash>> {
self.client.storage_hash(hash, key)
}
fn storage_keys(
&self,
hash: Block::Hash,
prefix: Option<&StorageKey>,
start_key: Option<&StorageKey>,
) -> pezsp_blockchain::Result<KeysIter<BE::State, Block>> {
self.client.storage_keys(hash, prefix, start_key)
}
fn storage_pairs(
&self,
hash: <Block as BlockT>::Hash,
prefix: Option<&StorageKey>,
start_key: Option<&StorageKey>,
) -> pezsp_blockchain::Result<PairsIter<BE::State, Block>> {
self.client.storage_pairs(hash, prefix, start_key)
}
fn child_storage(
&self,
hash: Block::Hash,
child_info: &ChildInfo,
key: &StorageKey,
) -> pezsp_blockchain::Result<Option<StorageData>> {
self.client.child_storage(hash, child_info, key)
}
fn child_storage_keys(
&self,
hash: Block::Hash,
child_info: ChildInfo,
prefix: Option<&StorageKey>,
start_key: Option<&StorageKey>,
) -> pezsp_blockchain::Result<KeysIter<BE::State, Block>> {
self.client.child_storage_keys(hash, child_info, prefix, start_key)
}
fn child_storage_hash(
&self,
hash: Block::Hash,
child_info: &ChildInfo,
key: &StorageKey,
) -> pezsp_blockchain::Result<Option<Block::Hash>> {
self.client.child_storage_hash(hash, child_info, key)
}
fn closest_merkle_value(
&self,
hash: Block::Hash,
key: &StorageKey,
) -> pezsp_blockchain::Result<Option<MerkleValue<Block::Hash>>> {
self.client.closest_merkle_value(hash, key)
}
fn child_closest_merkle_value(
&self,
hash: Block::Hash,
child_info: &ChildInfo,
key: &StorageKey,
) -> pezsp_blockchain::Result<Option<MerkleValue<Block::Hash>>> {
self.client.child_closest_merkle_value(hash, child_info, key)
}
}
impl<Block: BlockT, Client: CallApiAt<Block>> CallApiAt<Block> for ChainHeadMockClient<Client> {
type StateBackend = <Client as CallApiAt<Block>>::StateBackend;
fn call_api_at(&self, params: CallApiAtParams<Block>) -> Result<Vec<u8>, pezsp_api::ApiError> {
self.client.call_api_at(params)
}
fn runtime_version_at(&self, hash: Block::Hash) -> Result<RuntimeVersion, pezsp_api::ApiError> {
self.client.runtime_version_at(hash)
}
fn state_at(&self, at: Block::Hash) -> Result<Self::StateBackend, pezsp_api::ApiError> {
self.client.state_at(at)
}
fn initialize_extensions(
&self,
at: <Block as BlockT>::Hash,
extensions: &mut pezsp_externalities::Extensions,
) -> Result<(), pezsp_api::ApiError> {
self.client.initialize_extensions(at, extensions)
}
}
impl<Block: BlockT, Client: BlockBackend<Block>> BlockBackend<Block>
for ChainHeadMockClient<Client>
{
fn block_body(
&self,
hash: Block::Hash,
) -> pezsp_blockchain::Result<Option<Vec<<Block as BlockT>::Extrinsic>>> {
self.client.block_body(hash)
}
fn block(&self, hash: Block::Hash) -> pezsp_blockchain::Result<Option<SignedBlock<Block>>> {
self.client.block(hash)
}
fn block_status(&self, hash: Block::Hash) -> pezsp_blockchain::Result<pezsp_consensus::BlockStatus> {
self.client.block_status(hash)
}
fn justifications(&self, hash: Block::Hash) -> pezsp_blockchain::Result<Option<Justifications>> {
self.client.justifications(hash)
}
fn block_hash(&self, number: NumberFor<Block>) -> pezsp_blockchain::Result<Option<Block::Hash>> {
self.client.block_hash(number)
}
fn indexed_transaction(&self, hash: Block::Hash) -> pezsp_blockchain::Result<Option<Vec<u8>>> {
self.client.indexed_transaction(hash)
}
fn has_indexed_transaction(&self, hash: Block::Hash) -> pezsp_blockchain::Result<bool> {
self.client.has_indexed_transaction(hash)
}
fn block_indexed_body(&self, hash: Block::Hash) -> pezsp_blockchain::Result<Option<Vec<Vec<u8>>>> {
self.client.block_indexed_body(hash)
}
fn requires_full_sync(&self) -> bool {
self.client.requires_full_sync()
}
}
impl<Block: BlockT, Client: HeaderMetadata<Block> + Send + Sync> HeaderMetadata<Block>
for ChainHeadMockClient<Client>
{
type Error = <Client as HeaderMetadata<Block>>::Error;
fn header_metadata(
&self,
hash: Block::Hash,
) -> Result<CachedHeaderMetadata<Block>, Self::Error> {
self.client.header_metadata(hash)
}
fn insert_header_metadata(
&self,
hash: Block::Hash,
header_metadata: CachedHeaderMetadata<Block>,
) {
self.client.insert_header_metadata(hash, header_metadata)
}
fn remove_header_metadata(&self, hash: Block::Hash) {
self.client.remove_header_metadata(hash)
}
}
impl<Block: BlockT<Hash = H256>, Client: HeaderBackend<Block> + Send + Sync> HeaderBackend<Block>
for ChainHeadMockClient<Client>
where
<<Block as pezsp_runtime::traits::Block>::Header as HeaderT>::Number: From<u64>,
{
fn header(
&self,
hash: Block::Hash,
) -> pezsp_blockchain::Result<Option<<Block as BlockT>::Header>> {
self.client.header(hash)
}
fn info(&self) -> Info<Block> {
let mut info = self.client.info();
if let Some((block_hash, block_num)) = self.best_block.lock().take() {
info.best_hash = block_hash;
info.best_number = block_num.into();
}
info
}
fn status(&self, hash: Block::Hash) -> pezsc_client_api::blockchain::Result<BlockStatus> {
self.client.status(hash)
}
fn number(
&self,
hash: Block::Hash,
) -> pezsc_client_api::blockchain::Result<Option<<<Block as BlockT>::Header as HeaderT>::Number>>
{
self.client.number(hash)
}
fn hash(
&self,
number: <<Block as BlockT>::Header as HeaderT>::Number,
) -> pezsp_blockchain::Result<Option<Block::Hash>> {
self.client.hash(number)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,41 @@
// 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 <https://www.gnu.org/licenses/>.
//! API trait of the chain spec.
use jsonrpsee::{core::RpcResult, proc_macros::rpc};
use pezsc_chain_spec::Properties;
#[rpc(client, server)]
pub trait ChainSpecApi {
/// Get the chain name, as present in the chain specification.
#[method(name = "chainSpec_v1_chainName")]
fn chain_spec_v1_chain_name(&self) -> RpcResult<String>;
/// Get the chain's genesis hash.
#[method(name = "chainSpec_v1_genesisHash")]
fn chain_spec_v1_genesis_hash(&self) -> RpcResult<String>;
/// Get the properties of the chain, as present in the chain specification.
///
/// # Note
///
/// The json whitespaces are not guaranteed to persist.
#[method(name = "chainSpec_v1_properties")]
fn chain_spec_v1_properties(&self) -> RpcResult<Properties>;
}
@@ -0,0 +1,60 @@
// 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 <https://www.gnu.org/licenses/>.
//! API implementation for the specification of a chain.
use crate::chain_spec::api::ChainSpecApiServer;
use jsonrpsee::core::RpcResult;
use pezsc_chain_spec::Properties;
/// An API for chain spec RPC calls.
pub struct ChainSpec {
/// The name of the chain.
name: String,
/// The hexadecimal encoded hash of the genesis block.
genesis_hash: String,
/// Chain properties.
properties: Properties,
}
impl ChainSpec {
/// Creates a new [`ChainSpec`].
pub fn new<Hash: AsRef<[u8]>>(
name: String,
genesis_hash: Hash,
properties: Properties,
) -> Self {
let genesis_hash = format!("0x{}", hex::encode(genesis_hash));
Self { name, properties, genesis_hash }
}
}
impl ChainSpecApiServer for ChainSpec {
fn chain_spec_v1_chain_name(&self) -> RpcResult<String> {
Ok(self.name.clone())
}
fn chain_spec_v1_genesis_hash(&self) -> RpcResult<String> {
Ok(self.genesis_hash.clone())
}
fn chain_spec_v1_properties(&self) -> RpcResult<Properties> {
Ok(self.properties.clone())
}
}
@@ -0,0 +1,38 @@
// 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 <https://www.gnu.org/licenses/>.
//! Bizinikiwi chain specification API.
//!
//! The *chain spec* (short for *chain specification*) allows inspecting the content of
//! the specification of the chain that a JSON-RPC server is targeting.
//!
//! The values returned by the API are guaranteed to never change during the lifetime of the
//! JSON-RPC server.
//!
//! # Note
//!
//! Methods are prefixed by `chainSpec`.
#[cfg(test)]
mod tests;
pub mod api;
pub mod chain_spec;
pub use api::ChainSpecApiServer;
pub use chain_spec::ChainSpec;
@@ -0,0 +1,61 @@
// 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 <https://www.gnu.org/licenses/>.
use super::*;
use jsonrpsee::{core::EmptyServerParams as EmptyParams, RpcModule};
use pezsc_chain_spec::Properties;
const CHAIN_NAME: &'static str = "TEST_CHAIN_NAME";
const CHAIN_GENESIS: [u8; 32] = [0; 32];
const CHAIN_PROPERTIES: &'static str = r#"{"three": "123", "one": 1, "two": 12}"#;
fn api() -> RpcModule<ChainSpec> {
ChainSpec::new(
CHAIN_NAME.to_string(),
CHAIN_GENESIS,
serde_json::from_str(CHAIN_PROPERTIES).unwrap(),
)
.into_rpc()
}
#[tokio::test]
async fn chain_spec_chain_name_works() {
let name = api()
.call::<_, String>("chainSpec_v1_chainName", EmptyParams::new())
.await
.unwrap();
assert_eq!(name, CHAIN_NAME);
}
#[tokio::test]
async fn chain_spec_genesis_hash_works() {
let genesis = api()
.call::<_, String>("chainSpec_v1_genesisHash", EmptyParams::new())
.await
.unwrap();
assert_eq!(genesis, format!("0x{}", hex::encode(CHAIN_GENESIS)));
}
#[tokio::test]
async fn chain_spec_properties_works() {
let properties = api()
.call::<_, Properties>("chainSpec_v1_properties", EmptyParams::new())
.await
.unwrap();
assert_eq!(properties, serde_json::from_str(CHAIN_PROPERTIES).unwrap());
}
@@ -0,0 +1,265 @@
// 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 <https://www.gnu.org/licenses/>.
use jsonrpsee::ConnectionId;
use parking_lot::Mutex;
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
/// Connection state which keeps track whether a connection exist and
/// the number of concurrent operations.
#[derive(Default, Clone)]
pub struct RpcConnections {
/// The number of identifiers that can be registered for each connection.
///
/// # Example
///
/// This is used to limit how many `chainHead_follow` subscriptions are active at one time.
capacity: usize,
/// Map the connecton ID to a set of identifiers.
data: Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
}
#[derive(Default)]
struct ConnectionData {
/// The total number of identifiers for the given connection.
///
/// An identifier for a connection might be:
/// - the subscription ID for chainHead_follow
/// - the operation ID for the transactionBroadcast API
/// - or simply how many times the transaction API has been called.
///
/// # Note
///
/// Because a pending subscription sink does not expose the future subscription ID,
/// we cannot register a subscription ID before the pending subscription is accepted.
/// This variable ensures that we have enough capacity to register an identifier, after
/// the subscription is accepted. Otherwise, a jsonrpc error object should be returned.
num_identifiers: usize,
/// Active registered identifiers for the given connection.
///
/// # Note
///
/// For chainHead, this represents the subscription ID.
/// For transactionBroadcast, this represents the operation ID.
/// For transaction, this is empty and the number of active calls is tracked by
/// [`Self::num_identifiers`].
identifiers: HashSet<String>,
}
impl RpcConnections {
/// Constructs a new instance of [`RpcConnections`].
pub fn new(capacity: usize) -> Self {
RpcConnections { capacity, data: Default::default() }
}
/// Reserve space for a new connection identifier.
///
/// If the number of active identifiers for the given connection exceeds the capacity,
/// returns None.
pub fn reserve_space(&self, connection_id: ConnectionId) -> Option<ReservedConnection> {
let mut data = self.data.lock();
let entry = data.entry(connection_id).or_insert_with(ConnectionData::default);
if entry.num_identifiers >= self.capacity {
return None;
}
entry.num_identifiers = entry.num_identifiers.saturating_add(1);
Some(ReservedConnection { connection_id, rpc_connections: Some(self.clone()) })
}
/// Gives back the reserved space before the connection identifier is registered.
///
/// # Note
///
/// This may happen if the pending subscription cannot be accepted (unlikely).
fn unreserve_space(&self, connection_id: ConnectionId) {
let mut data = self.data.lock();
let entry = data.entry(connection_id).or_insert_with(ConnectionData::default);
entry.num_identifiers = entry.num_identifiers.saturating_sub(1);
if entry.num_identifiers == 0 {
data.remove(&connection_id);
}
}
/// Register an identifier for the given connection.
///
/// Users must call [`Self::reserve_space`] before calling this method to ensure enough
/// space is available.
///
/// Returns true if the identifier was inserted successfully, false if the identifier was
/// already inserted or reached capacity.
fn register_identifier(&self, connection_id: ConnectionId, identifier: String) -> bool {
let mut data = self.data.lock();
let entry = data.entry(connection_id).or_insert_with(ConnectionData::default);
// Should be already checked `Self::reserve_space`.
if entry.identifiers.len() >= self.capacity {
return false;
}
entry.identifiers.insert(identifier)
}
/// Unregister an identifier for the given connection.
fn unregister_identifier(&self, connection_id: ConnectionId, identifier: &str) {
let mut data = self.data.lock();
if let Some(connection_data) = data.get_mut(&connection_id) {
connection_data.identifiers.remove(identifier);
connection_data.num_identifiers = connection_data.num_identifiers.saturating_sub(1);
if connection_data.num_identifiers == 0 {
data.remove(&connection_id);
}
}
}
/// Check if the given connection contains the given identifier.
pub fn contains_identifier(&self, connection_id: ConnectionId, identifier: &str) -> bool {
let data = self.data.lock();
data.get(&connection_id)
.map(|connection_data| connection_data.identifiers.contains(identifier))
.unwrap_or(false)
}
}
/// RAII wrapper that ensures the reserved space is given back if the object is
/// dropped before the identifier is registered.
pub struct ReservedConnection {
connection_id: ConnectionId,
rpc_connections: Option<RpcConnections>,
}
impl ReservedConnection {
/// Register the identifier for the given connection.
pub fn register(mut self, identifier: String) -> Option<RegisteredConnection> {
let rpc_connections = self.rpc_connections.take()?;
if rpc_connections.register_identifier(self.connection_id, identifier.clone()) {
Some(RegisteredConnection {
connection_id: self.connection_id,
identifier,
rpc_connections,
})
} else {
None
}
}
}
impl Drop for ReservedConnection {
fn drop(&mut self) {
if let Some(rpc_connections) = self.rpc_connections.take() {
rpc_connections.unreserve_space(self.connection_id);
}
}
}
/// RAII wrapper that ensures the identifier is unregistered if the object is dropped.
pub struct RegisteredConnection {
connection_id: ConnectionId,
identifier: String,
rpc_connections: RpcConnections,
}
impl Drop for RegisteredConnection {
fn drop(&mut self) {
self.rpc_connections.unregister_identifier(self.connection_id, &self.identifier);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reserve_space() {
let rpc_connections = RpcConnections::new(2);
let conn_id = ConnectionId(1);
let reserved = rpc_connections.reserve_space(conn_id);
assert!(reserved.is_some());
assert_eq!(1, rpc_connections.data.lock().get(&conn_id).unwrap().num_identifiers);
assert_eq!(rpc_connections.data.lock().len(), 1);
let reserved = reserved.unwrap();
let registered = reserved.register("identifier1".to_string()).unwrap();
assert!(rpc_connections.contains_identifier(conn_id, "identifier1"));
assert_eq!(1, rpc_connections.data.lock().get(&conn_id).unwrap().num_identifiers);
drop(registered);
// Data is dropped.
assert!(rpc_connections.data.lock().get(&conn_id).is_none());
assert!(rpc_connections.data.lock().is_empty());
// Checks can still happen.
assert!(!rpc_connections.contains_identifier(conn_id, "identifier1"));
}
#[test]
fn reserve_space_capacity_reached() {
let rpc_connections = RpcConnections::new(2);
let conn_id = ConnectionId(1);
// Reserve identifier for connection 1.
let reserved = rpc_connections.reserve_space(conn_id);
assert!(reserved.is_some());
assert_eq!(1, rpc_connections.data.lock().get(&conn_id).unwrap().num_identifiers);
// Add identifier for connection 1.
let reserved = reserved.unwrap();
let registered = reserved.register("identifier1".to_string()).unwrap();
assert!(rpc_connections.contains_identifier(conn_id, "identifier1"));
assert_eq!(1, rpc_connections.data.lock().get(&conn_id).unwrap().num_identifiers);
// Reserve identifier for connection 1 again.
let reserved = rpc_connections.reserve_space(conn_id);
assert!(reserved.is_some());
assert_eq!(2, rpc_connections.data.lock().get(&conn_id).unwrap().num_identifiers);
// Add identifier for connection 1 again.
let reserved = reserved.unwrap();
let registered_second = reserved.register("identifier2".to_string()).unwrap();
assert!(rpc_connections.contains_identifier(conn_id, "identifier2"));
assert_eq!(2, rpc_connections.data.lock().get(&conn_id).unwrap().num_identifiers);
// Cannot reserve more identifiers.
let reserved = rpc_connections.reserve_space(conn_id);
assert!(reserved.is_none());
// Drop the first identifier.
drop(registered);
assert_eq!(1, rpc_connections.data.lock().get(&conn_id).unwrap().num_identifiers);
assert!(rpc_connections.contains_identifier(conn_id, "identifier2"));
assert!(!rpc_connections.contains_identifier(conn_id, "identifier1"));
// Can reserve again after clearing the space.
let reserved = rpc_connections.reserve_space(conn_id);
assert!(reserved.is_some());
assert_eq!(2, rpc_connections.data.lock().get(&conn_id).unwrap().num_identifiers);
// Ensure data is cleared.
drop(reserved);
drop(registered_second);
assert!(rpc_connections.data.lock().get(&conn_id).is_none());
}
}
@@ -0,0 +1,482 @@
// 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 <https://www.gnu.org/licenses/>.
//! Common events for RPC-V2 spec.
use serde::{Deserialize, Serialize};
/// The storage item to query.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StorageQuery<Key> {
/// The provided key.
pub key: Key,
/// The type of the storage query.
#[serde(rename = "type")]
pub query_type: StorageQueryType,
/// The optional pagination start key for descendants queries.
/// Only valid for `DescendantsValues` and `DescendantsHashes` query types.
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub pagination_start_key: Option<Key>,
}
/// The type of the storage query.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum StorageQueryType {
/// Fetch the value of the provided key.
Value,
/// Fetch the hash of the value of the provided key.
Hash,
/// Fetch the closest descendant merkle value.
ClosestDescendantMerkleValue,
/// Fetch the values of all descendants of they provided key.
DescendantsValues,
/// Fetch the hashes of the values of all descendants of they provided key.
DescendantsHashes,
}
impl StorageQueryType {
/// Returns `true` if the query is a descendant query.
pub fn is_descendant_query(&self) -> bool {
matches!(self, Self::DescendantsValues | Self::DescendantsHashes)
}
}
/// The storage result.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StorageResult {
/// The hex-encoded key of the result.
pub key: String,
/// The result of the query.
#[serde(flatten)]
pub result: StorageResultType,
/// The child trie key if provided.
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub child_trie_key: Option<String>,
}
/// The type of the storage query.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum StorageResultType {
/// Fetch the value of the provided key.
Value(String),
/// Fetch the hash of the value of the provided key.
Hash(String),
/// Fetch the closest descendant merkle value.
ClosestDescendantMerkleValue(String),
}
/// The result of a storage call.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "event")]
pub enum ArchiveStorageEvent {
/// Query generated a result.
Storage(StorageResult),
/// Query encountered an error.
StorageError(ArchiveStorageMethodErr),
/// Operation storage is done.
StorageDone,
}
impl ArchiveStorageEvent {
/// Create a new `ArchiveStorageEvent::StorageErr` event.
pub fn err(error: String) -> Self {
Self::StorageError(ArchiveStorageMethodErr { error })
}
/// Create a new `ArchiveStorageEvent::StorageResult` event.
pub fn result(result: StorageResult) -> Self {
Self::Storage(result)
}
/// Checks if the event is a `StorageDone` event.
pub fn is_done(&self) -> bool {
matches!(self, Self::StorageDone)
}
/// Checks if the event is a `StorageErr` event.
pub fn is_err(&self) -> bool {
matches!(self, Self::StorageError(_))
}
/// Checks if the event is a `StorageResult` event.
pub fn is_result(&self) -> bool {
matches!(self, Self::Storage(_))
}
}
/// The error of a storage call.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArchiveStorageMethodErr {
/// Reported error.
pub error: String,
}
/// The type of the archive storage difference query.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ArchiveStorageDiffType {
/// The result is provided as value of the key.
Value,
/// The result the hash of the value of the key.
Hash,
}
/// The storage item to query.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArchiveStorageDiffItem<Key> {
/// The provided key.
pub key: Key,
/// The type of the storage query.
pub return_type: ArchiveStorageDiffType,
/// The child trie key if provided.
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub child_trie_key: Option<Key>,
}
/// The result of a storage difference call operation type.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ArchiveStorageDiffOperationType {
/// The key is added.
Added,
/// The key is modified.
Modified,
/// The key is removed.
Deleted,
}
/// The result of an individual storage difference key.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArchiveStorageDiffResult {
/// The hex-encoded key of the result.
pub key: String,
/// The result of the query.
#[serde(flatten)]
pub result: StorageResultType,
/// The operation type.
#[serde(rename = "type")]
pub operation_type: ArchiveStorageDiffOperationType,
/// The child trie key if provided.
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub child_trie_key: Option<String>,
}
/// The event generated by the `archive_storageDiff` method.
///
/// The `archive_storageDiff` can generate the following events:
/// - `storageDiff` event - generated when a `ArchiveStorageDiffResult` is produced.
/// - `storageDiffError` event - generated when an error is produced.
/// - `storageDiffDone` event - generated when the `archive_storageDiff` method completed.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "event")]
pub enum ArchiveStorageDiffEvent {
/// The `storageDiff` event.
StorageDiff(ArchiveStorageDiffResult),
/// The `storageDiffError` event.
StorageDiffError(ArchiveStorageMethodErr),
/// The `storageDiffDone` event.
StorageDiffDone,
}
impl ArchiveStorageDiffEvent {
/// Create a new `ArchiveStorageDiffEvent::StorageDiffError` event.
pub fn err(error: String) -> Self {
Self::StorageDiffError(ArchiveStorageMethodErr { error })
}
/// Checks if the event is a `StorageDiffDone` event.
pub fn is_done(&self) -> bool {
matches!(self, Self::StorageDiffDone)
}
/// Checks if the event is a `StorageDiffError` event.
pub fn is_err(&self) -> bool {
matches!(self, Self::StorageDiffError(_))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn archive_diff_input() {
// Item with Value.
let item = ArchiveStorageDiffItem {
key: "0x1",
return_type: ArchiveStorageDiffType::Value,
child_trie_key: None,
};
// Encode
let ser = serde_json::to_string(&item).unwrap();
let exp = r#"{"key":"0x1","returnType":"value"}"#;
assert_eq!(ser, exp);
// Decode
let dec: ArchiveStorageDiffItem<&str> = serde_json::from_str(exp).unwrap();
assert_eq!(dec, item);
// Item with Hash.
let item = ArchiveStorageDiffItem {
key: "0x1",
return_type: ArchiveStorageDiffType::Hash,
child_trie_key: None,
};
// Encode
let ser = serde_json::to_string(&item).unwrap();
let exp = r#"{"key":"0x1","returnType":"hash"}"#;
assert_eq!(ser, exp);
// Decode
let dec: ArchiveStorageDiffItem<&str> = serde_json::from_str(exp).unwrap();
assert_eq!(dec, item);
// Item with Value and child trie key.
let item = ArchiveStorageDiffItem {
key: "0x1",
return_type: ArchiveStorageDiffType::Value,
child_trie_key: Some("0x2"),
};
// Encode
let ser = serde_json::to_string(&item).unwrap();
let exp = r#"{"key":"0x1","returnType":"value","childTrieKey":"0x2"}"#;
assert_eq!(ser, exp);
// Decode
let dec: ArchiveStorageDiffItem<&str> = serde_json::from_str(exp).unwrap();
assert_eq!(dec, item);
// Item with Hash and child trie key.
let item = ArchiveStorageDiffItem {
key: "0x1",
return_type: ArchiveStorageDiffType::Hash,
child_trie_key: Some("0x2"),
};
// Encode
let ser = serde_json::to_string(&item).unwrap();
let exp = r#"{"key":"0x1","returnType":"hash","childTrieKey":"0x2"}"#;
assert_eq!(ser, exp);
// Decode
let dec: ArchiveStorageDiffItem<&str> = serde_json::from_str(exp).unwrap();
assert_eq!(dec, item);
}
#[test]
fn archive_diff_output() {
// Item with Value.
let item = ArchiveStorageDiffResult {
key: "0x1".into(),
result: StorageResultType::Value("res".into()),
operation_type: ArchiveStorageDiffOperationType::Added,
child_trie_key: None,
};
// Encode
let ser = serde_json::to_string(&item).unwrap();
let exp = r#"{"key":"0x1","value":"res","type":"added"}"#;
assert_eq!(ser, exp);
// Decode
let dec: ArchiveStorageDiffResult = serde_json::from_str(exp).unwrap();
assert_eq!(dec, item);
// Item with Hash.
let item = ArchiveStorageDiffResult {
key: "0x1".into(),
result: StorageResultType::Hash("res".into()),
operation_type: ArchiveStorageDiffOperationType::Modified,
child_trie_key: None,
};
// Encode
let ser = serde_json::to_string(&item).unwrap();
let exp = r#"{"key":"0x1","hash":"res","type":"modified"}"#;
assert_eq!(ser, exp);
// Decode
let dec: ArchiveStorageDiffResult = serde_json::from_str(exp).unwrap();
assert_eq!(dec, item);
// Item with Hash, child trie key and removed.
let item = ArchiveStorageDiffResult {
key: "0x1".into(),
result: StorageResultType::Hash("res".into()),
operation_type: ArchiveStorageDiffOperationType::Deleted,
child_trie_key: Some("0x2".into()),
};
// Encode
let ser = serde_json::to_string(&item).unwrap();
let exp = r#"{"key":"0x1","hash":"res","type":"deleted","childTrieKey":"0x2"}"#;
assert_eq!(ser, exp);
// Decode
let dec: ArchiveStorageDiffResult = serde_json::from_str(exp).unwrap();
assert_eq!(dec, item);
}
#[test]
fn storage_result() {
// Item with Value.
let item = StorageResult {
key: "0x1".into(),
result: StorageResultType::Value("res".into()),
child_trie_key: None,
};
// Encode
let ser = serde_json::to_string(&item).unwrap();
let exp = r#"{"key":"0x1","value":"res"}"#;
assert_eq!(ser, exp);
// Decode
let dec: StorageResult = serde_json::from_str(exp).unwrap();
assert_eq!(dec, item);
// Item with Hash.
let item = StorageResult {
key: "0x1".into(),
result: StorageResultType::Hash("res".into()),
child_trie_key: None,
};
// Encode
let ser = serde_json::to_string(&item).unwrap();
let exp = r#"{"key":"0x1","hash":"res"}"#;
assert_eq!(ser, exp);
// Decode
let dec: StorageResult = serde_json::from_str(exp).unwrap();
assert_eq!(dec, item);
// Item with DescendantsValues.
let item = StorageResult {
key: "0x1".into(),
result: StorageResultType::ClosestDescendantMerkleValue("res".into()),
child_trie_key: None,
};
// Encode
let ser = serde_json::to_string(&item).unwrap();
let exp = r#"{"key":"0x1","closestDescendantMerkleValue":"res"}"#;
assert_eq!(ser, exp);
// Decode
let dec: StorageResult = serde_json::from_str(exp).unwrap();
assert_eq!(dec, item);
}
#[test]
fn storage_query() {
// Item with Value.
let item = StorageQuery {
key: "0x1",
query_type: StorageQueryType::Value,
pagination_start_key: None,
};
// Encode
let ser = serde_json::to_string(&item).unwrap();
let exp = r#"{"key":"0x1","type":"value"}"#;
assert_eq!(ser, exp);
// Decode
let dec: StorageQuery<&str> = serde_json::from_str(exp).unwrap();
assert_eq!(dec, item);
// Item with Hash.
let item = StorageQuery {
key: "0x1",
query_type: StorageQueryType::Hash,
pagination_start_key: None,
};
// Encode
let ser = serde_json::to_string(&item).unwrap();
let exp = r#"{"key":"0x1","type":"hash"}"#;
assert_eq!(ser, exp);
// Decode
let dec: StorageQuery<&str> = serde_json::from_str(exp).unwrap();
assert_eq!(dec, item);
// Item with DescendantsValues.
let item = StorageQuery {
key: "0x1",
query_type: StorageQueryType::DescendantsValues,
pagination_start_key: None,
};
// Encode
let ser = serde_json::to_string(&item).unwrap();
let exp = r#"{"key":"0x1","type":"descendantsValues"}"#;
assert_eq!(ser, exp);
// Decode
let dec: StorageQuery<&str> = serde_json::from_str(exp).unwrap();
assert_eq!(dec, item);
// Item with DescendantsHashes.
let item = StorageQuery {
key: "0x1",
query_type: StorageQueryType::DescendantsHashes,
pagination_start_key: None,
};
// Encode
let ser = serde_json::to_string(&item).unwrap();
let exp = r#"{"key":"0x1","type":"descendantsHashes"}"#;
assert_eq!(ser, exp);
// Decode
let dec: StorageQuery<&str> = serde_json::from_str(exp).unwrap();
assert_eq!(dec, item);
// Item with Merkle.
let item = StorageQuery {
key: "0x1",
query_type: StorageQueryType::ClosestDescendantMerkleValue,
pagination_start_key: None,
};
// Encode
let ser = serde_json::to_string(&item).unwrap();
let exp = r#"{"key":"0x1","type":"closestDescendantMerkleValue"}"#;
assert_eq!(ser, exp);
// Decode
let dec: StorageQuery<&str> = serde_json::from_str(exp).unwrap();
assert_eq!(dec, item);
// Item with DescendantsValues and paginationStartKey.
let item = StorageQuery {
key: "0x1",
query_type: StorageQueryType::DescendantsValues,
pagination_start_key: Some("0x2"),
};
// Encode
let ser = serde_json::to_string(&item).unwrap();
let exp = r#"{"key":"0x1","type":"descendantsValues","paginationStartKey":"0x2"}"#;
assert_eq!(ser, exp);
// Decode
let dec: StorageQuery<&str> = serde_json::from_str(exp).unwrap();
assert_eq!(dec, item);
// Item with DescendantsHashes and paginationStartKey.
let item = StorageQuery {
key: "0x1",
query_type: StorageQueryType::DescendantsHashes,
pagination_start_key: Some("0x2"),
};
// Encode
let ser = serde_json::to_string(&item).unwrap();
let exp = r#"{"key":"0x1","type":"descendantsHashes","paginationStartKey":"0x2"}"#;
assert_eq!(ser, exp);
// Decode
let dec: StorageQuery<&str> = serde_json::from_str(exp).unwrap();
assert_eq!(dec, item);
}
}
@@ -0,0 +1,23 @@
// 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 <https://www.gnu.org/licenses/>.
//! Common types and functionality for the RPC-V2 spec.
pub mod connections;
pub mod events;
pub mod storage;
@@ -0,0 +1,309 @@
// 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 <https://www.gnu.org/licenses/>.
//! Storage queries for the RPC-V2 spec.
use std::{marker::PhantomData, sync::Arc};
use pezsc_client_api::{Backend, ChildInfo, StorageKey, StorageProvider};
use pezsp_runtime::traits::Block as BlockT;
use tokio::sync::mpsc;
use super::events::{StorageQuery, StorageQueryType, StorageResult, StorageResultType};
use crate::hex_string;
/// Call into the storage of blocks.
pub struct Storage<Client, Block, BE> {
/// Bizinikiwi client.
client: Arc<Client>,
_phandom: PhantomData<(BE, Block)>,
}
impl<Client, Block, BE> Clone for Storage<Client, Block, BE> {
fn clone(&self) -> Self {
Self { client: self.client.clone(), _phandom: PhantomData }
}
}
impl<Client, Block, BE> Storage<Client, Block, BE> {
/// Constructs a new [`Storage`].
pub fn new(client: Arc<Client>) -> Self {
Self { client, _phandom: PhantomData }
}
}
/// Query to iterate over storage.
#[derive(Debug)]
pub struct QueryIter {
/// The key from which the iteration was started.
pub query_key: StorageKey,
/// The key after which pagination should resume.
pub pagination_start_key: Option<StorageKey>,
/// The type of the query (either value or hash).
pub ty: IterQueryType,
}
/// The query type of an iteration.
#[derive(Debug)]
pub enum IterQueryType {
/// Iterating over (key, value) pairs.
Value,
/// Iterating over (key, hash) pairs.
Hash,
}
/// The result of making a query call.
pub type QueryResult = Result<Option<StorageResult>, String>;
impl<Client, Block, BE> Storage<Client, Block, BE>
where
Block: BlockT + 'static,
BE: Backend<Block> + 'static,
Client: StorageProvider<Block, BE> + 'static,
{
/// Fetch the value from storage.
pub fn query_value(
&self,
hash: Block::Hash,
key: &StorageKey,
child_key: Option<&ChildInfo>,
) -> QueryResult {
let result = if let Some(child_key) = child_key {
self.client.child_storage(hash, child_key, key)
} else {
self.client.storage(hash, key)
};
result
.map(|opt| {
QueryResult::Ok(opt.map(|storage_data| StorageResult {
key: hex_string(&key.0),
result: StorageResultType::Value(hex_string(&storage_data.0)),
child_trie_key: child_key.map(|c| hex_string(&c.storage_key())),
}))
})
.unwrap_or_else(|error| QueryResult::Err(error.to_string()))
}
/// Fetch the hash of a value from storage.
pub fn query_hash(
&self,
hash: Block::Hash,
key: &StorageKey,
child_key: Option<&ChildInfo>,
) -> QueryResult {
let result = if let Some(child_key) = child_key {
self.client.child_storage_hash(hash, child_key, key)
} else {
self.client.storage_hash(hash, key)
};
result
.map(|opt| {
QueryResult::Ok(opt.map(|storage_data| StorageResult {
key: hex_string(&key.0),
result: StorageResultType::Hash(hex_string(&storage_data.as_ref())),
child_trie_key: child_key.map(|c| hex_string(&c.storage_key())),
}))
})
.unwrap_or_else(|error| QueryResult::Err(error.to_string()))
}
/// Fetch the closest merkle value.
pub fn query_merkle_value(
&self,
hash: Block::Hash,
key: &StorageKey,
child_key: Option<&ChildInfo>,
) -> QueryResult {
let result = if let Some(ref child_key) = child_key {
self.client.child_closest_merkle_value(hash, child_key, key)
} else {
self.client.closest_merkle_value(hash, key)
};
result
.map(|opt| {
QueryResult::Ok(opt.map(|storage_data| {
let result = match &storage_data {
pezsc_client_api::MerkleValue::Node(data) => hex_string(&data.as_slice()),
pezsc_client_api::MerkleValue::Hash(hash) => hex_string(&hash.as_ref()),
};
StorageResult {
key: hex_string(&key.0),
result: StorageResultType::ClosestDescendantMerkleValue(result),
child_trie_key: child_key.map(|c| hex_string(&c.storage_key())),
}
}))
})
.unwrap_or_else(|error| QueryResult::Err(error.to_string()))
}
/// Iterate over the storage keys and send the results to the provided sender.
///
/// Because this relies on a bounded channel, it will pause the storage iteration
// if the channel is becomes full which in turn provides backpressure.
pub fn query_iter_pagination_with_producer(
&self,
query: QueryIter,
hash: Block::Hash,
child_key: Option<&ChildInfo>,
tx: &mpsc::Sender<QueryResult>,
) {
let QueryIter { ty, query_key, pagination_start_key } = query;
let maybe_storage = if let Some(child_key) = child_key {
self.client.child_storage_keys(
hash,
child_key.to_owned(),
Some(&query_key),
pagination_start_key.as_ref(),
)
} else {
self.client.storage_keys(hash, Some(&query_key), pagination_start_key.as_ref())
};
let keys_iter = match maybe_storage {
Ok(keys_iter) => keys_iter,
Err(error) => {
_ = tx.blocking_send(Err(error.to_string()));
return;
},
};
for key in keys_iter {
let result = match ty {
IterQueryType::Value => self.query_value(hash, &key, child_key),
IterQueryType::Hash => self.query_hash(hash, &key, child_key),
};
if tx.blocking_send(result).is_err() {
break;
}
}
}
/// Raw iterator over the keys.
pub fn raw_keys_iter(
&self,
hash: Block::Hash,
child_key: Option<ChildInfo>,
) -> Result<impl Iterator<Item = StorageKey>, String> {
let keys_iter = if let Some(child_key) = child_key {
self.client.child_storage_keys(hash, child_key, None, None)
} else {
self.client.storage_keys(hash, None, None)
};
keys_iter.map_err(|err| err.to_string())
}
}
/// Generates storage events for `chainHead_storage` and `archive_storage` subscriptions.
pub struct StorageSubscriptionClient<Client, Block, BE> {
/// Storage client.
client: Storage<Client, Block, BE>,
_phandom: PhantomData<(BE, Block)>,
}
impl<Client, Block, BE> Clone for StorageSubscriptionClient<Client, Block, BE> {
fn clone(&self) -> Self {
Self { client: self.client.clone(), _phandom: PhantomData }
}
}
impl<Client, Block, BE> StorageSubscriptionClient<Client, Block, BE> {
/// Constructs a new [`StorageSubscriptionClient`].
pub fn new(client: Arc<Client>) -> Self {
Self { client: Storage::new(client), _phandom: PhantomData }
}
}
impl<Client, Block, BE> StorageSubscriptionClient<Client, Block, BE>
where
Block: BlockT + 'static,
BE: Backend<Block> + 'static,
Client: StorageProvider<Block, BE> + Send + Sync + 'static,
{
/// Generate storage events to the provided sender.
pub async fn generate_events(
&mut self,
hash: Block::Hash,
items: Vec<StorageQuery<StorageKey>>,
child_key: Option<ChildInfo>,
tx: mpsc::Sender<QueryResult>,
) -> Result<(), tokio::task::JoinError> {
let this = self.clone();
tokio::task::spawn_blocking(move || {
for item in items {
match item.query_type {
StorageQueryType::Value => {
let rp = this.client.query_value(hash, &item.key, child_key.as_ref());
if tx.blocking_send(rp).is_err() {
break;
}
},
StorageQueryType::Hash => {
let rp = this.client.query_hash(hash, &item.key, child_key.as_ref());
if tx.blocking_send(rp).is_err() {
break;
}
},
StorageQueryType::ClosestDescendantMerkleValue => {
let rp =
this.client.query_merkle_value(hash, &item.key, child_key.as_ref());
if tx.blocking_send(rp).is_err() {
break;
}
},
StorageQueryType::DescendantsValues => {
let query = QueryIter {
query_key: item.key,
ty: IterQueryType::Value,
pagination_start_key: item.pagination_start_key,
};
this.client.query_iter_pagination_with_producer(
query,
hash,
child_key.as_ref(),
&tx,
)
},
StorageQueryType::DescendantsHashes => {
let query = QueryIter {
query_key: item.key,
ty: IterQueryType::Hash,
pagination_start_key: item.pagination_start_key,
};
this.client.query_iter_pagination_with_producer(
query,
hash,
child_key.as_ref(),
&tx,
)
},
}
}
})
.await?;
Ok(())
}
}
+41
View File
@@ -0,0 +1,41 @@
// 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 <https://www.gnu.org/licenses/>.
//! Bizinikiwi JSON-RPC interface v2.
//!
//! Specification [document](https://paritytech.github.io/json-rpc-interface-spec/).
#![warn(missing_docs)]
#![deny(unused_crate_dependencies)]
use pezsp_core::hexdisplay::{AsBytesRef, HexDisplay};
mod common;
pub mod archive;
pub mod chain_head;
pub mod chain_spec;
pub mod transaction;
/// Task executor that is being used by RPC subscriptions.
pub type SubscriptionTaskExecutor = std::sync::Arc<dyn pezsp_core::traits::SpawnNamed>;
/// Util function to encode a value as a hex string
pub fn hex_string<Data: AsBytesRef>(data: &Data) -> String {
format!("0x{:?}", HexDisplay::from(data))
}
@@ -0,0 +1,61 @@
// 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 <https://www.gnu.org/licenses/>.
//! API trait for transactions.
use crate::transaction::{error::ErrorBroadcast, event::TransactionEvent};
use jsonrpsee::{core::RpcResult, proc_macros::rpc};
use pezsp_core::Bytes;
#[rpc(client, server)]
pub trait TransactionApi<Hash: Clone> {
/// Submit an extrinsic to watch.
///
/// See [`TransactionEvent`](crate::transaction::event::TransactionEvent) for details on
/// transaction life cycle.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[subscription(
name = "transactionWatch_v1_submitAndWatch" => "transactionWatch_v1_watchEvent",
unsubscribe = "transactionWatch_v1_unwatch",
item = TransactionEvent<Hash>,
)]
fn submit_and_watch(&self, bytes: Bytes);
}
#[rpc(client, server)]
pub trait TransactionBroadcastApi {
/// Broadcast an extrinsic to the chain.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "transaction_v1_broadcast", with_extensions)]
async fn broadcast(&self, bytes: Bytes) -> RpcResult<Option<String>>;
/// Broadcast an extrinsic to the chain.
///
/// # Unstable
///
/// This method is unstable and subject to change in the future.
#[method(name = "transaction_v1_stop", with_extensions)]
async fn stop_broadcast(&self, operation_id: String) -> Result<(), ErrorBroadcast>;
}
@@ -0,0 +1,127 @@
// 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 <https://www.gnu.org/licenses/>.
//! Transaction RPC errors.
//!
//! Errors are interpreted as transaction events for subscriptions.
use crate::transaction::event::{TransactionError, TransactionEvent};
use jsonrpsee::types::error::ErrorObject;
use pezsc_transaction_pool_api::error::Error as PoolError;
use pezsp_runtime::transaction_validity::InvalidTransaction;
/// Transaction RPC errors.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// Transaction pool error.
#[error("Transaction pool error: {}", .0)]
Pool(#[from] PoolError),
/// Verification error.
#[error("Extrinsic verification error: {}", .0)]
Verification(Box<dyn std::error::Error + Send + Sync>),
}
impl<Hash> From<Error> for TransactionEvent<Hash> {
fn from(e: Error) -> Self {
match e {
Error::Verification(e) => TransactionEvent::Invalid(TransactionError {
error: format!("Verification error: {}", e),
}),
Error::Pool(PoolError::InvalidTransaction(InvalidTransaction::Custom(e))) =>
TransactionEvent::Invalid(TransactionError {
error: format!("Invalid transaction with custom error: {}", e),
}),
Error::Pool(PoolError::InvalidTransaction(e)) => {
let msg: &str = e.into();
TransactionEvent::Invalid(TransactionError {
error: format!("Invalid transaction: {}", msg),
})
},
Error::Pool(PoolError::UnknownTransaction(e)) => {
let msg: &str = e.into();
TransactionEvent::Invalid(TransactionError {
error: format!("Unknown transaction validity: {}", msg),
})
},
Error::Pool(PoolError::TemporarilyBanned) =>
TransactionEvent::Invalid(TransactionError {
error: "Transaction is temporarily banned".into(),
}),
Error::Pool(PoolError::AlreadyImported(_)) =>
TransactionEvent::Invalid(TransactionError {
error: "Transaction is already imported".into(),
}),
Error::Pool(PoolError::TooLowPriority { old, new }) =>
TransactionEvent::Invalid(TransactionError {
error: format!(
"The priority of the transaction is too low (pool {} > current {})",
old, new
),
}),
Error::Pool(PoolError::CycleDetected) => TransactionEvent::Invalid(TransactionError {
error: "The transaction contains a cyclic dependency".into(),
}),
Error::Pool(PoolError::ImmediatelyDropped) =>
TransactionEvent::Invalid(TransactionError {
error: "The transaction could not enter the pool because of the limit".into(),
}),
Error::Pool(PoolError::Unactionable) => TransactionEvent::Invalid(TransactionError {
error: "Transaction cannot be propagated and the local node does not author blocks"
.into(),
}),
Error::Pool(PoolError::NoTagsProvided) => TransactionEvent::Invalid(TransactionError {
error: "Transaction does not provide any tags, so the pool cannot identify it"
.into(),
}),
Error::Pool(PoolError::InvalidBlockId(_)) =>
TransactionEvent::Invalid(TransactionError {
error: "The provided block ID is not valid".into(),
}),
Error::Pool(PoolError::RejectedFutureTransaction) =>
TransactionEvent::Invalid(TransactionError {
error: "The pool is not accepting future transactions".into(),
}),
}
}
}
/// TransactionBroadcast error.
#[derive(Debug, thiserror::Error)]
pub enum ErrorBroadcast {
/// The provided operation ID is invalid.
#[error("Invalid operation id")]
InvalidOperationID,
}
/// General purpose errors, as defined in
/// <https://www.jsonrpc.org/specification#error_object>.
pub mod json_rpc_spec {
/// Invalid parameter error.
pub const INVALID_PARAM_ERROR: i32 = -32602;
}
impl From<ErrorBroadcast> for ErrorObject<'static> {
fn from(e: ErrorBroadcast) -> Self {
let msg = e.to_string();
match e {
ErrorBroadcast::InvalidOperationID =>
ErrorObject::owned(json_rpc_spec::INVALID_PARAM_ERROR, msg, None::<()>),
}
}
}
@@ -0,0 +1,303 @@
// 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 <https://www.gnu.org/licenses/>.
//! The transaction's event returned as json compatible object.
use serde::{Deserialize, Serialize};
/// The transaction was included in a block of the chain.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransactionBlock<Hash> {
/// The hash of the block the transaction was included into.
pub hash: Hash,
/// The index (zero-based) of the transaction within the body of the block.
pub index: usize,
}
/// The transaction could not be processed due to an error.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransactionError {
/// Reason of the error.
pub error: String,
}
/// The transaction was dropped because of exceeding limits.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransactionDropped {
/// Reason of the event.
pub error: String,
}
/// Possible transaction status events.
///
/// The status events can be grouped based on their kinds as:
///
/// 1. Runtime validated the transaction and it entered the pool:
/// - `Validated`
///
/// 2. Leaving the pool:
/// - `BestChainBlockIncluded`
/// - `Invalid`
///
/// 3. Block finalized:
/// - `Finalized`
///
/// 4. At any time:
/// - `Dropped`
/// - `Error`
///
/// The subscription's stream is considered finished whenever the following events are
/// received: `Finalized`, `Error`, `Invalid` or `Dropped`. However, the user is allowed
/// to unsubscribe at any moment.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
// We need to manually specify the trait bounds for the `Hash` trait to ensure `into` and
// `from` still work.
#[serde(bound(
serialize = "Hash: Serialize + Clone",
deserialize = "Hash: Deserialize<'de> + Clone"
))]
#[serde(into = "TransactionEventIR<Hash>", from = "TransactionEventIR<Hash>")]
pub enum TransactionEvent<Hash> {
/// The transaction was validated by the runtime.
Validated,
/// The transaction was included in a best block of the chain.
///
/// # Note
///
/// This may contain `None` if the block is no longer a best
/// block of the chain.
BestChainBlockIncluded(Option<TransactionBlock<Hash>>),
/// The transaction was included in a finalized block.
Finalized(TransactionBlock<Hash>),
/// The transaction could not be processed due to an error.
Error(TransactionError),
/// The transaction is marked as invalid.
Invalid(TransactionError),
/// The client was not capable of keeping track of this transaction.
Dropped(TransactionDropped),
}
impl<Hash> TransactionEvent<Hash> {
/// Returns true if this is the last event emitted by the RPC subscription.
pub fn is_final(&self) -> bool {
matches!(
&self,
TransactionEvent::Finalized(_) |
TransactionEvent::Error(_) |
TransactionEvent::Invalid(_) |
TransactionEvent::Dropped(_)
)
}
}
/// Intermediate representation (IR) for the transaction events
/// that handles block events only.
///
/// The block events require a JSON compatible interpretation similar to:
///
/// ```json
/// { event: "EVENT", block: { hash: "0xFF", index: 0 } }
/// ```
///
/// This IR is introduced to circumvent that the block events need to
/// be serialized/deserialized with "tag" and "content", while other
/// events only require "tag".
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "event", content = "block")]
enum TransactionEventBlockIR<Hash> {
/// The transaction was included in the best block of the chain.
BestChainBlockIncluded(Option<TransactionBlock<Hash>>),
/// The transaction was included in a finalized block of the chain.
Finalized(TransactionBlock<Hash>),
}
/// Intermediate representation (IR) for the transaction events
/// that handles non-block events only.
///
/// The non-block events require a JSON compatible interpretation similar to:
///
/// ```json
/// { event: "EVENT", num_peers: 0 }
/// ```
///
/// This IR is introduced to circumvent that the block events need to
/// be serialized/deserialized with "tag" and "content", while other
/// events only require "tag".
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "event")]
enum TransactionEventNonBlockIR {
Validated,
Error(TransactionError),
Invalid(TransactionError),
Dropped(TransactionDropped),
}
/// Intermediate representation (IR) used for serialization/deserialization of the
/// [`TransactionEvent`] in a JSON compatible format.
///
/// Serde cannot mix `#[serde(tag = "event")]` with `#[serde(tag = "event", content = "block")]`
/// for specific enum variants. Therefore, this IR is introduced to circumvent this
/// restriction, while exposing a simplified [`TransactionEvent`] for users of the
/// rust ecosystem.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(bound(serialize = "Hash: Serialize", deserialize = "Hash: Deserialize<'de>"))]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
enum TransactionEventIR<Hash> {
Block(TransactionEventBlockIR<Hash>),
NonBlock(TransactionEventNonBlockIR),
}
impl<Hash> From<TransactionEvent<Hash>> for TransactionEventIR<Hash> {
fn from(value: TransactionEvent<Hash>) -> Self {
match value {
TransactionEvent::Validated =>
TransactionEventIR::NonBlock(TransactionEventNonBlockIR::Validated),
TransactionEvent::BestChainBlockIncluded(event) =>
TransactionEventIR::Block(TransactionEventBlockIR::BestChainBlockIncluded(event)),
TransactionEvent::Finalized(event) =>
TransactionEventIR::Block(TransactionEventBlockIR::Finalized(event)),
TransactionEvent::Error(event) =>
TransactionEventIR::NonBlock(TransactionEventNonBlockIR::Error(event)),
TransactionEvent::Invalid(event) =>
TransactionEventIR::NonBlock(TransactionEventNonBlockIR::Invalid(event)),
TransactionEvent::Dropped(event) =>
TransactionEventIR::NonBlock(TransactionEventNonBlockIR::Dropped(event)),
}
}
}
impl<Hash> From<TransactionEventIR<Hash>> for TransactionEvent<Hash> {
fn from(value: TransactionEventIR<Hash>) -> Self {
match value {
TransactionEventIR::NonBlock(status) => match status {
TransactionEventNonBlockIR::Validated => TransactionEvent::Validated,
TransactionEventNonBlockIR::Error(event) => TransactionEvent::Error(event),
TransactionEventNonBlockIR::Invalid(event) => TransactionEvent::Invalid(event),
TransactionEventNonBlockIR::Dropped(event) => TransactionEvent::Dropped(event),
},
TransactionEventIR::Block(block) => match block {
TransactionEventBlockIR::Finalized(event) => TransactionEvent::Finalized(event),
TransactionEventBlockIR::BestChainBlockIncluded(event) =>
TransactionEvent::BestChainBlockIncluded(event),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pezsp_core::H256;
#[test]
fn validated_event() {
let event: TransactionEvent<()> = TransactionEvent::Validated;
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"validated"}"#;
assert_eq!(ser, exp);
let event_dec: TransactionEvent<()> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
#[test]
fn best_chain_event() {
let event: TransactionEvent<()> = TransactionEvent::BestChainBlockIncluded(None);
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"bestChainBlockIncluded","block":null}"#;
assert_eq!(ser, exp);
let event_dec: TransactionEvent<()> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
let event: TransactionEvent<H256> =
TransactionEvent::BestChainBlockIncluded(Some(TransactionBlock {
hash: H256::from_low_u64_be(1),
index: 2,
}));
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"bestChainBlockIncluded","block":{"hash":"0x0000000000000000000000000000000000000000000000000000000000000001","index":2}}"#;
assert_eq!(ser, exp);
let event_dec: TransactionEvent<H256> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
#[test]
fn finalized_event() {
let event: TransactionEvent<H256> = TransactionEvent::Finalized(TransactionBlock {
hash: H256::from_low_u64_be(1),
index: 10,
});
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"finalized","block":{"hash":"0x0000000000000000000000000000000000000000000000000000000000000001","index":10}}"#;
assert_eq!(ser, exp);
let event_dec: TransactionEvent<H256> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
#[test]
fn error_event() {
let event: TransactionEvent<()> =
TransactionEvent::Error(TransactionError { error: "abc".to_string() });
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"error","error":"abc"}"#;
assert_eq!(ser, exp);
let event_dec: TransactionEvent<()> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
#[test]
fn invalid_event() {
let event: TransactionEvent<()> =
TransactionEvent::Invalid(TransactionError { error: "abc".to_string() });
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"invalid","error":"abc"}"#;
assert_eq!(ser, exp);
let event_dec: TransactionEvent<()> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
#[test]
fn dropped_event() {
let event: TransactionEvent<()> =
TransactionEvent::Dropped(TransactionDropped { error: "abc".to_string() });
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"event":"dropped","error":"abc"}"#;
assert_eq!(ser, exp);
let event_dec: TransactionEvent<()> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, event);
}
}
@@ -0,0 +1,155 @@
// 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 <https://www.gnu.org/licenses/>.
//! Metrics for recording transaction events.
use std::{collections::HashSet, time::Instant};
use prometheus_endpoint::{
exponential_buckets, linear_buckets, register, Histogram, HistogramOpts, PrometheusError,
Registry,
};
use super::TransactionEvent;
/// RPC layer metrics for transaction pool.
#[derive(Debug, Clone)]
pub struct Metrics {
validated: Histogram,
in_block: Histogram,
finalized: Histogram,
dropped: Histogram,
invalid: Histogram,
error: Histogram,
}
impl Metrics {
/// Creates a new [`Metrics`] instance.
pub fn new(registry: &Registry) -> Result<Self, PrometheusError> {
let validated = register(
Histogram::with_opts(
HistogramOpts::new(
"rpc_transaction_validation_time",
"RPC Transaction validation time in seconds",
)
.buckets(exponential_buckets(0.01, 2.0, 16).expect("Valid buckets; qed")),
)?,
registry,
)?;
let in_block = register(
Histogram::with_opts(
HistogramOpts::new(
"rpc_transaction_in_block_time",
"RPC Transaction in block time in seconds",
)
.buckets(linear_buckets(0.0, 3.0, 20).expect("Valid buckets; qed")),
)?,
registry,
)?;
let finalized = register(
Histogram::with_opts(
HistogramOpts::new(
"rpc_transaction_finalized_time",
"RPC Transaction finalized time in seconds",
)
.buckets(linear_buckets(0.01, 40.0, 20).expect("Valid buckets; qed")),
)?,
registry,
)?;
let dropped = register(
Histogram::with_opts(
HistogramOpts::new(
"rpc_transaction_dropped_time",
"RPC Transaction dropped time in seconds",
)
.buckets(linear_buckets(0.01, 3.0, 20).expect("Valid buckets; qed")),
)?,
registry,
)?;
let invalid = register(
Histogram::with_opts(
HistogramOpts::new(
"rpc_transaction_invalid_time",
"RPC Transaction invalid time in seconds",
)
.buckets(linear_buckets(0.01, 3.0, 20).expect("Valid buckets; qed")),
)?,
registry,
)?;
let error = register(
Histogram::with_opts(
HistogramOpts::new(
"rpc_transaction_error_time",
"RPC Transaction error time in seconds",
)
.buckets(linear_buckets(0.01, 3.0, 20).expect("Valid buckets; qed")),
)?,
registry,
)?;
Ok(Metrics { validated, in_block, finalized, dropped, invalid, error })
}
}
/// Transaction metrics for a single transaction instance.
pub struct InstanceMetrics {
/// The metrics instance.
metrics: Option<Metrics>,
/// The time when the transaction was submitted.
submitted_at: Instant,
/// Ensure the states are reported once.
reported_states: HashSet<&'static str>,
}
impl InstanceMetrics {
/// Creates a new [`InstanceMetrics`] instance.
pub fn new(metrics: Option<Metrics>) -> Self {
Self { metrics, submitted_at: Instant::now(), reported_states: HashSet::new() }
}
/// Record the execution time of a transaction state.
///
/// This represents how long it took for the transaction to move to the next state.
///
/// The method must be called before the transaction event is provided to the user.
pub fn register_event<Hash>(&mut self, event: &TransactionEvent<Hash>) {
let Some(ref metrics) = self.metrics else {
return;
};
let (histogram, target_state) = match event {
TransactionEvent::Validated => (&metrics.validated, "validated"),
TransactionEvent::BestChainBlockIncluded(Some(_)) => (&metrics.in_block, "in_block"),
TransactionEvent::BestChainBlockIncluded(None) => (&metrics.in_block, "retracted"),
TransactionEvent::Finalized(..) => (&metrics.finalized, "finalized"),
TransactionEvent::Error(..) => (&metrics.error, "error"),
TransactionEvent::Dropped(..) => (&metrics.dropped, "dropped"),
TransactionEvent::Invalid(..) => (&metrics.invalid, "invalid"),
};
// Only record the state if it hasn't been reported before.
if self.reported_states.insert(target_state) {
histogram.observe(self.submitted_at.elapsed().as_secs_f64());
}
}
}
@@ -0,0 +1,43 @@
// 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 <https://www.gnu.org/licenses/>.
//! Bizinikiwi transaction API.
//!
//! The transaction methods allow submitting a transaction and subscribing to
//! its status updates generated by the chain.
//!
//! # Note
//!
//! Methods are prefixed by `transaction`.
#[cfg(test)]
mod tests;
mod metrics;
pub mod api;
pub mod error;
pub mod event;
pub mod transaction;
pub mod transaction_broadcast;
pub use api::{TransactionApiServer, TransactionBroadcastApiServer};
pub use event::{TransactionBlock, TransactionDropped, TransactionError, TransactionEvent};
pub use metrics::Metrics as TransactionMetrics;
pub use transaction::Transaction;
pub use transaction_broadcast::TransactionBroadcast;
@@ -0,0 +1,100 @@
// 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 <https://www.gnu.org/licenses/>.
use pezsp_core::{testing::TaskExecutor, traits::SpawnNamed};
use std::sync::{atomic::AtomicUsize, Arc};
use tokio::sync::mpsc;
/// Wrap the `TaskExecutor` to know when the broadcast future is dropped.
#[derive(Clone)]
pub struct TaskExecutorBroadcast {
executor: TaskExecutor,
sender: mpsc::UnboundedSender<()>,
num_tasks: Arc<AtomicUsize>,
}
/// The channel that receives events when the broadcast futures are dropped.
pub type TaskExecutorRecv = mpsc::UnboundedReceiver<()>;
/// The state of the `TaskExecutorBroadcast`.
pub struct TaskExecutorState {
pub recv: TaskExecutorRecv,
pub num_tasks: Arc<AtomicUsize>,
}
impl TaskExecutorState {
pub fn num_tasks(&self) -> usize {
self.num_tasks.load(std::sync::atomic::Ordering::Acquire)
}
}
impl TaskExecutorBroadcast {
/// Construct a new `TaskExecutorBroadcast` and a receiver to know when the broadcast futures
/// are dropped.
pub fn new() -> (Self, TaskExecutorState) {
let (sender, recv) = mpsc::unbounded_channel();
let num_tasks = Arc::new(AtomicUsize::new(0));
(
Self { executor: TaskExecutor::new(), sender, num_tasks: num_tasks.clone() },
TaskExecutorState { recv, num_tasks },
)
}
}
impl SpawnNamed for TaskExecutorBroadcast {
fn spawn(
&self,
name: &'static str,
group: Option<&'static str>,
future: futures::future::BoxFuture<'static, ()>,
) {
let sender = self.sender.clone();
let num_tasks = self.num_tasks.clone();
let future = Box::pin(async move {
num_tasks.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
future.await;
num_tasks.fetch_sub(1, std::sync::atomic::Ordering::AcqRel);
let _ = sender.send(());
});
self.executor.spawn(name, group, future)
}
fn spawn_blocking(
&self,
name: &'static str,
group: Option<&'static str>,
future: futures::future::BoxFuture<'static, ()>,
) {
let sender = self.sender.clone();
let num_tasks = self.num_tasks.clone();
let future = Box::pin(async move {
num_tasks.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
future.await;
num_tasks.fetch_sub(1, std::sync::atomic::Ordering::AcqRel);
let _ = sender.send(());
});
self.executor.spawn_blocking(name, group, future)
}
}
@@ -0,0 +1,190 @@
// 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 <https://www.gnu.org/licenses/>.
use async_trait::async_trait;
use codec::Encode;
use pezsc_transaction_pool::BasicPool;
use pezsc_transaction_pool_api::{
ImportNotificationStream, PoolStatus, ReadyTransactions, TransactionFor, TransactionPool,
TransactionSource, TransactionStatusStreamFor, TxHash, TxInvalidityReportMap,
};
use crate::hex_string;
use futures::StreamExt;
use pezsp_runtime::traits::Block as BlockT;
use std::{collections::HashMap, pin::Pin, sync::Arc};
use bizinikiwi_test_runtime_transaction_pool::TestApi;
use tokio::sync::mpsc;
pub type Block = bizinikiwi_test_runtime_client::runtime::Block;
pub type TxTestPool = MiddlewarePool;
pub type TxStatusType<Pool> = pezsc_transaction_pool_api::TransactionStatus<
pezsc_transaction_pool_api::TxHash<Pool>,
pezsc_transaction_pool_api::BlockHash<Pool>,
>;
pub type TxStatusTypeTest = TxStatusType<TxTestPool>;
/// The type of the event that the middleware captures.
#[derive(Debug, PartialEq)]
pub enum MiddlewarePoolEvent {
TransactionStatus {
transaction: String,
status: pezsc_transaction_pool_api::TransactionStatus<
<Block as BlockT>::Hash,
<Block as BlockT>::Hash,
>,
},
PoolError {
transaction: String,
err: String,
},
}
/// The channel that receives events when the broadcast futures are dropped.
pub type MiddlewarePoolRecv = mpsc::UnboundedReceiver<MiddlewarePoolEvent>;
/// Add a middleware to the transaction pool.
///
/// This wraps the `submit_and_watch` to gain access to the events.
pub struct MiddlewarePool {
pub inner_pool: Arc<BasicPool<TestApi, Block>>,
/// Send the middleware events to the test.
sender: mpsc::UnboundedSender<MiddlewarePoolEvent>,
}
impl MiddlewarePool {
/// Construct a new [`MiddlewarePool`].
pub fn new(pool: Arc<BasicPool<TestApi, Block>>) -> (Self, MiddlewarePoolRecv) {
let (sender, recv) = mpsc::unbounded_channel();
(MiddlewarePool { inner_pool: pool, sender }, recv)
}
}
#[async_trait]
impl TransactionPool for MiddlewarePool {
type Block = <BasicPool<TestApi, Block> as TransactionPool>::Block;
type Hash = <BasicPool<TestApi, Block> as TransactionPool>::Hash;
type InPoolTransaction = <BasicPool<TestApi, Block> as TransactionPool>::InPoolTransaction;
type Error = <BasicPool<TestApi, Block> as TransactionPool>::Error;
async fn submit_at(
&self,
at: <Self::Block as BlockT>::Hash,
source: TransactionSource,
xts: Vec<TransactionFor<Self>>,
) -> Result<Vec<Result<TxHash<Self>, Self::Error>>, Self::Error> {
self.inner_pool.submit_at(at, source, xts).await
}
async fn submit_one(
&self,
at: <Self::Block as BlockT>::Hash,
source: TransactionSource,
xt: TransactionFor<Self>,
) -> Result<TxHash<Self>, Self::Error> {
self.inner_pool.submit_one(at, source, xt).await
}
async fn submit_and_watch(
&self,
at: <Self::Block as BlockT>::Hash,
source: TransactionSource,
xt: TransactionFor<Self>,
) -> Result<Pin<Box<TransactionStatusStreamFor<Self>>>, Self::Error> {
let transaction = hex_string(&xt.encode());
let sender = self.sender.clone();
let watcher = match self.inner_pool.submit_and_watch(at, source, xt).await {
Ok(watcher) => watcher,
Err(err) => {
let _ = sender.send(MiddlewarePoolEvent::PoolError {
transaction: transaction.clone(),
err: err.to_string(),
});
return Err(err);
},
};
let watcher = watcher.map(move |status| {
let sender = sender.clone();
let transaction = transaction.clone();
let _ = sender.send(MiddlewarePoolEvent::TransactionStatus {
transaction,
status: status.clone(),
});
status
});
Ok(watcher.boxed())
}
async fn report_invalid(
&self,
at: Option<<Self::Block as BlockT>::Hash>,
invalid_tx_errors: TxInvalidityReportMap<TxHash<Self>>,
) -> Vec<Arc<Self::InPoolTransaction>> {
self.inner_pool.report_invalid(at, invalid_tx_errors).await
}
fn status(&self) -> PoolStatus {
self.inner_pool.status()
}
fn import_notification_stream(&self) -> ImportNotificationStream<TxHash<Self>> {
self.inner_pool.import_notification_stream()
}
fn hash_of(&self, xt: &TransactionFor<Self>) -> TxHash<Self> {
self.inner_pool.hash_of(xt)
}
fn on_broadcasted(&self, propagations: HashMap<TxHash<Self>, Vec<String>>) {
self.inner_pool.on_broadcasted(propagations)
}
fn ready_transaction(&self, hash: &TxHash<Self>) -> Option<Arc<Self::InPoolTransaction>> {
self.inner_pool.ready_transaction(hash)
}
async fn ready_at(
&self,
at: <Self::Block as BlockT>::Hash,
) -> Box<dyn ReadyTransactions<Item = Arc<Self::InPoolTransaction>> + Send> {
self.inner_pool.ready_at(at).await
}
fn ready(&self) -> Box<dyn ReadyTransactions<Item = Arc<Self::InPoolTransaction>> + Send> {
self.inner_pool.ready()
}
fn futures(&self) -> Vec<Self::InPoolTransaction> {
self.inner_pool.futures()
}
async fn ready_at_with_timeout(
&self,
at: <Self::Block as BlockT>::Hash,
_timeout: std::time::Duration,
) -> Box<dyn ReadyTransactions<Item = Arc<Self::InPoolTransaction>> + Send> {
self.inner_pool.ready_at(at).await
}
}
@@ -0,0 +1,25 @@
// 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 <https://www.gnu.org/licenses/>.
mod executor;
mod middleware_pool;
#[macro_use]
mod setup;
mod transaction_broadcast_tests;
mod transaction_tests;
@@ -0,0 +1,161 @@
// 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 <https://www.gnu.org/licenses/>.
use crate::{
chain_head::test_utils::ChainHeadMockClient,
transaction::{
api::{TransactionApiServer, TransactionBroadcastApiServer},
tests::executor::{TaskExecutorBroadcast, TaskExecutorState},
Transaction as RpcTransaction, TransactionBroadcast as RpcTransactionBroadcast,
},
};
use futures::Future;
use jsonrpsee::RpcModule;
use pezsc_transaction_pool::*;
use std::{pin::Pin, sync::Arc};
use bizinikiwi_test_runtime_client::{prelude::*, Client};
use bizinikiwi_test_runtime_transaction_pool::TestApi;
use crate::transaction::tests::middleware_pool::{MiddlewarePool, MiddlewarePoolRecv};
pub type Block = bizinikiwi_test_runtime_client::runtime::Block;
/// Initial Alice account nonce.
pub const ALICE_NONCE: u64 = 209;
fn create_basic_pool_with_genesis(
test_api: Arc<TestApi>,
options: Options,
) -> (BasicPool<TestApi, Block>, Pin<Box<dyn Future<Output = ()> + Send>>) {
let genesis_hash = {
test_api
.chain()
.read()
.block_by_number
.get(&0)
.map(|blocks| blocks[0].0.header.hash())
.expect("there is block 0. qed")
};
BasicPool::new_test(test_api, genesis_hash, genesis_hash, options)
}
fn maintained_pool(
options: Options,
) -> (BasicPool<TestApi, Block>, Arc<TestApi>, futures::executor::ThreadPool) {
let api = Arc::new(TestApi::with_alice_nonce(ALICE_NONCE));
let (pool, background_task) = create_basic_pool_with_genesis(api.clone(), options);
let thread_pool = futures::executor::ThreadPool::new().unwrap();
thread_pool.spawn_ok(background_task);
(pool, api, thread_pool)
}
pub fn setup_api(
options: Options,
max_tx_per_connection: usize,
) -> (
Arc<TestApi>,
Arc<MiddlewarePool>,
Arc<ChainHeadMockClient<Client<Backend>>>,
RpcModule<RpcTransactionBroadcast<MiddlewarePool, ChainHeadMockClient<Client<Backend>>>>,
TaskExecutorState,
MiddlewarePoolRecv,
) {
let (pool, api, _) = maintained_pool(options);
let (pool, pool_state) = MiddlewarePool::new(Arc::new(pool).clone());
let pool = Arc::new(pool);
let builder = TestClientBuilder::new();
let client = Arc::new(builder.build());
let client_mock = Arc::new(ChainHeadMockClient::new(client.clone()));
let (task_executor, executor_recv) = TaskExecutorBroadcast::new();
let tx_api = RpcTransactionBroadcast::new(
client_mock.clone(),
pool.clone(),
Arc::new(task_executor),
max_tx_per_connection,
)
.into_rpc();
(api, pool, client_mock, tx_api, executor_recv, pool_state)
}
pub fn setup_api_tx() -> (
Arc<TestApi>,
Arc<MiddlewarePool>,
Arc<ChainHeadMockClient<Client<Backend>>>,
RpcModule<RpcTransaction<MiddlewarePool, ChainHeadMockClient<Client<Backend>>>>,
TaskExecutorState,
MiddlewarePoolRecv,
) {
let (pool, api, _) = maintained_pool(Default::default());
let (pool, pool_state) = MiddlewarePool::new(Arc::new(pool).clone());
let pool = Arc::new(pool);
let builder = TestClientBuilder::new();
let client = Arc::new(builder.build());
let client_mock = Arc::new(ChainHeadMockClient::new(client.clone()));
let (task_executor, executor_recv) = TaskExecutorBroadcast::new();
let tx_api =
RpcTransaction::new(client_mock.clone(), pool.clone(), Arc::new(task_executor), None)
.into_rpc();
(api, pool, client_mock, tx_api, executor_recv, pool_state)
}
/// Get the next event from the provided middleware in at most 5 seconds.
macro_rules! get_next_event {
($middleware:expr) => {
tokio::time::timeout(std::time::Duration::from_secs(5), $middleware.recv())
.await
.unwrap()
.unwrap()
};
}
/// Get the next event from the provided middleware in at most 5 seconds.
macro_rules! get_next_event_sub {
($sub:expr) => {
tokio::time::timeout(std::time::Duration::from_secs(5), $sub.next())
.await
.unwrap()
.unwrap()
.unwrap()
.0
};
}
/// Collect the next number of transaction events from the provided middleware.
macro_rules! get_next_tx_events {
($middleware:expr, $num:expr) => {{
let mut events = std::collections::HashMap::new();
for _ in 0..$num {
let event = get_next_event!($middleware);
match event {
crate::transaction::tests::middleware_pool::MiddlewarePoolEvent::TransactionStatus { transaction, status } => {
events.entry(transaction).or_insert_with(|| vec![]).push(status);
},
other => panic!("Expected TransactionStatus, received {:?}", other),
};
}
events
}};
}
@@ -0,0 +1,561 @@
// 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 <https://www.gnu.org/licenses/>.
use crate::{hex_string, transaction::error::json_rpc_spec};
use assert_matches::assert_matches;
use codec::Encode;
use jsonrpsee::{rpc_params, MethodsError as Error};
use pezsc_transaction_pool::{Options, PoolLimit};
use pezsc_transaction_pool_api::{ChainEvent, MaintainedTransactionPool, TransactionPool};
use std::sync::Arc;
use bizinikiwi_test_runtime_client::Sr25519Keyring::*;
use bizinikiwi_test_runtime_transaction_pool::uxt;
const MAX_TX_PER_CONNECTION: usize = 4;
// Test helpers.
use crate::transaction::tests::{
middleware_pool::{MiddlewarePoolEvent, TxStatusTypeTest},
setup::{setup_api, ALICE_NONCE},
};
#[tokio::test]
async fn tx_broadcast_enters_pool() {
let (api, pool, client_mock, tx_api, mut exec_middleware, mut pool_middleware) =
setup_api(Default::default(), MAX_TX_PER_CONNECTION);
// Start at block 1.
let block_1_header = api.push_block(1, vec![], true);
let uxt = uxt(Alice, ALICE_NONCE);
let xt = hex_string(&uxt.encode());
// Announce block 1 to `transaction_v1_broadcast`.
client_mock.set_best_block(block_1_header.hash(), 1);
let operation_id: String =
tx_api.call("transaction_v1_broadcast", rpc_params![&xt]).await.unwrap();
// Ensure the tx propagated from `transaction_v1_broadcast` to the transaction pool.
let event = get_next_event!(&mut pool_middleware);
assert_eq!(
event,
MiddlewarePoolEvent::TransactionStatus {
transaction: xt.clone(),
status: TxStatusTypeTest::Ready
}
);
assert_eq!(1, pool.inner_pool.status().ready);
assert_eq!(uxt.encode().len(), pool.inner_pool.status().ready_bytes);
// Import block 2 with the transaction included.
let block_2_header = api.push_block(2, vec![uxt.clone()], true);
let block_2 = block_2_header.hash();
// Announce block 2 to the pool.
let event = ChainEvent::NewBestBlock { hash: block_2, tree_route: None };
pool.inner_pool.maintain(event).await;
assert_eq!(0, pool.inner_pool.status().ready);
let event = get_next_event!(&mut pool_middleware);
assert_eq!(
event,
MiddlewarePoolEvent::TransactionStatus {
transaction: xt.clone(),
status: TxStatusTypeTest::InBlock((block_2, 0))
}
);
// The future broadcast awaits for the finalized status to be reached.
// Force the future to exit by calling stop.
let _: () = tx_api.call("transaction_v1_stop", rpc_params![&operation_id]).await.unwrap();
// Ensure the broadcast future finishes.
let _ = get_next_event!(&mut exec_middleware.recv);
assert_eq!(0, exec_middleware.num_tasks());
}
#[tokio::test]
async fn tx_broadcast_invalid_tx() {
let (_, pool, _, tx_api, exec_middleware, _) =
setup_api(Default::default(), MAX_TX_PER_CONNECTION);
// Invalid parameters.
let err = tx_api
.call::<_, serde_json::Value>("transaction_v1_broadcast", [1u8])
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(err) if err.code() == json_rpc_spec::INVALID_PARAM_ERROR && err.message() == "Invalid params"
);
assert_eq!(0, pool.status().ready);
// Invalid transaction that cannot be decoded. The broadcast silently exits.
let xt = "0xdeadbeef";
let operation_id: String =
tx_api.call("transaction_v1_broadcast", rpc_params![&xt]).await.unwrap();
assert_eq!(0, pool.status().ready);
// The broadcast future should never be spawned when the tx decoding fails.
assert_eq!(0, exec_middleware.num_tasks());
// The operation ID is no longer active.
// When the operation is not active, either from the tx being finalized or a
// terminal error; the stop method should return an error.
let err = tx_api
.call::<_, serde_json::Value>("transaction_v1_stop", rpc_params![&operation_id])
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(err) if err.code() == json_rpc_spec::INVALID_PARAM_ERROR && err.message() == "Invalid operation id"
);
}
#[tokio::test]
async fn tx_stop_with_invalid_operation_id() {
let (_, _, _, tx_api, _, _) = setup_api(Default::default(), MAX_TX_PER_CONNECTION);
// Make an invalid stop call.
let err = tx_api
.call::<_, serde_json::Value>("transaction_v1_stop", ["invalid_operation_id"])
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(err) if err.code() == json_rpc_spec::INVALID_PARAM_ERROR && err.message() == "Invalid operation id"
);
}
#[tokio::test]
async fn tx_broadcast_resubmits_future_nonce_tx() {
let (api, pool, client_mock, tx_api, mut exec_middleware, mut pool_middleware) =
setup_api(Default::default(), MAX_TX_PER_CONNECTION);
// Start at block 1.
let block_1_header = api.push_block(1, vec![], true);
let block_1 = block_1_header.hash();
let current_uxt = uxt(Alice, ALICE_NONCE);
let current_xt = hex_string(&current_uxt.encode());
// This lives in the future.
let future_uxt = uxt(Alice, ALICE_NONCE + 1);
let future_xt = hex_string(&future_uxt.encode());
// Announce block 1 to `transaction_v1_broadcast`.
client_mock.set_best_block(block_1_header.hash(), 1);
let future_operation_id: String =
tx_api.call("transaction_v1_broadcast", rpc_params![&future_xt]).await.unwrap();
// Ensure the tx propagated from `transaction_v1_broadcast` to the transaction pool.
let event = get_next_event!(&mut pool_middleware);
assert_eq!(
event,
MiddlewarePoolEvent::TransactionStatus {
transaction: future_xt.clone(),
status: TxStatusTypeTest::Future
}
);
let event = ChainEvent::NewBestBlock { hash: block_1, tree_route: None };
pool.inner_pool.maintain(event).await;
assert_eq!(0, pool.inner_pool.status().ready);
// Ensure the tx is in the future.
assert_eq!(1, pool.inner_pool.status().future);
let block_2_header = api.push_block(2, vec![], true);
let block_2 = block_2_header.hash();
client_mock.set_best_block(block_2_header.hash(), 1);
let operation_id: String =
tx_api.call("transaction_v1_broadcast", rpc_params![&current_xt]).await.unwrap();
assert_ne!(future_operation_id, operation_id);
// Collect the events of both transactions.
let events = get_next_tx_events!(&mut pool_middleware, 2);
// Transactions entered the ready queue.
assert_eq!(events.get(&current_xt).unwrap(), &vec![TxStatusTypeTest::Ready]);
assert_eq!(events.get(&future_xt).unwrap(), &vec![TxStatusTypeTest::Ready]);
let event = ChainEvent::NewBestBlock { hash: block_2, tree_route: None };
pool.inner_pool.maintain(event).await;
assert_eq!(2, pool.inner_pool.status().ready);
assert_eq!(0, pool.inner_pool.status().future);
// Finalize transactions.
let block_3_header = api.push_block(3, vec![current_uxt, future_uxt], true);
let block_3 = block_3_header.hash();
client_mock.trigger_import_stream(block_3_header).await;
let event = ChainEvent::Finalized { hash: block_3, tree_route: Arc::from(vec![]) };
pool.inner_pool.maintain(event).await;
assert_eq!(0, pool.inner_pool.status().ready);
assert_eq!(0, pool.inner_pool.status().future);
let events = get_next_tx_events!(&mut pool_middleware, 4);
assert_eq!(
events.get(&current_xt).unwrap(),
&vec![TxStatusTypeTest::InBlock((block_3, 0)), TxStatusTypeTest::Finalized((block_3, 0))]
);
assert_eq!(
events.get(&future_xt).unwrap(),
&vec![TxStatusTypeTest::InBlock((block_3, 1)), TxStatusTypeTest::Finalized((block_3, 1))]
);
// Both broadcast futures must exit.
let _ = get_next_event!(&mut exec_middleware.recv);
let _ = get_next_event!(&mut exec_middleware.recv);
assert_eq!(0, exec_middleware.num_tasks());
}
/// This test is similar to `tx_broadcast_enters_pool`
/// However the last block is announced as finalized to force the
/// broadcast future to exit before the `stop` is called.
#[tokio::test]
async fn tx_broadcast_stop_after_broadcast_finishes() {
let (api, pool, client_mock, tx_api, mut exec_middleware, mut pool_middleware) =
setup_api(Default::default(), MAX_TX_PER_CONNECTION);
// Start at block 1.
let block_1_header = api.push_block(1, vec![], true);
let uxt = uxt(Alice, ALICE_NONCE);
let xt = hex_string(&uxt.encode());
// Announce block 1 to `transaction_v1_broadcast`.
client_mock.set_best_block(block_1_header.hash(), 1);
let operation_id: String =
tx_api.call("transaction_v1_broadcast", rpc_params![&xt]).await.unwrap();
// Ensure the tx propagated from `transaction_v1_broadcast` to the transaction
// pool.inner_pool.
let event = get_next_event!(&mut pool_middleware);
assert_eq!(
event,
MiddlewarePoolEvent::TransactionStatus {
transaction: xt.clone(),
status: TxStatusTypeTest::Ready
}
);
assert_eq!(1, pool.inner_pool.status().ready);
assert_eq!(uxt.encode().len(), pool.inner_pool.status().ready_bytes);
// Import block 2 with the transaction included.
let block_2_header = api.push_block(2, vec![uxt.clone()], true);
let block_2 = block_2_header.hash();
// Announce block 2 to the pool.inner_pool.
let event = ChainEvent::Finalized { hash: block_2, tree_route: Arc::from(vec![]) };
pool.inner_pool.maintain(event).await;
assert_eq!(0, pool.inner_pool.status().ready);
let event = get_next_event!(&mut pool_middleware);
assert_eq!(
event,
MiddlewarePoolEvent::TransactionStatus {
transaction: xt.clone(),
status: TxStatusTypeTest::InBlock((block_2, 0))
}
);
let event = get_next_event!(&mut pool_middleware);
assert_eq!(
event,
MiddlewarePoolEvent::TransactionStatus {
transaction: xt.clone(),
status: TxStatusTypeTest::Finalized((block_2, 0))
}
);
// Ensure the broadcast future terminated properly.
let _ = get_next_event!(&mut exec_middleware.recv);
assert_eq!(0, exec_middleware.num_tasks());
// The operation ID is no longer valid, check that the broadcast future
// cleared out the inner state of the operation.
let err = tx_api
.call::<_, serde_json::Value>("transaction_v1_stop", rpc_params![&operation_id])
.await
.unwrap_err();
assert_matches!(err,
Error::JsonRpc(err) if err.code() == json_rpc_spec::INVALID_PARAM_ERROR && err.message() == "Invalid operation id"
);
}
#[tokio::test]
async fn tx_broadcast_resubmits_invalid_tx() {
let limits = PoolLimit { count: 8192, total_bytes: 20 * 1024 * 1024 };
let options = Options {
ready: limits.clone(),
future: limits,
reject_future_transactions: false,
// This ensures that a transaction is not banned.
ban_time: std::time::Duration::ZERO,
};
let (api, pool, client_mock, tx_api, mut exec_middleware, mut pool_middleware) =
setup_api(options, MAX_TX_PER_CONNECTION);
let uxt = uxt(Alice, ALICE_NONCE);
let xt = hex_string(&uxt.encode());
let block_1_header = api.push_block(1, vec![], true);
let block_1 = block_1_header.hash();
// Announce block 1 to `transaction_v1_broadcast`.
client_mock.set_best_block(block_1_header.hash(), 1);
let _operation_id: String =
tx_api.call("transaction_v1_broadcast", rpc_params![&xt]).await.unwrap();
// Ensure the tx propagated from `transaction_v1_broadcast` to the transaction pool.
let event = get_next_event!(&mut pool_middleware);
assert_eq!(
event,
MiddlewarePoolEvent::TransactionStatus {
transaction: xt.clone(),
status: TxStatusTypeTest::Ready,
}
);
assert_eq!(1, pool.inner_pool.status().ready);
assert_eq!(uxt.encode().len(), pool.inner_pool.status().ready_bytes);
// Mark the transaction as invalid from the API, causing a temporary ban.
api.add_invalid(&uxt);
// Push an event to the pool to ensure the transaction is excluded.
let event = ChainEvent::NewBestBlock { hash: block_1, tree_route: None };
pool.inner_pool.maintain(event).await;
assert_eq!(1, pool.inner_pool.status().ready);
// Ensure the `transaction_v1_broadcast` is aware of the invalid transaction.
let event = get_next_event!(&mut pool_middleware);
// Because we have received an `Invalid` status, we try to broadcast the transaction with the
// next announced block.
assert_eq!(
event,
MiddlewarePoolEvent::TransactionStatus {
transaction: xt.clone(),
status: TxStatusTypeTest::Invalid
}
);
// Import block 2.
let block_2_header = api.push_block(2, vec![], true);
client_mock.trigger_import_stream(block_2_header).await;
// Ensure we propagate the temporary ban error to `submit_and_watch`.
// This ensures we'll loop again with the next announced block and try to resubmit the
// transaction. The transaction remains temporarily banned until the pool is maintained.
let event = get_next_event!(&mut pool_middleware);
assert_matches!(event, MiddlewarePoolEvent::PoolError { transaction, err } if transaction == xt && err.contains("Transaction temporarily Banned"));
// Import block 3.
let block_3_header = api.push_block(3, vec![], true);
let block_3 = block_3_header.hash();
// Remove the invalid transaction from the pool to allow it to pass through.
api.remove_invalid(&uxt);
let event = ChainEvent::NewBestBlock { hash: block_3, tree_route: None };
// We have to maintain the pool to ensure the transaction is no longer invalid.
// This clears out the banned transactions.
pool.inner_pool.maintain(event).await;
assert_eq!(0, pool.inner_pool.status().ready);
// Announce block to `transaction_v1_broadcast`.
client_mock.trigger_import_stream(block_3_header).await;
let event = get_next_event!(&mut pool_middleware);
assert_eq!(
event,
MiddlewarePoolEvent::TransactionStatus {
transaction: xt.clone(),
status: TxStatusTypeTest::Ready,
}
);
assert_eq!(1, pool.inner_pool.status().ready);
let block_4_header = api.push_block(4, vec![uxt], true);
let block_4 = block_4_header.hash();
let event = ChainEvent::Finalized { hash: block_4, tree_route: Arc::from(vec![]) };
pool.inner_pool.maintain(event).await;
let event = get_next_event!(&mut pool_middleware);
assert_eq!(
event,
MiddlewarePoolEvent::TransactionStatus {
transaction: xt.clone(),
status: TxStatusTypeTest::InBlock((block_4, 0)),
}
);
let event = get_next_event!(&mut pool_middleware);
assert_eq!(
event,
MiddlewarePoolEvent::TransactionStatus {
transaction: xt.clone(),
status: TxStatusTypeTest::Finalized((block_4, 0)),
}
);
// Ensure the broadcast future terminated properly.
let _ = get_next_event!(&mut exec_middleware.recv);
assert_eq!(0, exec_middleware.num_tasks());
}
/// This is similar to `tx_broadcast_resubmits_invalid_tx`.
/// However, it forces the tx to be resubmitted because of the pool
/// limits. Which is a different code path than the invalid tx.
#[tokio::test]
async fn tx_broadcast_resubmits_dropped_tx() {
let limits = PoolLimit { count: 1, total_bytes: 1000 };
let options = Options {
ready: limits.clone(),
future: limits,
reject_future_transactions: false,
// This ensures that a transaction is not banned.
ban_time: std::time::Duration::ZERO,
};
let (api, pool, client_mock, tx_api, _, mut pool_middleware) =
setup_api(options, MAX_TX_PER_CONNECTION);
let current_uxt = uxt(Alice, ALICE_NONCE);
let current_xt = hex_string(&current_uxt.encode());
// This lives in the future.
let future_uxt = uxt(Alice, ALICE_NONCE + 1);
let future_xt = hex_string(&future_uxt.encode());
// By default the `validate_transaction` mock uses priority 1 for
// transactions. Bump the priority to ensure other transactions
// are immediately dropped.
api.set_priority(&current_uxt, 10);
// Announce block 1 to `transaction_v1_broadcast`.
let block_1_header = api.push_block(1, vec![], true);
let event =
ChainEvent::Finalized { hash: block_1_header.hash(), tree_route: Arc::from(vec![]) };
pool.inner_pool.maintain(event).await;
// Announce block 1 to `transaction_v1_broadcast`.
client_mock.set_best_block(block_1_header.hash(), 1);
let current_operation_id: String =
tx_api.call("transaction_v1_broadcast", rpc_params![&current_xt]).await.unwrap();
let event = get_next_event!(&mut pool_middleware);
assert_eq!(
event,
MiddlewarePoolEvent::TransactionStatus {
transaction: current_xt.clone(),
status: TxStatusTypeTest::Ready,
}
);
assert_eq!(1, pool.inner_pool.status().ready);
// The future tx has priority 2, smaller than the current 10.
api.set_priority(&future_uxt, 2);
let block_2_header = api.push_block(2, vec![], true);
let event =
ChainEvent::Finalized { hash: block_2_header.hash(), tree_route: Arc::from(vec![]) };
pool.inner_pool.maintain(event).await;
client_mock.set_best_block(block_2_header.hash(), 1);
let future_operation_id: String =
tx_api.call("transaction_v1_broadcast", rpc_params![&future_xt]).await.unwrap();
assert_ne!(current_operation_id, future_operation_id);
// We must have at most 1 transaction in the pool, as per limits above.
assert_eq!(1, pool.inner_pool.status().ready);
let event = get_next_event!(&mut pool_middleware);
assert_eq!(
event,
MiddlewarePoolEvent::PoolError {
transaction: future_xt.clone(),
err: "Transaction couldn't enter the pool because of the limit".into()
}
);
let block_3_header = api.push_block(3, vec![current_uxt], true);
let event =
ChainEvent::Finalized { hash: block_3_header.hash(), tree_route: Arc::from(vec![]) };
pool.inner_pool.maintain(event).await;
client_mock.trigger_import_stream(block_3_header.clone()).await;
// The first tx is in a finalized block; the future tx must enter the pool.
let events = get_next_tx_events!(&mut pool_middleware, 3);
assert_eq!(
events.get(&current_xt).unwrap(),
&vec![
TxStatusTypeTest::InBlock((block_3_header.hash(), 0)),
TxStatusTypeTest::Finalized((block_3_header.hash(), 0))
]
);
// The dropped transaction was resubmitted.
assert_eq!(events.get(&future_xt).unwrap(), &vec![TxStatusTypeTest::Ready]);
}
#[tokio::test]
async fn tx_broadcast_limit_reached() {
// One operation per connection.
let (api, _pool, client_mock, tx_api, mut exec_middleware, mut pool_middleware) =
setup_api(Default::default(), 1);
// Start at block 1.
let block_1_header = api.push_block(1, vec![], true);
let uxt = uxt(Alice, ALICE_NONCE);
let xt = hex_string(&uxt.encode());
// Announce block 1 to `transaction_v1_broadcast`.
client_mock.set_best_block(block_1_header.hash(), 1);
let operation_id: String =
tx_api.call("transaction_v1_broadcast", rpc_params![&xt]).await.unwrap();
// Ensure the tx propagated from `transaction_v1_broadcast` to the transaction pool.
let event = get_next_event!(&mut pool_middleware);
assert_eq!(
event,
MiddlewarePoolEvent::TransactionStatus {
transaction: xt.clone(),
status: TxStatusTypeTest::Ready
}
);
assert_eq!(1, exec_middleware.num_tasks());
let operation_id_limit_reached: Option<String> =
tx_api.call("transaction_v1_broadcast", rpc_params![&xt]).await.unwrap();
assert!(operation_id_limit_reached.is_none(), "No operation ID => tx was rejected");
// We still have in flight one operation.
assert_eq!(1, exec_middleware.num_tasks());
// Force the future to exit by calling stop.
let _: () = tx_api.call("transaction_v1_stop", rpc_params![&operation_id]).await.unwrap();
// Ensure the broadcast future finishes.
let _ = get_next_event!(&mut exec_middleware.recv);
assert_eq!(0, exec_middleware.num_tasks());
// Can resubmit again now.
let _operation_id: String =
tx_api.call("transaction_v1_broadcast", rpc_params![&xt]).await.unwrap();
}
@@ -0,0 +1,245 @@
// 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 <https://www.gnu.org/licenses/>.
use crate::{
hex_string,
transaction::{TransactionBlock, TransactionEvent},
};
use assert_matches::assert_matches;
use codec::Encode;
use jsonrpsee::rpc_params;
use pezsc_transaction_pool_api::{ChainEvent, MaintainedTransactionPool};
use pezsp_core::H256;
use std::{sync::Arc, vec};
use bizinikiwi_test_runtime_client::Sr25519Keyring::*;
use bizinikiwi_test_runtime_transaction_pool::uxt;
// Test helpers.
use crate::transaction::tests::setup::{setup_api_tx, ALICE_NONCE};
#[tokio::test]
async fn tx_invalid_bytes() {
let (_api, _pool, _client_mock, tx_api, _exec_middleware, _pool_middleware) = setup_api_tx();
// This should not rely on the tx pool state.
let mut sub = tx_api
.subscribe_unbounded("transactionWatch_v1_submitAndWatch", rpc_params![&"0xdeadbeef"])
.await
.unwrap();
let event: TransactionEvent<H256> = get_next_event_sub!(&mut sub);
assert_matches!(event, TransactionEvent::Invalid(_));
}
#[tokio::test]
async fn tx_in_finalized() {
let (api, pool, client, tx_api, _exec_middleware, _pool_middleware) = setup_api_tx();
let block_1_header = api.push_block(1, vec![], true);
client.set_best_block(block_1_header.hash(), 1);
let uxt = uxt(Alice, ALICE_NONCE);
let xt = hex_string(&uxt.encode());
let mut sub = tx_api
.subscribe_unbounded("transactionWatch_v1_submitAndWatch", rpc_params![&xt])
.await
.unwrap();
let event: TransactionEvent<H256> = get_next_event_sub!(&mut sub);
assert_eq!(event, TransactionEvent::Validated);
// Import block 2 with the transaction included.
let block_2_header = api.push_block(2, vec![uxt.clone()], true);
let block_2 = block_2_header.hash();
// Announce block 2 to the pool.
let event = ChainEvent::NewBestBlock { hash: block_2, tree_route: None };
pool.inner_pool.maintain(event).await;
let event = ChainEvent::Finalized { hash: block_2, tree_route: Arc::from(vec![]) };
pool.inner_pool.maintain(event).await;
let event: TransactionEvent<H256> = get_next_event_sub!(&mut sub);
assert_eq!(
event,
TransactionEvent::BestChainBlockIncluded(Some(TransactionBlock {
hash: block_2,
index: 0
}))
);
let event: TransactionEvent<H256> = get_next_event_sub!(&mut sub);
assert_eq!(event, TransactionEvent::Finalized(TransactionBlock { hash: block_2, index: 0 }));
}
#[tokio::test]
async fn tx_with_pruned_best_block() {
let (api, pool, client, tx_api, _exec_middleware, _pool_middleware) = setup_api_tx();
let block_1_header = api.push_block(1, vec![], true);
client.set_best_block(block_1_header.hash(), 1);
let uxt = uxt(Alice, ALICE_NONCE);
let xt = hex_string(&uxt.encode());
let mut sub = tx_api
.subscribe_unbounded("transactionWatch_v1_submitAndWatch", rpc_params![&xt])
.await
.unwrap();
let event: TransactionEvent<H256> = get_next_event_sub!(&mut sub);
assert_eq!(event, TransactionEvent::Validated);
// Import block 2 with the transaction included.
let block_2_header = api.push_block(2, vec![uxt.clone()], true);
let block_2 = block_2_header.hash();
let event = ChainEvent::NewBestBlock { hash: block_2, tree_route: None };
pool.inner_pool.maintain(event).await;
let event: TransactionEvent<H256> = get_next_event_sub!(&mut sub);
assert_eq!(
event,
TransactionEvent::BestChainBlockIncluded(Some(TransactionBlock {
hash: block_2,
index: 0
}))
);
// Import block 2 again without the transaction included.
let block_2_header = api.push_block(2, vec![], true);
let block_2 = block_2_header.hash();
let event = ChainEvent::NewBestBlock { hash: block_2, tree_route: None };
pool.inner_pool.maintain(event).await;
let event: TransactionEvent<H256> = get_next_event_sub!(&mut sub);
assert_eq!(event, TransactionEvent::BestChainBlockIncluded(None));
let block_2_header = api.push_block(2, vec![uxt.clone()], true);
let block_2 = block_2_header.hash();
let event = ChainEvent::NewBestBlock { hash: block_2, tree_route: None };
pool.inner_pool.maintain(event).await;
// The tx is validated again against the new block.
let event: TransactionEvent<H256> = get_next_event_sub!(&mut sub);
assert_eq!(event, TransactionEvent::Validated);
let event: TransactionEvent<H256> = get_next_event_sub!(&mut sub);
assert_eq!(
event,
TransactionEvent::BestChainBlockIncluded(Some(TransactionBlock {
hash: block_2,
index: 0
}))
);
let event = ChainEvent::Finalized { hash: block_2, tree_route: Arc::from(vec![]) };
pool.inner_pool.maintain(event).await;
let event: TransactionEvent<H256> = get_next_event_sub!(&mut sub);
assert_eq!(event, TransactionEvent::Finalized(TransactionBlock { hash: block_2, index: 0 }));
}
#[tokio::test]
async fn tx_slow_client_replace_old_messages() {
let (api, pool, client, tx_api, _exec_middleware, _pool_middleware) = setup_api_tx();
let block_1_header = api.push_block(1, vec![], true);
client.set_best_block(block_1_header.hash(), 1);
let uxt = uxt(Alice, ALICE_NONCE);
let xt = hex_string(&uxt.encode());
// The subscription itself has a buffer of length 1 and no way to create
// it without a buffer.
//
// Then `transactionWatch` has its own buffer of length 3 which leads to
// that it's limited to 5 items in the tests.
//
// 1. Send will complete immediately
// 2. Send will be pending in the subscription sink (not possible to cancel)
// 3. The rest of messages will be kept in a RingBuffer and older messages are replaced by newer
// items.
let mut sub = tx_api
.subscribe("transactionWatch_v1_submitAndWatch", rpc_params![&xt], 1)
.await
.unwrap();
assert_eq!(TransactionEvent::<H256>::Validated, sub.next().await.unwrap().unwrap().0);
// Import block 2 with the transaction included.
let block = api.push_block(2, vec![uxt.clone()], true);
let block_hash = block.hash();
let event = ChainEvent::NewBestBlock { hash: block_hash, tree_route: None };
pool.inner_pool.maintain(event).await;
// Import again with block 3
let block3 = api.push_block(3, vec![uxt.clone()], true);
let event = ChainEvent::NewBestBlock { hash: dbg!(block3.hash()), tree_route: None };
pool.inner_pool.maintain(event).await;
// Import block 3 again without the transaction included.
let block_not_imported = api.push_block(3, vec![], true);
let event = ChainEvent::NewBestBlock { hash: block_not_imported.hash(), tree_route: None };
pool.inner_pool.maintain(event).await;
// Import again with block 4
let block4 = api.push_block(4, vec![uxt.clone()], true);
let event = ChainEvent::NewBestBlock { hash: dbg!(block4.hash()), tree_route: None };
pool.inner_pool.maintain(event).await;
// Import again with block 5
let block5 = api.push_block(5, vec![uxt.clone()], true);
let event = ChainEvent::NewBestBlock { hash: dbg!(block5.hash()), tree_route: None };
pool.inner_pool.maintain(event).await;
// Finalize the transaction
let event = ChainEvent::Finalized { hash: block5.hash(), tree_route: Arc::from(vec![]) };
pool.inner_pool.maintain(event).await;
// Hack to mimic a slow client.
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
// Read the events.
let mut res: Vec<TransactionEvent<_>> = Vec::new();
while let Some(item) = tokio::time::timeout(std::time::Duration::from_secs(5), sub.next())
.await
.unwrap()
{
let (ev, _) = item.unwrap();
res.push(ev);
}
// `BestBlockIncluded(None)` is dropped and not seen.
let expected = vec![
TransactionEvent::BestChainBlockIncluded(Some(TransactionBlock {
hash: block_hash,
index: 0,
})),
TransactionEvent::BestChainBlockIncluded(Some(TransactionBlock {
hash: block3.hash(),
index: 0,
})),
TransactionEvent::BestChainBlockIncluded(Some(TransactionBlock {
hash: block4.hash(),
index: 0,
})),
TransactionEvent::BestChainBlockIncluded(Some(TransactionBlock {
hash: block5.hash(),
index: 0,
})),
TransactionEvent::Finalized(TransactionBlock { hash: block5.hash(), index: 0 }),
];
assert_eq!(expected, res);
}
@@ -0,0 +1,191 @@
// 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 <https://www.gnu.org/licenses/>.
//! API implementation for submitting transactions.
use crate::{
transaction::{
api::TransactionApiServer,
error::Error,
event::{TransactionBlock, TransactionDropped, TransactionError, TransactionEvent},
},
SubscriptionTaskExecutor,
};
use codec::Decode;
use futures::{StreamExt, TryFutureExt};
use jsonrpsee::{core::async_trait, PendingSubscriptionSink};
use super::metrics::{InstanceMetrics, Metrics};
use pezsc_rpc::utils::{RingBuffer, Subscription};
use pezsc_transaction_pool_api::{
error::IntoPoolError, BlockHash, TransactionFor, TransactionPool, TransactionSource,
TransactionStatus,
};
use pezsp_blockchain::HeaderBackend;
use pezsp_core::Bytes;
use pezsp_runtime::traits::Block as BlockT;
use std::sync::Arc;
pub(crate) const LOG_TARGET: &str = "rpc-spec-v2";
/// An API for transaction RPC calls.
pub struct Transaction<Pool, Client> {
/// Bizinikiwi client.
client: Arc<Client>,
/// Transactions pool.
pool: Arc<Pool>,
/// Executor to spawn subscriptions.
executor: SubscriptionTaskExecutor,
/// Metrics for transactions.
metrics: Option<Metrics>,
}
impl<Pool, Client> Transaction<Pool, Client> {
/// Creates a new [`Transaction`].
pub fn new(
client: Arc<Client>,
pool: Arc<Pool>,
executor: SubscriptionTaskExecutor,
metrics: Option<Metrics>,
) -> Self {
Transaction { client, pool, executor, metrics }
}
}
/// Currently we treat all RPC transactions as externals.
///
/// Possibly in the future we could allow opt-in for special treatment
/// of such transactions, so that the block authors can inject
/// some unique transactions via RPC and have them included in the pool.
const TX_SOURCE: TransactionSource = TransactionSource::External;
#[async_trait]
impl<Pool, Client> TransactionApiServer<BlockHash<Pool>> for Transaction<Pool, Client>
where
Pool: TransactionPool + Sync + Send + 'static,
Pool::Hash: Unpin,
<Pool::Block as BlockT>::Hash: Unpin,
Client: HeaderBackend<Pool::Block> + Send + Sync + 'static,
{
fn submit_and_watch(&self, pending: PendingSubscriptionSink, xt: Bytes) {
let client = self.client.clone();
let pool = self.pool.clone();
// Get a new transaction metrics instance and increment the counter.
let mut metrics = InstanceMetrics::new(self.metrics.clone());
let fut = async move {
let decoded_extrinsic = match TransactionFor::<Pool>::decode(&mut &xt[..]) {
Ok(decoded_extrinsic) => decoded_extrinsic,
Err(e) => {
log::debug!(target: LOG_TARGET, "Extrinsic bytes cannot be decoded: {:?}", e);
let Ok(sink) = pending.accept().await.map(Subscription::from) else { return };
let event = TransactionEvent::Invalid::<BlockHash<Pool>>(TransactionError {
error: "Extrinsic bytes cannot be decoded".into(),
});
metrics.register_event(&event);
// The transaction is invalid.
let _ = sink.send(&event).await;
return;
},
};
let best_block_hash = client.info().best_hash;
let submit = pool
.submit_and_watch(best_block_hash, TX_SOURCE, decoded_extrinsic)
.map_err(|e| {
e.into_pool_error()
.map(Error::from)
.unwrap_or_else(|e| Error::Verification(Box::new(e)))
});
let Ok(sink) = pending.accept().await.map(Subscription::from) else {
return;
};
match submit.await {
Ok(stream) => {
let stream = stream
.filter_map(|event| {
let event = handle_event(event);
event.as_ref().inspect(|event| {
metrics.register_event(event);
});
async move { event }
})
.boxed();
// If the subscription is too slow older events will be overwritten.
sink.pipe_from_stream(stream, RingBuffer::new(3)).await;
},
Err(err) => {
// We have not created an `Watcher` for the tx. Make sure the
// error is still propagated as an event.
let event: TransactionEvent<<Pool::Block as BlockT>::Hash> = err.into();
metrics.register_event(&event);
_ = sink.send(&event).await;
},
};
};
pezsc_rpc::utils::spawn_subscription_task(&self.executor, fut);
}
}
/// Handle events generated by the transaction-pool and convert them
/// to the new API expected state.
#[inline]
fn handle_event<Hash: Clone, BlockHash: Clone>(
event: TransactionStatus<Hash, BlockHash>,
) -> Option<TransactionEvent<BlockHash>> {
match event {
TransactionStatus::Ready | TransactionStatus::Future =>
Some(TransactionEvent::<BlockHash>::Validated),
TransactionStatus::InBlock((hash, index)) =>
Some(TransactionEvent::BestChainBlockIncluded(Some(TransactionBlock { hash, index }))),
TransactionStatus::Retracted(_) => Some(TransactionEvent::BestChainBlockIncluded(None)),
TransactionStatus::FinalityTimeout(_) =>
Some(TransactionEvent::Dropped(TransactionDropped {
error: "Maximum number of finality watchers has been reached".into(),
})),
TransactionStatus::Finalized((hash, index)) =>
Some(TransactionEvent::Finalized(TransactionBlock { hash, index })),
TransactionStatus::Usurped(_) => Some(TransactionEvent::Invalid(TransactionError {
error: "Extrinsic was rendered invalid by another extrinsic".into(),
})),
TransactionStatus::Dropped => Some(TransactionEvent::Dropped(TransactionDropped {
error: "Extrinsic dropped from the pool due to exceeding limits".into(),
})),
TransactionStatus::Invalid => Some(TransactionEvent::Invalid(TransactionError {
error: "Extrinsic marked as invalid".into(),
})),
// These are the events that are not supported by the new API.
TransactionStatus::Broadcast(_) => None,
}
}
@@ -0,0 +1,333 @@
// 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 <https://www.gnu.org/licenses/>.
//! API implementation for broadcasting transactions.
use crate::{
common::connections::RpcConnections, transaction::api::TransactionBroadcastApiServer,
SubscriptionTaskExecutor,
};
use codec::Decode;
use futures::{FutureExt, Stream, StreamExt};
use futures_util::stream::AbortHandle;
use jsonrpsee::{
core::{async_trait, RpcResult},
ConnectionId, Extensions,
};
use parking_lot::RwLock;
use rand::{distributions::Alphanumeric, Rng};
use pezsc_client_api::BlockchainEvents;
use pezsc_transaction_pool_api::{
error::IntoPoolError, TransactionFor, TransactionPool, TransactionSource,
};
use pezsp_blockchain::HeaderBackend;
use pezsp_core::Bytes;
use pezsp_runtime::traits::Block as BlockT;
use std::{collections::HashMap, sync::Arc};
use super::error::ErrorBroadcast;
/// An API for transaction RPC calls.
pub struct TransactionBroadcast<Pool: TransactionPool, Client> {
/// Bizinikiwi client.
client: Arc<Client>,
/// Transactions pool.
pool: Arc<Pool>,
/// Executor to spawn subscriptions.
executor: SubscriptionTaskExecutor,
/// The broadcast operation IDs.
broadcast_ids: Arc<RwLock<HashMap<String, BroadcastState<Pool>>>>,
/// Keep track of how many concurrent operations are active for each connection.
rpc_connections: RpcConnections,
}
/// The state of a broadcast operation.
struct BroadcastState<Pool: TransactionPool> {
/// Handle to abort the running future that broadcasts the transaction.
handle: AbortHandle,
/// Associated tx hash.
tx_hash: <Pool as TransactionPool>::Hash,
}
impl<Pool: TransactionPool, Client> TransactionBroadcast<Pool, Client> {
/// Creates a new [`TransactionBroadcast`].
pub fn new(
client: Arc<Client>,
pool: Arc<Pool>,
executor: SubscriptionTaskExecutor,
max_transactions_per_connection: usize,
) -> Self {
TransactionBroadcast {
client,
pool,
executor,
broadcast_ids: Default::default(),
rpc_connections: RpcConnections::new(max_transactions_per_connection),
}
}
/// Generate an unique operation ID for the `transaction_broadcast` RPC method.
pub fn generate_unique_id(&self) -> String {
let generate_operation_id = || {
// The length of the operation ID.
const OPERATION_ID_LEN: usize = 16;
rand::thread_rng()
.sample_iter(Alphanumeric)
.take(OPERATION_ID_LEN)
.map(char::from)
.collect::<String>()
};
let mut id = generate_operation_id();
let broadcast_ids = self.broadcast_ids.read();
while broadcast_ids.contains_key(&id) {
id = generate_operation_id();
}
id
}
}
/// Currently we treat all RPC transactions as externals.
///
/// Possibly in the future we could allow opt-in for special treatment
/// of such transactions, so that the block authors can inject
/// some unique transactions via RPC and have them included in the pool.
const TX_SOURCE: TransactionSource = TransactionSource::External;
#[async_trait]
impl<Pool, Client> TransactionBroadcastApiServer for TransactionBroadcast<Pool, Client>
where
Pool: TransactionPool + Sync + Send + 'static,
Pool::Error: IntoPoolError,
<Pool::Block as BlockT>::Hash: Unpin,
Client: HeaderBackend<Pool::Block> + BlockchainEvents<Pool::Block> + Send + Sync + 'static,
{
async fn broadcast(&self, ext: &Extensions, bytes: Bytes) -> RpcResult<Option<String>> {
let pool = self.pool.clone();
let conn_id = ext
.get::<ConnectionId>()
.copied()
.expect("ConnectionId is always set by jsonrpsee; qed");
// The unique ID of this operation.
let id = self.generate_unique_id();
// Ensure that the connection has not reached the maximum number of active operations.
let Some(reserved_connection) = self.rpc_connections.reserve_space(conn_id) else {
return Ok(None);
};
let Some(reserved_identifier) = reserved_connection.register(id.clone()) else {
// This can only happen if the generated operation ID is not unique.
return Ok(None);
};
// The JSON-RPC server might check whether the transaction is valid before broadcasting it.
// If it does so and if the transaction is invalid, the server should silently do nothing
// and the JSON-RPC client is not informed of the problem. Invalid transactions should still
// count towards the limit to the number of simultaneously broadcasted transactions.
let Ok(decoded_extrinsic) = TransactionFor::<Pool>::decode(&mut &bytes[..]) else {
return Ok(Some(id));
};
// Save the tx hash to remove it later.
let tx_hash = pool.hash_of(&decoded_extrinsic);
// Get a stream of best block hashes that immediately produces the current best block.
// This is used for the broadcast method to retry submitting the transaction to a future
// block if the error is retriable (for example, the transaction is invalid at the moment,
// but will become valid at a later block N + 1).
//
// Providing the best hash immediately is important for chains that are configured with
// `InstantSeal`.
let best_hash = self.client.info().best_hash;
// The compiler can no longer deduce the type of the stream and complains
// about `one type is more general than the other`.
let mut best_block_import_stream: std::pin::Pin<
Box<dyn Stream<Item = <Pool::Block as BlockT>::Hash> + Send>,
> = Box::pin(futures::stream::select(
futures::stream::iter(std::iter::once(best_hash)),
self.client.import_notification_stream().filter_map(|notification| async move {
notification.is_new_best.then_some(notification.hash)
}),
));
let broadcast_transaction_fut = async move {
// Flag to determine if the we should broadcast the transaction again.
let mut is_done = false;
while !is_done {
// Wait for the last block to become available.
let Some(best_block_hash) =
last_stream_element(&mut best_block_import_stream).await
else {
return;
};
let mut stream = match pool
.submit_and_watch(best_block_hash, TX_SOURCE, decoded_extrinsic.clone())
.await
{
Ok(stream) => stream,
// The transaction was not included to the pool.
Err(e) => {
let Ok(pool_err) = e.into_pool_error() else { return };
if pool_err.is_retriable() {
// Try to resubmit the transaction at a later block for
// recoverable errors.
continue;
} else {
return;
}
},
};
while let Some(event) = stream.next().await {
// Check if the transaction could be submitted again
// at a later time.
if event.is_retriable() {
break;
}
// Stop if this is the final event of the transaction stream
// and the event is not retriable.
if event.is_final() {
is_done = true;
break;
}
}
}
};
// Convert the future into an abortable future, for easily terminating it from the
// `transaction_stop` method.
let (fut, handle) = futures::future::abortable(broadcast_transaction_fut);
let broadcast_ids = self.broadcast_ids.clone();
let drop_id = id.clone();
let pool = self.pool.clone();
// The future expected by the executor must be `Future<Output = ()>` instead of
// `Future<Output = Result<(), Aborted>>`.
let fut = fut.then(move |result| {
async move {
// Connection space is cleaned when this object is dropped.
drop(reserved_identifier);
// Remove the entry from the broadcast IDs map.
let Some(broadcast_state) = broadcast_ids.write().remove(&drop_id) else { return };
// The broadcast was not stopped.
if result.is_ok() {
return;
}
// Best effort pool removal (tx can already be finalized).
pool.report_invalid(None, [(broadcast_state.tx_hash, None)].into()).await;
}
});
// Keep track of this entry and the abortable handle.
{
let mut broadcast_ids = self.broadcast_ids.write();
broadcast_ids.insert(id.clone(), BroadcastState { handle, tx_hash });
}
pezsc_rpc::utils::spawn_subscription_task(&self.executor, fut);
Ok(Some(id))
}
async fn stop_broadcast(
&self,
ext: &Extensions,
operation_id: String,
) -> Result<(), ErrorBroadcast> {
let conn_id = ext
.get::<ConnectionId>()
.copied()
.expect("ConnectionId is always set by jsonrpsee; qed");
// The operation ID must correlate to the same connection ID.
if !self.rpc_connections.contains_identifier(conn_id, &operation_id) {
return Err(ErrorBroadcast::InvalidOperationID);
}
let mut broadcast_ids = self.broadcast_ids.write();
let Some(broadcast_state) = broadcast_ids.remove(&operation_id) else {
return Err(ErrorBroadcast::InvalidOperationID);
};
broadcast_state.handle.abort();
Ok(())
}
}
/// Returns the last element of the provided stream, or `None` if the stream is closed.
async fn last_stream_element<S>(stream: &mut S) -> Option<S::Item>
where
S: Stream + Unpin,
{
let Some(mut element) = stream.next().await else { return None };
// We are effectively polling the stream for the last available item at this time.
// The `now_or_never` returns `None` if the stream is `Pending`.
//
// If the stream contains `Hash0x1 Hash0x2 Hash0x3 Hash0x4`, we want only `Hash0x4`.
while let Some(next) = stream.next().now_or_never() {
let Some(next) = next else {
// Nothing to do if the stream terminated.
return None;
};
element = next;
}
Some(element)
}
#[cfg(test)]
mod tests {
use super::*;
use tokio_stream::wrappers::ReceiverStream;
#[tokio::test]
async fn check_last_stream_element() {
let (tx, rx) = tokio::sync::mpsc::channel(16);
let mut stream = ReceiverStream::new(rx);
// Check the stream with one element queued.
tx.send(1).await.unwrap();
assert_eq!(last_stream_element(&mut stream).await, Some(1));
// Check the stream with multiple elements.
tx.send(1).await.unwrap();
tx.send(2).await.unwrap();
tx.send(3).await.unwrap();
assert_eq!(last_stream_element(&mut stream).await, Some(3));
// Drop the stream with some elements
tx.send(1).await.unwrap();
tx.send(2).await.unwrap();
drop(tx);
assert_eq!(last_stream_element(&mut stream).await, None);
}
}