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:
@@ -0,0 +1,90 @@
|
||||
[package]
|
||||
name = "pezsc-rpc-spec-v2"
|
||||
version = "0.34.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Bizinikiwi RPC interface v2."
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
jsonrpsee = { workspace = true, features = [
|
||||
"client-core",
|
||||
"macros",
|
||||
"server-core",
|
||||
] }
|
||||
# Internal chain structures for "chain_spec".
|
||||
pezsc-chain-spec = { workspace = true, default-features = true }
|
||||
# Pool for submitting extrinsics required by "transaction"
|
||||
array-bytes = { workspace = true, default-features = true }
|
||||
codec = { workspace = true, default-features = true }
|
||||
futures = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
hex = { workspace = true, default-features = true }
|
||||
itertools = { workspace = true }
|
||||
log = { workspace = true, default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
prometheus-endpoint = { workspace = true, default-features = true }
|
||||
rand = { workspace = true, default-features = true }
|
||||
pezsc-client-api = { workspace = true, default-features = true }
|
||||
pezsc-rpc = { workspace = true, default-features = true }
|
||||
pezsc-transaction-pool-api = { workspace = true, default-features = true }
|
||||
schnellru = { workspace = true }
|
||||
serde = { workspace = true, default-features = true }
|
||||
pezsp-api = { workspace = true, default-features = true }
|
||||
pezsp-blockchain = { workspace = true, default-features = true }
|
||||
pezsp-core = { workspace = true, default-features = true }
|
||||
pezsp-rpc = { workspace = true, default-features = true }
|
||||
pezsp-runtime = { workspace = true, default-features = true }
|
||||
pezsp-version = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { features = ["sync"], workspace = true, default-features = true }
|
||||
tokio-stream = { features = ["sync"], workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
jsonrpsee = { workspace = true, features = ["server", "ws-client"] }
|
||||
pretty_assertions = { workspace = true }
|
||||
pezsc-block-builder = { workspace = true, default-features = true }
|
||||
pezsc-rpc = { workspace = true, default-features = true, features = [
|
||||
"test-helpers",
|
||||
] }
|
||||
pezsc-service = { workspace = true, default-features = true }
|
||||
pezsc-transaction-pool = { workspace = true, default-features = true }
|
||||
pezsc-utils = { workspace = true, default-features = true }
|
||||
serde_json = { workspace = true, default-features = true }
|
||||
pezsp-consensus = { workspace = true, default-features = true }
|
||||
pezsp-externalities = { workspace = true, default-features = true }
|
||||
pezsp-maybe-compressed-blob = { workspace = true, default-features = true }
|
||||
bizinikiwi-test-runtime = { workspace = true }
|
||||
bizinikiwi-test-runtime-client = { workspace = true }
|
||||
bizinikiwi-test-runtime-transaction-pool = { workspace = true }
|
||||
tokio = { features = ["macros"], workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"pezsc-block-builder/runtime-benchmarks",
|
||||
"pezsc-chain-spec/runtime-benchmarks",
|
||||
"pezsc-client-api/runtime-benchmarks",
|
||||
"pezsc-rpc/runtime-benchmarks",
|
||||
"pezsc-service/runtime-benchmarks",
|
||||
"pezsc-transaction-pool-api/runtime-benchmarks",
|
||||
"pezsc-transaction-pool/runtime-benchmarks",
|
||||
"pezsp-api/runtime-benchmarks",
|
||||
"pezsp-blockchain/runtime-benchmarks",
|
||||
"pezsp-consensus/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
"pezsp-version/runtime-benchmarks",
|
||||
"bizinikiwi-test-runtime-client/runtime-benchmarks",
|
||||
"bizinikiwi-test-runtime-transaction-pool/runtime-benchmarks",
|
||||
"bizinikiwi-test-runtime/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
Bizinikiwi RPC interfaces.
|
||||
|
||||
A collection of RPC methods and subscriptions supported by all Bizinikiwi clients.
|
||||
|
||||
License: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
@@ -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(¶m).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(¶m).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(¶m) {
|
||||
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(¬ification.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(())
|
||||
}
|
||||
}
|
||||
@@ -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(¤t_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![¤t_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(¤t_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(¤t_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(¤t_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(¤t_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![¤t_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(¤t_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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user