feat: Rebrand Polkadot/Substrate references to PezkuwiChain

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

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

This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
2025-12-14 00:04:10 +03:00
parent 286de54384
commit 1c0e57d984
9084 changed files with 997839 additions and 997557 deletions
+121
View File
@@ -0,0 +1,121 @@
[package]
name = "pezsc-service"
version = "0.35.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 service. Starts a thread that spins up the network, client, and extrinsic pool. Manages communication between them."
readme = "README.md"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
async-trait = { workspace = true }
codec = { workspace = true, default-features = true }
directories = { workspace = true }
exit-future = { workspace = true }
futures = { workspace = true }
futures-timer = { workspace = true }
jsonrpsee = { features = ["server"], workspace = true }
log = { workspace = true, default-features = true }
parking_lot = { workspace = true, default-features = true }
pin-project = { workspace = true }
prometheus-endpoint = { workspace = true, default-features = true }
rand = { workspace = true, default-features = true }
pezsc-chain-spec = { workspace = true, default-features = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-client-db = { workspace = true }
pezsc-consensus = { workspace = true, default-features = true }
pezsc-executor = { workspace = true, default-features = true }
pezsc-informant = { workspace = true, default-features = true }
pezsc-keystore = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsc-network-common = { workspace = true, default-features = true }
pezsc-network-light = { workspace = true, default-features = true }
pezsc-network-sync = { workspace = true, default-features = true }
pezsc-network-transactions = { workspace = true, default-features = true }
pezsc-network-types = { workspace = true, default-features = true }
pezsc-rpc = { workspace = true, default-features = true }
pezsc-rpc-server = { workspace = true, default-features = true }
pezsc-rpc-spec-v2 = { workspace = true, default-features = true }
pezsc-sysinfo = { workspace = true, default-features = true }
pezsc-telemetry = { workspace = true, default-features = true }
pezsc-tracing = { workspace = true, default-features = true }
pezsc-transaction-pool = { workspace = true, default-features = true }
pezsc-transaction-pool-api = { workspace = true, default-features = true }
pezsc-utils = { workspace = true, default-features = true }
schnellru = { workspace = true }
serde = { workspace = true, default-features = true }
serde_json = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-externalities = { workspace = true, default-features = true }
pezsp-keystore = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-session = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
pezsp-storage = { workspace = true, default-features = true }
pezsp-transaction-pool = { workspace = true, default-features = true }
pezsp-transaction-storage-proof = { workspace = true, default-features = true }
pezsp-trie = { workspace = true, default-features = true }
pezsp-version = { workspace = true, default-features = true }
static_init = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
tokio = { features = [
"parking_lot",
"rt-multi-thread",
"time",
], workspace = true, default-features = true }
tracing = { workspace = true, default-features = true }
tracing-futures = { workspace = true }
[dev-dependencies]
bizinikiwi-test-runtime = { workspace = true }
bizinikiwi-test-runtime-client = { workspace = true }
[features]
default = ["rocksdb"]
# The RocksDB feature activates the RocksDB database backend. If it is not activated, and you pass
# a path to a database, an error will be produced at runtime.
rocksdb = ["pezsc-client-db/rocksdb"]
runtime-benchmarks = [
"pezsc-chain-spec/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-client-db/runtime-benchmarks",
"pezsc-consensus/runtime-benchmarks",
"pezsc-executor/runtime-benchmarks",
"pezsc-informant/runtime-benchmarks",
"pezsc-network-common/runtime-benchmarks",
"pezsc-network-light/runtime-benchmarks",
"pezsc-network-sync/runtime-benchmarks",
"pezsc-network-transactions/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsc-rpc-server/runtime-benchmarks",
"pezsc-rpc-spec-v2/runtime-benchmarks",
"pezsc-rpc/runtime-benchmarks",
"pezsc-sysinfo/runtime-benchmarks",
"pezsc-tracing/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-session/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
"pezsp-transaction-pool/runtime-benchmarks",
"pezsp-transaction-storage-proof/runtime-benchmarks",
"pezsp-trie/runtime-benchmarks",
"pezsp-version/runtime-benchmarks",
"bizinikiwi-test-runtime-client/runtime-benchmarks",
"bizinikiwi-test-runtime/runtime-benchmarks",
]
+4
View File
@@ -0,0 +1,4 @@
Bizinikiwi service. Starts a thread that spins up the network, client, and extrinsic pool.
Manages communication between them.
License: GPL-3.0-or-later WITH Classpath-exception-2.0
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,54 @@
// 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::error::Error;
use codec::Encode;
use pezsc_client_api::{BlockBackend, HeaderBackend};
use pezsc_consensus::import_queue::ImportQueue;
use pezsp_runtime::{generic::BlockId, traits::Block as BlockT};
use crate::chain_ops::import_blocks;
use std::sync::Arc;
/// Re-validate known block.
pub async fn check_block<B, IQ, C>(
client: Arc<C>,
import_queue: IQ,
block_id: BlockId<B>,
) -> Result<(), Error>
where
C: BlockBackend<B> + HeaderBackend<B> + Send + Sync + 'static,
B: BlockT + for<'de> serde::Deserialize<'de>,
IQ: ImportQueue<B> + 'static,
{
let maybe_block = client
.block_hash_from_id(&block_id)?
.map(|hash| client.block(hash))
.transpose()?
.flatten();
match maybe_block {
Some(block) => {
let mut buf = Vec::new();
1u64.encode_to(&mut buf);
block.encode_to(&mut buf);
let reader = std::io::Cursor::new(buf);
import_blocks(client, import_queue, reader, true, true).await
},
None => Err("Unknown block")?,
}
}
@@ -0,0 +1,107 @@
// 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::error::Error;
use codec::Encode;
use futures::{future, prelude::*};
use log::info;
use pezsp_runtime::{
generic::BlockId,
traits::{Block as BlockT, NumberFor, One, SaturatedConversion, Zero},
};
use pezsc_client_api::{BlockBackend, HeaderBackend, UsageProvider};
use std::{io::Write, pin::Pin, sync::Arc, task::Poll};
/// Performs the blocks export.
pub fn export_blocks<B, C>(
client: Arc<C>,
mut output: impl Write + 'static,
from: NumberFor<B>,
to: Option<NumberFor<B>>,
binary: bool,
) -> Pin<Box<dyn Future<Output = Result<(), Error>>>>
where
C: HeaderBackend<B> + BlockBackend<B> + UsageProvider<B> + 'static,
B: BlockT,
{
let mut block = from;
let last = match to {
Some(v) if v.is_zero() => One::one(),
Some(v) => v,
None => client.usage_info().chain.best_number,
};
let mut wrote_header = false;
// Exporting blocks is implemented as a future, because we want the operation to be
// interruptible.
//
// Every time we write a block to the output, the `Future` re-schedules itself and returns
// `Poll::Pending`.
// This makes it possible either to interleave other operations in-between the block exports,
// or to stop the operation completely.
let export = future::poll_fn(move |cx| {
let client = &client;
if last < block {
return Poll::Ready(Err("Invalid block range specified".into()));
}
if !wrote_header {
info!("Exporting blocks from #{} to #{}", block, last);
if binary {
let last_: u64 = last.saturated_into::<u64>();
let block_: u64 = block.saturated_into::<u64>();
let len: u64 = last_ - block_ + 1;
output.write_all(&len.encode())?;
}
wrote_header = true;
}
match client
.block_hash_from_id(&BlockId::number(block))?
.map(|hash| client.block(hash))
.transpose()?
.flatten()
{
Some(block) =>
if binary {
output.write_all(&block.encode())?;
} else {
serde_json::to_writer(&mut output, &block)
.map_err(|e| format!("Error writing JSON: {}", e))?;
},
None => return Poll::Ready(Ok(())),
}
if (block % 10000u32.into()).is_zero() {
info!("#{}", block);
}
if block == last {
return Poll::Ready(Ok(()));
}
block += One::one();
// Re-schedule the task in order to continue the operation.
cx.waker().wake_by_ref();
Poll::Pending
});
Box::pin(export)
}
@@ -0,0 +1,63 @@
// 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::error::Error;
use pezsc_client_api::{StorageProvider, UsageProvider};
use pezsp_core::storage::{well_known_keys, ChildInfo, Storage, StorageChild, StorageKey, StorageMap};
use pezsp_runtime::traits::Block as BlockT;
use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
};
/// Export the raw state at the given `block`. If `block` is `None`, the
/// best block will be used.
pub fn export_raw_state<B, BA, C>(client: Arc<C>, hash: B::Hash) -> Result<Storage, Error>
where
C: UsageProvider<B> + StorageProvider<B, BA>,
B: BlockT,
BA: pezsc_client_api::backend::Backend<B>,
{
let mut top = BTreeMap::new();
let mut children_default = HashMap::new();
for (key, value) in client.storage_pairs(hash, None, None)? {
// Remove all default child storage roots from the top storage and collect the child storage
// pairs.
if key.0.starts_with(well_known_keys::DEFAULT_CHILD_STORAGE_KEY_PREFIX) {
let child_root_key = StorageKey(
key.0[well_known_keys::DEFAULT_CHILD_STORAGE_KEY_PREFIX.len()..].to_vec(),
);
let child_info = ChildInfo::new_default(&child_root_key.0);
let mut pairs = StorageMap::new();
for child_key in client.child_storage_keys(hash, child_info.clone(), None, None)? {
if let Some(child_value) = client.child_storage(hash, &child_info, &child_key)? {
pairs.insert(child_key.0, child_value.0);
}
}
children_default.insert(child_root_key.0, StorageChild { child_info, data: pairs });
continue;
}
top.insert(key.0, value.0);
}
Ok(Storage { top, children_default })
}
@@ -0,0 +1,496 @@
// 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::{error, error::Error};
use codec::{Decode, IoReader as CodecIoReader};
use futures::{future, prelude::*};
use futures_timer::Delay;
use log::{info, warn};
use pezsc_chain_spec::ChainSpec;
use pezsc_client_api::HeaderBackend;
use pezsc_consensus::import_queue::{
BlockImportError, BlockImportStatus, ImportQueue, IncomingBlock, Link,
};
use serde_json::{de::IoRead as JsonIoRead, Deserializer, StreamDeserializer};
use pezsp_consensus::BlockOrigin;
use pezsp_runtime::{
generic::SignedBlock,
traits::{
Block as BlockT, CheckedDiv, Header, MaybeSerializeDeserialize, NumberFor, Saturating, Zero,
},
};
use std::{
io::Read,
pin::Pin,
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
},
task::Poll,
time::{Duration, Instant},
};
/// Number of blocks we will add to the queue before waiting for the queue to catch up.
const MAX_PENDING_BLOCKS: u64 = 10_000;
/// Number of milliseconds to wait until next poll.
const DELAY_TIME: u64 = 200;
/// Number of milliseconds that must have passed between two updates.
const TIME_BETWEEN_UPDATES: u64 = 3_000;
/// Build a chain spec json
pub fn build_spec(spec: &dyn ChainSpec, raw: bool) -> error::Result<String> {
spec.as_json(raw).map_err(Into::into)
}
/// Helper enum that wraps either a binary decoder (from parity-scale-codec), or a JSON decoder
/// (from serde_json). Implements the Iterator Trait, calling `next()` will decode the next
/// SignedBlock and return it.
enum BlockIter<R, B>
where
R: std::io::Read,
{
Binary {
// Total number of blocks we are expecting to decode.
num_expected_blocks: u64,
// Number of blocks we have decoded thus far.
read_block_count: u64,
// Reader to the data, used for decoding new blocks.
reader: CodecIoReader<R>,
},
Json {
// Number of blocks we have decoded thus far.
read_block_count: u64,
// Stream to the data, used for decoding new blocks.
reader: StreamDeserializer<'static, JsonIoRead<R>, SignedBlock<B>>,
},
}
impl<R, B> BlockIter<R, B>
where
R: Read + 'static,
B: BlockT + MaybeSerializeDeserialize,
{
fn new(input: R, binary: bool) -> Result<Self, String> {
if binary {
let mut reader = CodecIoReader(input);
// If the file is encoded in binary format, it is expected to first specify the number
// of blocks that are going to be decoded. We read it and add it to our enum struct.
let num_expected_blocks: u64 = Decode::decode(&mut reader)
.map_err(|e| format!("Failed to decode the number of blocks: {:?}", e))?;
Ok(BlockIter::Binary { num_expected_blocks, read_block_count: 0, reader })
} else {
let stream_deser = Deserializer::from_reader(input).into_iter::<SignedBlock<B>>();
Ok(BlockIter::Json { reader: stream_deser, read_block_count: 0 })
}
}
/// Returns the number of blocks read thus far.
fn read_block_count(&self) -> u64 {
match self {
BlockIter::Binary { read_block_count, .. } |
BlockIter::Json { read_block_count, .. } => *read_block_count,
}
}
/// Returns the total number of blocks to be imported, if possible.
fn num_expected_blocks(&self) -> Option<u64> {
match self {
BlockIter::Binary { num_expected_blocks, .. } => Some(*num_expected_blocks),
BlockIter::Json { .. } => None,
}
}
}
impl<R, B> Iterator for BlockIter<R, B>
where
R: Read + 'static,
B: BlockT + MaybeSerializeDeserialize,
{
type Item = Result<SignedBlock<B>, String>;
fn next(&mut self) -> Option<Self::Item> {
match self {
BlockIter::Binary { num_expected_blocks, read_block_count, reader } => {
if read_block_count < num_expected_blocks {
let block_result: Result<SignedBlock<B>, _> =
SignedBlock::<B>::decode(reader).map_err(|e| e.to_string());
*read_block_count += 1;
Some(block_result)
} else {
// `read_block_count` == `num_expected_blocks` so we've read enough blocks.
None
}
},
BlockIter::Json { reader, read_block_count } => {
let res = Some(reader.next()?.map_err(|e| e.to_string()));
*read_block_count += 1;
res
},
}
}
}
/// Imports the SignedBlock to the queue.
fn import_block_to_queue<TBl, TImpQu>(
signed_block: SignedBlock<TBl>,
queue: &mut TImpQu,
force: bool,
) where
TBl: BlockT + MaybeSerializeDeserialize,
TImpQu: 'static + ImportQueue<TBl>,
{
let (header, extrinsics) = signed_block.block.deconstruct();
let hash = header.hash();
// import queue handles verification and importing it into the client.
queue.service_ref().import_blocks(
BlockOrigin::File,
vec![IncomingBlock::<TBl> {
hash,
header: Some(header),
body: Some(extrinsics),
indexed_body: None,
justifications: signed_block.justifications,
origin: None,
allow_missing_state: false,
import_existing: force,
state: None,
skip_execution: false,
}],
);
}
/// Returns true if we have imported every block we were supposed to import, else returns false.
fn importing_is_done(
num_expected_blocks: Option<u64>,
read_block_count: u64,
imported_blocks: u64,
) -> bool {
if let Some(num_expected_blocks) = num_expected_blocks {
imported_blocks >= num_expected_blocks
} else {
imported_blocks >= read_block_count
}
}
/// Structure used to log the block importing speed.
struct Speedometer<B: BlockT> {
best_number: NumberFor<B>,
last_number: Option<NumberFor<B>>,
last_update: Instant,
}
impl<B: BlockT> Speedometer<B> {
/// Creates a fresh Speedometer.
fn new() -> Self {
Self {
best_number: NumberFor::<B>::from(0u32),
last_number: None,
last_update: Instant::now(),
}
}
/// Calculates `(best_number - last_number) / (now - last_update)` and
/// logs the speed of import.
fn display_speed(&self) {
// Number of milliseconds elapsed since last time.
let elapsed_ms = {
let elapsed = self.last_update.elapsed();
let since_last_millis = elapsed.as_secs() * 1000;
let since_last_subsec_millis = elapsed.subsec_millis() as u64;
since_last_millis + since_last_subsec_millis
};
// Number of blocks that have been imported since last time.
let diff = match self.last_number {
None => return,
Some(n) => self.best_number.saturating_sub(n),
};
if let Ok(diff) = TryInto::<u128>::try_into(diff) {
// If the number of blocks can be converted to a regular integer, then it's easy: just
// do the math and turn it into a `f64`.
let speed = diff
.saturating_mul(10_000)
.checked_div(u128::from(elapsed_ms))
.map_or(0.0, |s| s as f64) /
10.0;
info!("📦 Current best block: {} ({:4.1} bps)", self.best_number, speed);
} else {
// If the number of blocks can't be converted to a regular integer, then we need a more
// algebraic approach and we stay within the realm of integers.
let one_thousand = NumberFor::<B>::from(1_000u32);
let elapsed =
NumberFor::<B>::from(<u32 as TryFrom<_>>::try_from(elapsed_ms).unwrap_or(u32::MAX));
let speed = diff
.saturating_mul(one_thousand)
.checked_div(&elapsed)
.unwrap_or_else(Zero::zero);
info!("📦 Current best block: {} ({} bps)", self.best_number, speed)
}
}
/// Updates the Speedometer.
fn update(&mut self, best_number: NumberFor<B>) {
self.last_number = Some(self.best_number);
self.best_number = best_number;
self.last_update = Instant::now();
}
// If more than TIME_BETWEEN_UPDATES has elapsed since last update,
// then print and update the speedometer.
fn notify_user(&mut self, best_number: NumberFor<B>) {
let delta = Duration::from_millis(TIME_BETWEEN_UPDATES);
if Instant::now().duration_since(self.last_update) >= delta {
self.display_speed();
self.update(best_number);
}
}
}
/// Different State that the `import_blocks` future could be in.
enum ImportState<R, B>
where
R: Read + 'static,
B: BlockT + MaybeSerializeDeserialize,
{
/// We are reading from the [`BlockIter`] structure, adding those blocks to the queue if
/// possible.
Reading { block_iter: BlockIter<R, B> },
/// The queue is full (contains at least MAX_PENDING_BLOCKS blocks) and we are waiting for it
/// to catch up.
WaitingForImportQueueToCatchUp {
block_iter: BlockIter<R, B>,
delay: Delay,
block: SignedBlock<B>,
},
// We have added all the blocks to the queue but they are still being processed.
WaitingForImportQueueToFinish {
num_expected_blocks: Option<u64>,
read_block_count: u64,
delay: Delay,
},
}
/// Starts the process of importing blocks.
pub fn import_blocks<B, IQ, C>(
client: Arc<C>,
mut import_queue: IQ,
input: impl Read + Send + 'static,
force: bool,
binary: bool,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>
where
C: HeaderBackend<B> + Send + Sync + 'static,
B: BlockT + for<'de> serde::Deserialize<'de>,
IQ: ImportQueue<B> + 'static,
{
struct WaitLink {
imported_blocks: AtomicU64,
has_error: AtomicBool,
}
impl WaitLink {
fn new() -> WaitLink {
WaitLink { imported_blocks: AtomicU64::new(0), has_error: AtomicBool::new(false) }
}
}
impl<B: BlockT> Link<B> for WaitLink {
fn blocks_processed(
&self,
imported: usize,
_num_expected_blocks: usize,
results: Vec<(Result<BlockImportStatus<NumberFor<B>>, BlockImportError>, B::Hash)>,
) {
self.imported_blocks.fetch_add(imported as u64, Ordering::AcqRel);
for result in results {
if let (Err(err), hash) = result {
warn!("There was an error importing block with hash {:?}: {}", hash, err);
self.has_error.store(true, Ordering::Release);
break;
}
}
}
}
let mut link = WaitLink::new();
let block_iter_res: Result<BlockIter<_, B>, String> = BlockIter::new(input, binary);
let block_iter = match block_iter_res {
Ok(block_iter) => block_iter,
Err(e) => {
// We've encountered an error while creating the block iterator
// so we can just return a future that returns an error.
return future::ready(Err(Error::Other(e))).boxed();
},
};
let mut state = Some(ImportState::Reading { block_iter });
let mut speedometer = Speedometer::<B>::new();
// Importing blocks is implemented as a future, because we want the operation to be
// interruptible.
//
// Every time we read a block from the input or import a bunch of blocks from the import
// queue, the `Future` re-schedules itself and returns `Poll::Pending`.
// This makes it possible either to interleave other operations in-between the block imports,
// or to stop the operation completely.
let import = future::poll_fn(move |cx| {
let client = &client;
let queue = &mut import_queue;
match state.take().expect("state should never be None; qed") {
ImportState::Reading { mut block_iter } => {
match block_iter.next() {
None => {
// The iterator is over: we now need to wait for the import queue to finish.
let num_expected_blocks = block_iter.num_expected_blocks();
let read_block_count = block_iter.read_block_count();
let delay = Delay::new(Duration::from_millis(DELAY_TIME));
state = Some(ImportState::WaitingForImportQueueToFinish {
num_expected_blocks,
read_block_count,
delay,
});
},
Some(block_result) => {
let read_block_count = block_iter.read_block_count();
match block_result {
Ok(block) => {
if read_block_count - link.imported_blocks.load(Ordering::Acquire) >=
MAX_PENDING_BLOCKS
{
// The queue is full, so do not add this block and simply wait
// until the queue has made some progress.
let delay = Delay::new(Duration::from_millis(DELAY_TIME));
state = Some(ImportState::WaitingForImportQueueToCatchUp {
block_iter,
delay,
block,
});
} else {
// Queue is not full, we can keep on adding blocks to the queue.
import_block_to_queue(block, queue, force);
state = Some(ImportState::Reading { block_iter });
}
},
Err(e) =>
return Poll::Ready(Err(Error::Other(format!(
"Error reading block #{}: {}",
read_block_count, e
)))),
}
},
}
},
ImportState::WaitingForImportQueueToCatchUp { block_iter, mut delay, block } => {
let read_block_count = block_iter.read_block_count();
if read_block_count - link.imported_blocks.load(Ordering::Acquire) >=
MAX_PENDING_BLOCKS
{
// Queue is still full, so wait until there is room to insert our block.
match Pin::new(&mut delay).poll(cx) {
Poll::Pending => {
state = Some(ImportState::WaitingForImportQueueToCatchUp {
block_iter,
delay,
block,
});
return Poll::Pending;
},
Poll::Ready(_) => {
delay.reset(Duration::from_millis(DELAY_TIME));
},
}
state = Some(ImportState::WaitingForImportQueueToCatchUp {
block_iter,
delay,
block,
});
} else {
// Queue is no longer full, so we can add our block to the queue.
import_block_to_queue(block, queue, force);
// Switch back to Reading state.
state = Some(ImportState::Reading { block_iter });
}
},
ImportState::WaitingForImportQueueToFinish {
num_expected_blocks,
read_block_count,
mut delay,
} => {
// All the blocks have been added to the queue, which doesn't mean they
// have all been properly imported.
if importing_is_done(
num_expected_blocks,
read_block_count,
link.imported_blocks.load(Ordering::Acquire),
) {
// Importing is done, we can log the result and return.
info!(
"🎉 Imported {} blocks. Best: #{}",
read_block_count,
client.info().best_number
);
return Poll::Ready(Ok(()));
} else {
// Importing is not done, we still have to wait for the queue to finish.
// Wait for the delay, because we know the queue is lagging behind.
match Pin::new(&mut delay).poll(cx) {
Poll::Pending => {
state = Some(ImportState::WaitingForImportQueueToFinish {
num_expected_blocks,
read_block_count,
delay,
});
return Poll::Pending;
},
Poll::Ready(_) => {
delay.reset(Duration::from_millis(DELAY_TIME));
},
}
state = Some(ImportState::WaitingForImportQueueToFinish {
num_expected_blocks,
read_block_count,
delay,
});
}
},
}
queue.poll_actions(cx, &mut link);
let best_number = client.info().best_number;
speedometer.notify_user(best_number);
if link.has_error.load(Ordering::Acquire) {
return Poll::Ready(Err(Error::Other(format!(
"Stopping after #{} blocks because of an error",
link.imported_blocks.load(Ordering::Acquire)
))));
}
cx.waker().wake_by_ref();
Poll::Pending
});
Box::pin(import)
}
@@ -0,0 +1,31 @@
// 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/>.
//! Chain utilities.
mod check_block;
mod export_blocks;
mod export_raw_state;
mod import_blocks;
mod revert_chain;
pub use check_block::*;
pub use export_blocks::*;
pub use export_raw_state::*;
pub use import_blocks::*;
pub use revert_chain::*;
@@ -0,0 +1,52 @@
// 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::error::Error;
use log::info;
use pezsc_client_api::{Backend, UsageProvider};
use pezsp_runtime::traits::{Block as BlockT, NumberFor, Zero};
use std::sync::Arc;
/// Performs a revert of `blocks` blocks.
pub fn revert_chain<B, BA, C>(
client: Arc<C>,
backend: Arc<BA>,
blocks: NumberFor<B>,
) -> Result<(), Error>
where
B: BlockT,
C: UsageProvider<B>,
BA: Backend<B>,
{
let reverted = backend.revert(blocks, false)?;
let info = client.usage_info().chain;
if reverted.0.is_zero() {
info!("There aren't any non-finalized blocks to revert.");
} else {
info!("Reverted {} blocks. Best: #{} ({})", reverted.0, info.best_number, info.best_hash);
if reverted.0 > blocks {
info!(
"Number of reverted blocks is higher than requested \
because of reverted leaves higher than the best block."
)
}
}
Ok(())
}
@@ -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/>.
//! Client fixed chain specification rules
use std::collections::{HashMap, HashSet};
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
use pezsc_client_api::{BadBlocks, ForkBlocks};
/// Chain specification rules lookup result.
pub enum LookupResult<B: BlockT> {
/// Specification rules do not contain any special rules about this block
NotSpecial,
/// The block is known to be bad and should not be imported
KnownBad,
/// There is a specified canonical block hash for the given height
Expected(B::Hash),
}
/// Chain-specific block filtering rules.
///
/// This holds known bad blocks and known good forks, and
/// is usually part of the chain spec.
pub struct BlockRules<B: BlockT> {
bad: HashSet<B::Hash>,
forks: HashMap<NumberFor<B>, B::Hash>,
}
impl<B: BlockT> BlockRules<B> {
/// New block rules with provided black and white lists.
pub fn new(fork_blocks: ForkBlocks<B>, bad_blocks: BadBlocks<B>) -> Self {
Self {
bad: bad_blocks.unwrap_or_default(),
forks: fork_blocks.unwrap_or_default().into_iter().collect(),
}
}
/// Mark a new block as bad.
pub fn mark_bad(&mut self, hash: B::Hash) {
self.bad.insert(hash);
}
/// Check if there's any rule affecting the given block.
pub fn lookup(&self, number: NumberFor<B>, hash: &B::Hash) -> LookupResult<B> {
if let Some(hash_for_height) = self.forks.get(&number) {
if hash_for_height != hash {
return LookupResult::Expected(*hash_for_height);
}
}
if self.bad.contains(hash) {
return LookupResult::KnownBad;
}
LookupResult::NotSpecial
}
}
@@ -0,0 +1,268 @@
// 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::{code_provider::CodeProvider, ClientConfig};
use pezsc_client_api::{
backend, call_executor::CallExecutor, execution_extensions::ExecutionExtensions, HeaderBackend,
TrieCacheContext,
};
use pezsc_executor::{RuntimeVersion, RuntimeVersionOf};
use pezsp_api::ProofRecorder;
use pezsp_core::traits::{CallContext, CodeExecutor};
use pezsp_externalities::Extensions;
use pezsp_runtime::{
generic::BlockId,
traits::{Block as BlockT, HashingFor},
};
use pezsp_state_machine::{backend::AsTrieBackend, OverlayedChanges, StateMachine, StorageProof};
use std::{cell::RefCell, sync::Arc};
/// Call executor that executes methods locally, querying all required
/// data from local backend.
pub struct LocalCallExecutor<Block: BlockT, B, E> {
backend: Arc<B>,
executor: E,
code_provider: CodeProvider<Block, B, E>,
execution_extensions: Arc<ExecutionExtensions<Block>>,
}
impl<Block: BlockT, B, E> LocalCallExecutor<Block, B, E>
where
E: CodeExecutor + RuntimeVersionOf + Clone + 'static,
B: backend::Backend<Block>,
{
/// Creates new instance of local call executor.
pub fn new(
backend: Arc<B>,
executor: E,
client_config: ClientConfig<Block>,
execution_extensions: ExecutionExtensions<Block>,
) -> pezsp_blockchain::Result<Self> {
let code_provider = CodeProvider::new(&client_config, executor.clone(), backend.clone())?;
Ok(LocalCallExecutor {
backend,
executor,
code_provider,
execution_extensions: Arc::new(execution_extensions),
})
}
}
impl<Block: BlockT, B, E> Clone for LocalCallExecutor<Block, B, E>
where
E: Clone,
{
fn clone(&self) -> Self {
LocalCallExecutor {
backend: self.backend.clone(),
executor: self.executor.clone(),
code_provider: self.code_provider.clone(),
execution_extensions: self.execution_extensions.clone(),
}
}
}
impl<B, E, Block> CallExecutor<Block> for LocalCallExecutor<Block, B, E>
where
B: backend::Backend<Block>,
E: CodeExecutor + RuntimeVersionOf + Clone + 'static,
Block: BlockT,
{
type Error = E::Error;
type Backend = B;
fn execution_extensions(&self) -> &ExecutionExtensions<Block> {
&self.execution_extensions
}
fn call(
&self,
at_hash: Block::Hash,
method: &str,
call_data: &[u8],
context: CallContext,
) -> pezsp_blockchain::Result<Vec<u8>> {
let mut changes = OverlayedChanges::default();
let at_number =
self.backend.blockchain().expect_block_number_from_id(&BlockId::Hash(at_hash))?;
let state = self.backend.state_at(at_hash, context.into())?;
let state_runtime_code = pezsp_state_machine::backend::BackendRuntimeCode::new(&state);
let runtime_code =
state_runtime_code.runtime_code().map_err(pezsp_blockchain::Error::RuntimeCode)?;
let runtime_code = self.code_provider.maybe_override_code(runtime_code, &state, at_hash)?.0;
let mut extensions = self.execution_extensions.extensions(at_hash, at_number);
let mut sm = StateMachine::new(
&state,
&mut changes,
&self.executor,
method,
call_data,
&mut extensions,
&runtime_code,
context,
)
.set_parent_hash(at_hash);
sm.execute().map_err(Into::into)
}
fn contextual_call(
&self,
at_hash: Block::Hash,
method: &str,
call_data: &[u8],
changes: &RefCell<OverlayedChanges<HashingFor<Block>>>,
recorder: &Option<ProofRecorder<Block>>,
call_context: CallContext,
extensions: &RefCell<Extensions>,
) -> Result<Vec<u8>, pezsp_blockchain::Error> {
let state = self.backend.state_at(at_hash, call_context.into())?;
let changes = &mut *changes.borrow_mut();
// It is important to extract the runtime code here before we create the proof
// recorder to not record it. We also need to fetch the runtime code from `state` to
// make sure we use the caching layers.
let state_runtime_code = pezsp_state_machine::backend::BackendRuntimeCode::new(&state);
let runtime_code =
state_runtime_code.runtime_code().map_err(pezsp_blockchain::Error::RuntimeCode)?;
let runtime_code = self.code_provider.maybe_override_code(runtime_code, &state, at_hash)?.0;
let mut extensions = extensions.borrow_mut();
match recorder {
Some(recorder) => {
let trie_state = state.as_trie_backend();
let backend = pezsp_state_machine::TrieBackendBuilder::wrap(&trie_state)
.with_recorder(recorder.clone())
.build();
let mut state_machine = StateMachine::new(
&backend,
changes,
&self.executor,
method,
call_data,
&mut extensions,
&runtime_code,
call_context,
)
.set_parent_hash(at_hash);
state_machine.execute()
},
None => {
let mut state_machine = StateMachine::new(
&state,
changes,
&self.executor,
method,
call_data,
&mut extensions,
&runtime_code,
call_context,
)
.set_parent_hash(at_hash);
state_machine.execute()
},
}
.map_err(Into::into)
}
fn runtime_version(&self, at_hash: Block::Hash) -> pezsp_blockchain::Result<RuntimeVersion> {
let state = self.backend.state_at(at_hash, backend::TrieCacheContext::Untrusted)?;
let state_runtime_code = pezsp_state_machine::backend::BackendRuntimeCode::new(&state);
let runtime_code =
state_runtime_code.runtime_code().map_err(pezsp_blockchain::Error::RuntimeCode)?;
self.code_provider
.maybe_override_code(runtime_code, &state, at_hash)
.map(|(_, v)| v)
}
fn prove_execution(
&self,
at_hash: Block::Hash,
method: &str,
call_data: &[u8],
) -> pezsp_blockchain::Result<(Vec<u8>, StorageProof)> {
let at_number =
self.backend.blockchain().expect_block_number_from_id(&BlockId::Hash(at_hash))?;
let state = self.backend.state_at(at_hash, TrieCacheContext::Untrusted)?;
let trie_backend = state.as_trie_backend();
let state_runtime_code = pezsp_state_machine::backend::BackendRuntimeCode::new(trie_backend);
let runtime_code =
state_runtime_code.runtime_code().map_err(pezsp_blockchain::Error::RuntimeCode)?;
let runtime_code = self.code_provider.maybe_override_code(runtime_code, &state, at_hash)?.0;
pezsp_state_machine::prove_execution_on_trie_backend(
trie_backend,
&mut Default::default(),
&self.executor,
method,
call_data,
&runtime_code,
&mut self.execution_extensions.extensions(at_hash, at_number),
)
.map_err(Into::into)
}
}
impl<B, E, Block> RuntimeVersionOf for LocalCallExecutor<Block, B, E>
where
E: RuntimeVersionOf,
Block: BlockT,
{
fn runtime_version(
&self,
ext: &mut dyn pezsp_externalities::Externalities,
runtime_code: &pezsp_core::traits::RuntimeCode,
) -> Result<pezsp_version::RuntimeVersion, pezsc_executor::error::Error> {
RuntimeVersionOf::runtime_version(&self.executor, ext, runtime_code)
}
}
impl<Block, B, E> pezsp_version::GetRuntimeVersionAt<Block> for LocalCallExecutor<Block, B, E>
where
B: backend::Backend<Block>,
E: CodeExecutor + RuntimeVersionOf + Clone + 'static,
Block: BlockT,
{
fn runtime_version(&self, at: Block::Hash) -> Result<pezsp_version::RuntimeVersion, String> {
CallExecutor::runtime_version(self, at).map_err(|e| e.to_string())
}
}
impl<Block, B, E> pezsp_version::GetNativeVersion for LocalCallExecutor<Block, B, E>
where
B: backend::Backend<Block>,
E: CodeExecutor + pezsp_version::GetNativeVersion + Clone + 'static,
Block: BlockT,
{
fn native_version(&self) -> &pezsp_version::NativeVersion {
self.executor.native_version()
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,352 @@
// 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::{client::ClientConfig, wasm_override::WasmOverride, wasm_substitutes::WasmSubstitutes};
use pezsc_client_api::{backend, TrieCacheContext};
use pezsc_executor::{RuntimeVersion, RuntimeVersionOf};
use pezsp_core::traits::{FetchRuntimeCode, RuntimeCode};
use pezsp_runtime::traits::Block as BlockT;
use pezsp_state_machine::{Ext, OverlayedChanges};
use std::sync::Arc;
/// Provider for fetching `:code` of a block.
///
/// As a node can run with code overrides or substitutes, this will ensure that these are taken into
/// account before returning the actual `code` for a block.
pub struct CodeProvider<Block: BlockT, Backend, Executor> {
backend: Arc<Backend>,
executor: Arc<Executor>,
wasm_override: Arc<Option<WasmOverride>>,
wasm_substitutes: WasmSubstitutes<Block, Executor, Backend>,
}
impl<Block: BlockT, Backend, Executor: Clone> Clone for CodeProvider<Block, Backend, Executor> {
fn clone(&self) -> Self {
Self {
backend: self.backend.clone(),
executor: self.executor.clone(),
wasm_override: self.wasm_override.clone(),
wasm_substitutes: self.wasm_substitutes.clone(),
}
}
}
impl<Block, Backend, Executor> CodeProvider<Block, Backend, Executor>
where
Block: BlockT,
Backend: backend::Backend<Block>,
Executor: RuntimeVersionOf,
{
/// Create a new instance.
pub fn new(
client_config: &ClientConfig<Block>,
executor: Executor,
backend: Arc<Backend>,
) -> pezsp_blockchain::Result<Self> {
let wasm_override = client_config
.wasm_runtime_overrides
.as_ref()
.map(|p| WasmOverride::new(p.clone(), &executor))
.transpose()?;
let executor = Arc::new(executor);
let wasm_substitutes = WasmSubstitutes::new(
client_config.wasm_runtime_substitutes.clone(),
executor.clone(),
backend.clone(),
)?;
Ok(Self { backend, executor, wasm_override: Arc::new(wasm_override), wasm_substitutes })
}
/// Returns the `:code` for the given `block`.
///
/// This takes into account potential overrides/substitutes.
pub fn code_at_ignoring_overrides(&self, block: Block::Hash) -> pezsp_blockchain::Result<Vec<u8>> {
let state = self.backend.state_at(block, TrieCacheContext::Untrusted)?;
let state_runtime_code = pezsp_state_machine::backend::BackendRuntimeCode::new(&state);
let runtime_code =
state_runtime_code.runtime_code().map_err(pezsp_blockchain::Error::RuntimeCode)?;
self.maybe_override_code_internal(runtime_code, &state, block, true)
.and_then(|r| {
r.0.fetch_runtime_code().map(Into::into).ok_or_else(|| {
pezsp_blockchain::Error::Backend("Could not find `:code` in backend.".into())
})
})
}
/// Maybe override the given `onchain_code`.
///
/// This takes into account potential overrides/substitutes.
pub fn maybe_override_code<'a>(
&'a self,
onchain_code: RuntimeCode<'a>,
state: &Backend::State,
hash: Block::Hash,
) -> pezsp_blockchain::Result<(RuntimeCode<'a>, RuntimeVersion)> {
self.maybe_override_code_internal(onchain_code, state, hash, false)
}
/// Maybe override the given `onchain_code`.
///
/// This takes into account potential overrides(depending on `ignore_overrides`)/substitutes.
fn maybe_override_code_internal<'a>(
&'a self,
onchain_code: RuntimeCode<'a>,
state: &Backend::State,
hash: Block::Hash,
ignore_overrides: bool,
) -> pezsp_blockchain::Result<(RuntimeCode<'a>, RuntimeVersion)> {
let on_chain_version = self.on_chain_runtime_version(&onchain_code, state)?;
let code_and_version = if let Some(d) = self.wasm_override.as_ref().as_ref().and_then(|o| {
if ignore_overrides {
return None;
}
o.get(
&on_chain_version.spec_version,
onchain_code.heap_pages,
&on_chain_version.spec_name,
)
}) {
tracing::debug!(target: "code-provider::overrides", block = ?hash, "using WASM override");
d
} else if let Some(s) =
self.wasm_substitutes
.get(on_chain_version.spec_version, onchain_code.heap_pages, hash)
{
tracing::debug!(target: "code-provider::substitutes", block = ?hash, "Using WASM substitute");
s
} else {
tracing::debug!(
target: "code-provider",
block = ?hash,
"Neither WASM override nor substitute available, using onchain code",
);
(onchain_code, on_chain_version)
};
Ok(code_and_version)
}
/// Returns the on chain runtime version.
fn on_chain_runtime_version(
&self,
code: &RuntimeCode,
state: &Backend::State,
) -> pezsp_blockchain::Result<RuntimeVersion> {
let mut overlay = OverlayedChanges::default();
let mut ext = Ext::new(&mut overlay, state, None);
self.executor
.runtime_version(&mut ext, code)
.map_err(|e| pezsp_blockchain::Error::VersionInvalid(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use backend::Backend;
use pezsc_client_api::{in_mem, HeaderBackend};
use pezsc_executor::WasmExecutor;
use pezsp_core::{
testing::TaskExecutor,
traits::{FetchRuntimeCode, WrappedRuntimeCode},
};
use std::collections::HashMap;
use bizinikiwi_test_runtime_client::{runtime, GenesisInit};
#[test]
fn no_override_no_substitutes_work() {
let executor = WasmExecutor::default();
let code_fetcher = WrappedRuntimeCode(bizinikiwi_test_runtime::wasm_binary_unwrap().into());
let onchain_code = RuntimeCode {
code_fetcher: &code_fetcher,
heap_pages: Some(128),
hash: vec![0, 0, 0, 0],
};
let backend = Arc::new(in_mem::Backend::<runtime::Block>::new());
// wasm_runtime_overrides is `None` here because we construct the
// LocalCallExecutor directly later on
let client_config = ClientConfig::default();
let genesis_block_builder = crate::GenesisBlockBuilder::new(
&bizinikiwi_test_runtime_client::GenesisParameters::default().genesis_storage(),
!client_config.no_genesis,
backend.clone(),
executor.clone(),
)
.expect("Creates genesis block builder");
// client is used for the convenience of creating and inserting the genesis block.
let _client =
crate::client::new_with_backend::<_, _, runtime::Block, _, runtime::RuntimeApi>(
backend.clone(),
executor.clone(),
genesis_block_builder,
Box::new(TaskExecutor::new()),
None,
None,
client_config.clone(),
)
.expect("Creates a client");
let executor = Arc::new(executor);
let code_provider = CodeProvider {
backend: backend.clone(),
executor: executor.clone(),
wasm_override: Arc::new(None),
wasm_substitutes: WasmSubstitutes::new(Default::default(), executor, backend.clone())
.unwrap(),
};
let check = code_provider
.maybe_override_code(
onchain_code,
&backend
.state_at(backend.blockchain().info().genesis_hash, TrieCacheContext::Untrusted)
.unwrap(),
backend.blockchain().info().genesis_hash,
)
.expect("RuntimeCode override")
.0;
assert_eq!(code_fetcher.fetch_runtime_code(), check.fetch_runtime_code());
}
#[test]
fn should_get_override_if_exists() {
let executor = WasmExecutor::default();
let overrides = crate::client::wasm_override::dummy_overrides();
let onchain_code = WrappedRuntimeCode(bizinikiwi_test_runtime::wasm_binary_unwrap().into());
let onchain_code = RuntimeCode {
code_fetcher: &onchain_code,
heap_pages: Some(128),
hash: vec![0, 0, 0, 0],
};
let backend = Arc::new(in_mem::Backend::<runtime::Block>::new());
// wasm_runtime_overrides is `None` here because we construct the
// LocalCallExecutor directly later on
let client_config = ClientConfig::default();
let genesis_block_builder = crate::GenesisBlockBuilder::new(
&bizinikiwi_test_runtime_client::GenesisParameters::default().genesis_storage(),
!client_config.no_genesis,
backend.clone(),
executor.clone(),
)
.expect("Creates genesis block builder");
// client is used for the convenience of creating and inserting the genesis block.
let _client =
crate::client::new_with_backend::<_, _, runtime::Block, _, runtime::RuntimeApi>(
backend.clone(),
executor.clone(),
genesis_block_builder,
Box::new(TaskExecutor::new()),
None,
None,
client_config.clone(),
)
.expect("Creates a client");
let executor = Arc::new(executor);
let code_provider = CodeProvider {
backend: backend.clone(),
executor: executor.clone(),
wasm_override: Arc::new(Some(overrides)),
wasm_substitutes: WasmSubstitutes::new(Default::default(), executor, backend.clone())
.unwrap(),
};
let check = code_provider
.maybe_override_code(
onchain_code,
&backend
.state_at(backend.blockchain().info().genesis_hash, TrieCacheContext::Untrusted)
.unwrap(),
backend.blockchain().info().genesis_hash,
)
.expect("RuntimeCode override")
.0;
assert_eq!(Some(vec![2, 2, 2, 2, 2, 2, 2, 2]), check.fetch_runtime_code().map(Into::into));
}
#[test]
fn returns_runtime_version_from_substitute() {
const SUBSTITUTE_SPEC_NAME: &str = "substitute-spec-name-cool";
let executor = WasmExecutor::default();
let backend = Arc::new(in_mem::Backend::<runtime::Block>::new());
// Let's only override the `spec_name` for our testing purposes.
let substitute = pezsp_version::embed::embed_runtime_version(
&bizinikiwi_test_runtime::WASM_BINARY_BLOATY.unwrap(),
pezsp_version::RuntimeVersion {
spec_name: SUBSTITUTE_SPEC_NAME.into(),
..bizinikiwi_test_runtime::VERSION
},
)
.unwrap();
let client_config = crate::client::ClientConfig {
wasm_runtime_substitutes: vec![(0, substitute)].into_iter().collect::<HashMap<_, _>>(),
..Default::default()
};
let genesis_block_builder = crate::GenesisBlockBuilder::new(
&bizinikiwi_test_runtime_client::GenesisParameters::default().genesis_storage(),
!client_config.no_genesis,
backend.clone(),
executor.clone(),
)
.expect("Creates genesis block builder");
// client is used for the convenience of creating and inserting the genesis block.
let client =
crate::client::new_with_backend::<_, _, runtime::Block, _, runtime::RuntimeApi>(
backend.clone(),
executor.clone(),
genesis_block_builder,
Box::new(TaskExecutor::new()),
None,
None,
client_config,
)
.expect("Creates a client");
let version = client.runtime_version_at(client.chain_info().genesis_hash).unwrap();
assert_eq!(SUBSTITUTE_SPEC_NAME, &*version.spec_name);
}
}
@@ -0,0 +1,59 @@
// 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 Client and associated logic.
//!
//! The [`Client`] is one of the most important components of Bizinikiwi. It mainly comprises two
//! parts:
//!
//! - A database containing the blocks and chain state, generally referred to as
//! the [`Backend`](pezsc_client_api::backend::Backend).
//! - A runtime environment, generally referred to as the
//! [`Executor`](pezsc_client_api::call_executor::CallExecutor).
//!
//! # Initialization
//!
//! Creating a [`Client`] is done by calling the `new` method and passing to it a
//! [`Backend`](pezsc_client_api::backend::Backend) and an
//! [`Executor`](pezsc_client_api::call_executor::CallExecutor).
//!
//! The former is typically provided by the `sc-client-db` crate.
//!
//! The latter typically requires passing one of:
//!
//! - A [`LocalCallExecutor`] running the runtime locally.
//! - A `RemoteCallExecutor` that will ask a third-party to perform the executions.
//! - A `RemoteOrLocalCallExecutor` combination of the two.
//!
//! Additionally, the fourth generic parameter of the `Client` is a marker type representing
//! the ways in which the runtime can interface with the outside. Any code that builds a `Client`
//! is responsible for putting the right marker.
mod block_rules;
mod call_executor;
mod client;
mod code_provider;
mod notification_pinning;
mod wasm_override;
mod wasm_substitutes;
pub use call_executor::LocalCallExecutor;
pub use client::{Client, ClientConfig};
pub(crate) use code_provider::CodeProvider;
pub use self::client::new_with_backend;
@@ -0,0 +1,353 @@
// 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/>.
//! Notification pinning related logic.
//!
//! This file contains a worker that should be started when a new client instance is created.
//! The goal is to avoid pruning of blocks that have active notifications in the node. Every
//! recipient of notifications should receive the chance to act upon them. In addition, notification
//! listeners can hold onto a [`pezsc_client_api::UnpinHandle`] to keep a block pinned. Once the handle
//! is dropped, a message is sent and the worker unpins the respective block.
use std::{
marker::PhantomData,
sync::{Arc, Weak},
};
use futures::StreamExt;
use pezsc_client_api::{Backend, UnpinWorkerMessage};
use pezsc_utils::mpsc::TracingUnboundedReceiver;
use schnellru::Limiter;
use pezsp_runtime::traits::Block as BlockT;
const LOG_TARGET: &str = "db::notification_pinning";
const NOTIFICATION_PINNING_LIMIT: usize = 1024;
/// A limiter which automatically unpins blocks that leave the data structure.
#[derive(Clone, Debug)]
struct UnpinningByLengthLimiter<Block: BlockT, B: Backend<Block>> {
max_length: usize,
backend: Weak<B>,
_phantom: PhantomData<Block>,
}
impl<Block: BlockT, B: Backend<Block>> UnpinningByLengthLimiter<Block, B> {
/// Creates a new length limiter with a given `max_length`.
pub fn new(max_length: usize, backend: Weak<B>) -> UnpinningByLengthLimiter<Block, B> {
UnpinningByLengthLimiter { max_length, backend, _phantom: PhantomData::<Block>::default() }
}
}
impl<Block: BlockT, B: Backend<Block>> Limiter<Block::Hash, u32>
for UnpinningByLengthLimiter<Block, B>
{
type KeyToInsert<'a> = Block::Hash;
type LinkType = usize;
fn is_over_the_limit(&self, length: usize) -> bool {
length > self.max_length
}
fn on_insert(
&mut self,
_length: usize,
key: Self::KeyToInsert<'_>,
value: u32,
) -> Option<(Block::Hash, u32)> {
log::debug!(target: LOG_TARGET, "Pinning block based on notification. hash = {key}");
if self.max_length > 0 {
Some((key, value))
} else {
None
}
}
fn on_replace(
&mut self,
_length: usize,
_old_key: &mut Block::Hash,
_new_key: Block::Hash,
_old_value: &mut u32,
_new_value: &mut u32,
) -> bool {
true
}
fn on_removed(&mut self, key: &mut Block::Hash, references: &mut u32) {
// If reference count was larger than 0 on removal,
// the item was removed due to capacity limitations.
// Since the cache should be large enough for pinned items,
// we want to know about these evictions.
if *references > 0 {
log::warn!(
target: LOG_TARGET,
"Notification block pinning limit reached. Unpinning block with hash = {key:?}"
);
if let Some(backend) = self.backend.upgrade() {
(0..*references).for_each(|_| backend.unpin_block(*key));
}
} else {
log::trace!(
target: LOG_TARGET,
"Unpinned block. hash = {key:?}",
)
}
}
fn on_cleared(&mut self) {}
fn on_grow(&mut self, _new_memory_usage: usize) -> bool {
true
}
}
/// Worker for the handling of notification pinning.
///
/// It receives messages from a receiver and pins/unpins based on the incoming messages.
/// All notification related unpinning should go through this worker. If the maximum number of
/// notification pins is reached, the block from the oldest notification is unpinned.
pub struct NotificationPinningWorker<Block: BlockT, Back: Backend<Block>> {
unpin_message_rx: TracingUnboundedReceiver<UnpinWorkerMessage<Block>>,
task_backend: Weak<Back>,
pinned_blocks: schnellru::LruMap<Block::Hash, u32, UnpinningByLengthLimiter<Block, Back>>,
}
impl<Block: BlockT, Back: Backend<Block>> NotificationPinningWorker<Block, Back> {
/// Creates a new `NotificationPinningWorker`.
pub fn new(
unpin_message_rx: TracingUnboundedReceiver<UnpinWorkerMessage<Block>>,
task_backend: Arc<Back>,
) -> Self {
let pinned_blocks =
schnellru::LruMap::<Block::Hash, u32, UnpinningByLengthLimiter<Block, Back>>::new(
UnpinningByLengthLimiter::new(
NOTIFICATION_PINNING_LIMIT,
Arc::downgrade(&task_backend),
),
);
Self { unpin_message_rx, task_backend: Arc::downgrade(&task_backend), pinned_blocks }
}
fn handle_announce_message(&mut self, hash: Block::Hash) {
if let Some(entry) = self.pinned_blocks.get_or_insert(hash, Default::default) {
*entry = *entry + 1;
}
}
fn handle_unpin_message(&mut self, hash: Block::Hash) -> Result<(), ()> {
if let Some(refcount) = self.pinned_blocks.peek_mut(&hash) {
*refcount = *refcount - 1;
if *refcount == 0 {
self.pinned_blocks.remove(&hash);
}
if let Some(backend) = self.task_backend.upgrade() {
log::debug!(target: LOG_TARGET, "Reducing pinning refcount for block hash = {hash:?}");
backend.unpin_block(hash);
} else {
log::debug!(target: LOG_TARGET, "Terminating unpin-worker, backend reference was dropped.");
return Err(());
}
} else {
log::debug!(target: LOG_TARGET, "Received unpin message for already unpinned block. hash = {hash:?}");
}
Ok(())
}
/// Start working on the received messages.
///
/// The worker maintains a map which keeps track of the pinned blocks and their reference count.
/// Depending upon the received message, it acts to pin/unpin the block.
pub async fn run(mut self) {
while let Some(message) = self.unpin_message_rx.next().await {
match message {
UnpinWorkerMessage::AnnouncePin(hash) => self.handle_announce_message(hash),
UnpinWorkerMessage::Unpin(hash) =>
if self.handle_unpin_message(hash).is_err() {
return;
},
}
}
log::debug!(target: LOG_TARGET, "Terminating unpin-worker, stream terminated.")
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use pezsc_client_api::{Backend, UnpinWorkerMessage};
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver};
use pezsp_core::H256;
use pezsp_runtime::traits::Block as BlockT;
type Block = bizinikiwi_test_runtime_client::runtime::Block;
use super::{NotificationPinningWorker, UnpinningByLengthLimiter};
impl<Block: BlockT, Back: Backend<Block>> NotificationPinningWorker<Block, Back> {
fn new_with_limit(
unpin_message_rx: TracingUnboundedReceiver<UnpinWorkerMessage<Block>>,
task_backend: Arc<Back>,
limit: usize,
) -> Self {
let pinned_blocks =
schnellru::LruMap::<Block::Hash, u32, UnpinningByLengthLimiter<Block, Back>>::new(
UnpinningByLengthLimiter::new(limit, Arc::downgrade(&task_backend)),
);
Self { unpin_message_rx, task_backend: Arc::downgrade(&task_backend), pinned_blocks }
}
fn lru(
&self,
) -> &schnellru::LruMap<Block::Hash, u32, UnpinningByLengthLimiter<Block, Back>> {
&self.pinned_blocks
}
}
#[test]
fn pinning_worker_handles_base_case() {
let (_tx, rx) = tracing_unbounded("testing", 1000);
let backend = Arc::new(pezsc_client_api::in_mem::Backend::<Block>::new());
let hash = H256::random();
let mut worker = NotificationPinningWorker::new(rx, backend.clone());
// Block got pinned and unpin message should unpin in the backend.
let _ = backend.pin_block(hash);
assert_eq!(backend.pin_refs(&hash), Some(1));
worker.handle_announce_message(hash);
assert_eq!(worker.lru().len(), 1);
let _ = worker.handle_unpin_message(hash);
assert_eq!(backend.pin_refs(&hash), Some(0));
assert!(worker.lru().is_empty());
}
#[test]
fn pinning_worker_handles_multiple_pins() {
let (_tx, rx) = tracing_unbounded("testing", 1000);
let backend = Arc::new(pezsc_client_api::in_mem::Backend::<Block>::new());
let hash = H256::random();
let mut worker = NotificationPinningWorker::new(rx, backend.clone());
// Block got pinned multiple times.
let _ = backend.pin_block(hash);
let _ = backend.pin_block(hash);
let _ = backend.pin_block(hash);
assert_eq!(backend.pin_refs(&hash), Some(3));
worker.handle_announce_message(hash);
worker.handle_announce_message(hash);
worker.handle_announce_message(hash);
assert_eq!(worker.lru().len(), 1);
let _ = worker.handle_unpin_message(hash);
assert_eq!(backend.pin_refs(&hash), Some(2));
let _ = worker.handle_unpin_message(hash);
assert_eq!(backend.pin_refs(&hash), Some(1));
let _ = worker.handle_unpin_message(hash);
assert_eq!(backend.pin_refs(&hash), Some(0));
assert!(worker.lru().is_empty());
let _ = worker.handle_unpin_message(hash);
assert_eq!(backend.pin_refs(&hash), Some(0));
}
#[test]
fn pinning_worker_handles_too_many_unpins() {
let (_tx, rx) = tracing_unbounded("testing", 1000);
let backend = Arc::new(pezsc_client_api::in_mem::Backend::<Block>::new());
let hash = H256::random();
let hash2 = H256::random();
let mut worker = NotificationPinningWorker::new(rx, backend.clone());
// Block was announced once but unpinned multiple times. The worker should ignore the
// additional unpins.
let _ = backend.pin_block(hash);
let _ = backend.pin_block(hash);
let _ = backend.pin_block(hash);
assert_eq!(backend.pin_refs(&hash), Some(3));
worker.handle_announce_message(hash);
assert_eq!(worker.lru().len(), 1);
let _ = worker.handle_unpin_message(hash);
assert_eq!(backend.pin_refs(&hash), Some(2));
let _ = worker.handle_unpin_message(hash);
assert_eq!(backend.pin_refs(&hash), Some(2));
assert!(worker.lru().is_empty());
let _ = worker.handle_unpin_message(hash2);
assert!(worker.lru().is_empty());
assert_eq!(backend.pin_refs(&hash2), None);
}
#[test]
fn pinning_worker_should_evict_when_limit_reached() {
let (_tx, rx) = tracing_unbounded("testing", 1000);
let backend = Arc::new(pezsc_client_api::in_mem::Backend::<Block>::new());
let hash1 = H256::random();
let hash2 = H256::random();
let hash3 = H256::random();
let hash4 = H256::random();
// Only two items fit into the cache.
let mut worker = NotificationPinningWorker::new_with_limit(rx, backend.clone(), 2);
// Multiple blocks are announced but the cache size is too small. We expect that blocks
// are evicted by the cache and unpinned in the backend.
let _ = backend.pin_block(hash1);
let _ = backend.pin_block(hash2);
let _ = backend.pin_block(hash3);
assert_eq!(backend.pin_refs(&hash1), Some(1));
assert_eq!(backend.pin_refs(&hash2), Some(1));
assert_eq!(backend.pin_refs(&hash3), Some(1));
worker.handle_announce_message(hash1);
assert!(worker.lru().peek(&hash1).is_some());
worker.handle_announce_message(hash2);
assert!(worker.lru().peek(&hash2).is_some());
worker.handle_announce_message(hash3);
assert!(worker.lru().peek(&hash3).is_some());
assert!(worker.lru().peek(&hash2).is_some());
assert_eq!(worker.lru().len(), 2);
// Hash 1 should have gotten unpinned, since its oldest.
assert_eq!(backend.pin_refs(&hash1), Some(0));
assert_eq!(backend.pin_refs(&hash2), Some(1));
assert_eq!(backend.pin_refs(&hash3), Some(1));
// Hash 2 is getting bumped.
worker.handle_announce_message(hash2);
assert_eq!(worker.lru().peek(&hash2), Some(&2));
// Since hash 2 was accessed, evict hash 3.
worker.handle_announce_message(hash4);
assert_eq!(worker.lru().peek(&hash3), None);
}
}
@@ -0,0 +1,347 @@
// 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/>.
//! # WASM Local Blob-Override
//!
//! WASM Local blob override provides tools to replace on-chain WASM with custom WASM.
//! These customized WASM blobs may include functionality that is not included in the
//! on-chain WASM, such as tracing or debugging information. This extra information is especially
//! useful in external scenarios, like exchanges or archive nodes.
//!
//! ## Usage
//!
//! WASM overrides may be enabled with the `--wasm-runtime-overrides` argument. The argument
//! expects a path to a directory that holds custom WASM.
//!
//! Any file ending in '.wasm' will be scraped and instantiated as a WASM blob. WASM can be built by
//! compiling the required runtime with the changes needed. For example, compiling a runtime with
//! tracing enabled would produce a WASM blob that can used.
//!
//! A custom WASM blob will override on-chain WASM if the spec version matches. If it is
//! required to overrides multiple runtimes, multiple WASM blobs matching each of the spec versions
//! needed must be provided in the given directory.
use pezsc_executor::RuntimeVersionOf;
use pezsp_blockchain::Result;
use pezsp_core::traits::{FetchRuntimeCode, RuntimeCode, WrappedRuntimeCode};
use pezsp_state_machine::BasicExternalities;
use pezsp_version::RuntimeVersion;
use std::{
collections::{hash_map::DefaultHasher, HashMap},
fs,
hash::Hasher as _,
path::{Path, PathBuf},
time::{Duration, Instant},
};
/// The interval in that we will print a warning when a wasm blob `spec_name`
/// doesn't match with the on-chain `spec_name`.
const WARN_INTERVAL: Duration = Duration::from_secs(30);
/// Auxiliary structure that holds a wasm blob and its hash.
#[derive(Debug)]
struct WasmBlob {
/// The actual wasm blob, aka the code.
code: Vec<u8>,
/// The hash of [`Self::code`].
hash: Vec<u8>,
/// The path where this blob was found.
path: PathBuf,
/// The runtime version of this blob.
version: RuntimeVersion,
/// When was the last time we have warned about the wasm blob having
/// a wrong `spec_name`?
last_warn: parking_lot::Mutex<Option<Instant>>,
}
impl WasmBlob {
fn new(code: Vec<u8>, hash: Vec<u8>, path: PathBuf, version: RuntimeVersion) -> Self {
Self { code, hash, path, version, last_warn: Default::default() }
}
fn runtime_code(&self, heap_pages: Option<u64>) -> RuntimeCode<'_> {
RuntimeCode { code_fetcher: self, hash: self.hash.clone(), heap_pages }
}
}
/// Make a hash out of a byte string using the default rust hasher
fn make_hash<K: std::hash::Hash + ?Sized>(val: &K) -> Vec<u8> {
let mut state = DefaultHasher::new();
val.hash(&mut state);
state.finish().to_le_bytes().to_vec()
}
impl FetchRuntimeCode for WasmBlob {
fn fetch_runtime_code(&self) -> Option<std::borrow::Cow<'_, [u8]>> {
Some(self.code.as_slice().into())
}
}
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum WasmOverrideError {
#[error("Failed to get runtime version: {0}")]
VersionInvalid(String),
#[error("WASM override IO error")]
Io(PathBuf, #[source] std::io::Error),
#[error("Overwriting WASM requires a directory where local \
WASM is stored. {} is not a directory", .0.display())]
NotADirectory(PathBuf),
#[error("Duplicate WASM Runtimes found: \n{}\n", .0.join("\n") )]
DuplicateRuntime(Vec<String>),
}
impl From<WasmOverrideError> for pezsp_blockchain::Error {
fn from(err: WasmOverrideError) -> Self {
Self::Application(Box::new(err))
}
}
/// Scrapes WASM from a folder and returns WASM from that folder
/// if the runtime spec version matches.
#[derive(Debug)]
pub struct WasmOverride {
// Map of runtime spec version -> Wasm Blob
overrides: HashMap<u32, WasmBlob>,
}
impl WasmOverride {
pub fn new<P, E>(path: P, executor: &E) -> Result<Self>
where
P: AsRef<Path>,
E: RuntimeVersionOf,
{
let overrides = Self::scrape_overrides(path.as_ref(), executor)?;
Ok(Self { overrides })
}
/// Gets an override by it's runtime spec version.
///
/// Returns `None` if an override for a spec version does not exist.
pub fn get<'a, 'b: 'a>(
&'b self,
spec: &u32,
pages: Option<u64>,
spec_name: &str,
) -> Option<(RuntimeCode<'a>, RuntimeVersion)> {
self.overrides.get(spec).and_then(|w| {
if spec_name == &*w.version.spec_name {
Some((w.runtime_code(pages), w.version.clone()))
} else {
let mut last_warn = w.last_warn.lock();
let now = Instant::now();
if last_warn.map_or(true, |l| l + WARN_INTERVAL <= now) {
*last_warn = Some(now);
tracing::warn!(
target = "wasm_overrides",
on_chain_spec_name = %spec_name,
override_spec_name = %w.version,
spec_version = %spec,
wasm_file = %w.path.display(),
"On chain and override `spec_name` do not match! Ignoring override.",
);
}
None
}
})
}
/// Scrapes a folder for WASM runtimes.
/// Returns a hashmap of the runtime version and wasm runtime code.
fn scrape_overrides<E>(dir: &Path, executor: &E) -> Result<HashMap<u32, WasmBlob>>
where
E: RuntimeVersionOf,
{
let handle_err = |e: std::io::Error| -> pezsp_blockchain::Error {
WasmOverrideError::Io(dir.to_owned(), e).into()
};
if !dir.is_dir() {
return Err(WasmOverrideError::NotADirectory(dir.to_owned()).into());
}
let mut overrides = HashMap::new();
let mut duplicates = Vec::new();
for entry in fs::read_dir(dir).map_err(handle_err)? {
let entry = entry.map_err(handle_err)?;
let path = entry.path();
if let Some("wasm") = path.extension().and_then(|e| e.to_str()) {
let code = fs::read(&path).map_err(handle_err)?;
let code_hash = make_hash(&code);
let version = Self::runtime_version(executor, &code, &code_hash, Some(128))?;
tracing::info!(
target: "wasm_overrides",
version = %version,
file = %path.display(),
"Found wasm override.",
);
let wasm = WasmBlob::new(code, code_hash, path.clone(), version.clone());
if let Some(other) = overrides.insert(version.spec_version, wasm) {
tracing::info!(
target: "wasm_overrides",
first = %other.path.display(),
second = %path.display(),
%version,
"Found duplicate spec version for runtime.",
);
duplicates.push(path.display().to_string());
}
}
}
if !duplicates.is_empty() {
return Err(WasmOverrideError::DuplicateRuntime(duplicates).into());
}
Ok(overrides)
}
fn runtime_version<E>(
executor: &E,
code: &[u8],
code_hash: &[u8],
heap_pages: Option<u64>,
) -> Result<RuntimeVersion>
where
E: RuntimeVersionOf,
{
let mut ext = BasicExternalities::default();
executor
.runtime_version(
&mut ext,
&RuntimeCode {
code_fetcher: &WrappedRuntimeCode(code.into()),
heap_pages,
hash: code_hash.into(),
},
)
.map_err(|e| WasmOverrideError::VersionInvalid(e.to_string()).into())
}
}
/// Returns a WasmOverride struct filled with dummy data for testing.
#[cfg(test)]
pub fn dummy_overrides() -> WasmOverride {
let version = RuntimeVersion { spec_name: "test".into(), ..Default::default() };
let mut overrides = HashMap::new();
overrides.insert(
0,
WasmBlob::new(vec![0, 0, 0, 0, 0, 0, 0, 0], vec![0], PathBuf::new(), version.clone()),
);
overrides.insert(
1,
WasmBlob::new(vec![1, 1, 1, 1, 1, 1, 1, 1], vec![1], PathBuf::new(), version.clone()),
);
overrides
.insert(2, WasmBlob::new(vec![2, 2, 2, 2, 2, 2, 2, 2], vec![2], PathBuf::new(), version));
WasmOverride { overrides }
}
#[cfg(test)]
mod tests {
use super::*;
use pezsc_executor::{HeapAllocStrategy, WasmExecutor};
use std::fs::{self, File};
fn executor() -> WasmExecutor {
WasmExecutor::builder()
.with_onchain_heap_alloc_strategy(HeapAllocStrategy::Static { extra_pages: 128 })
.with_offchain_heap_alloc_strategy(HeapAllocStrategy::Static { extra_pages: 128 })
.with_max_runtime_instances(1)
.with_runtime_cache_size(2)
.build()
}
fn wasm_test<F>(fun: F)
where
F: Fn(&Path, &[u8], &WasmExecutor),
{
let exec = executor();
let bytes = bizinikiwi_test_runtime::wasm_binary_unwrap();
let dir = tempfile::tempdir().expect("Create a temporary directory");
fun(dir.path(), bytes, &exec);
dir.close().expect("Temporary Directory should close");
}
#[test]
fn should_get_runtime_version() {
let executor = executor();
let version = WasmOverride::runtime_version(
&executor,
bizinikiwi_test_runtime::wasm_binary_unwrap(),
&[1],
Some(128),
)
.expect("should get the `RuntimeVersion` of the test-runtime wasm blob");
assert_eq!(version.spec_version, 2);
}
#[test]
fn should_scrape_wasm() {
wasm_test(|dir, wasm_bytes, exec| {
fs::write(dir.join("test.wasm"), wasm_bytes).expect("Create test file");
let overrides =
WasmOverride::scrape_overrides(dir, exec).expect("HashMap of u32 and WasmBlob");
let wasm = overrides.get(&2).expect("WASM binary");
assert_eq!(wasm.code, bizinikiwi_test_runtime::wasm_binary_unwrap().to_vec())
});
}
#[test]
fn should_check_for_duplicates() {
wasm_test(|dir, wasm_bytes, exec| {
fs::write(dir.join("test0.wasm"), wasm_bytes).expect("Create test file");
fs::write(dir.join("test1.wasm"), wasm_bytes).expect("Create test file");
let scraped = WasmOverride::scrape_overrides(dir, exec);
match scraped {
Err(pezsp_blockchain::Error::Application(e)) => {
match e.downcast_ref::<WasmOverrideError>() {
Some(WasmOverrideError::DuplicateRuntime(duplicates)) => {
assert_eq!(duplicates.len(), 1);
},
_ => panic!("Test should end with Msg Error Variant"),
}
},
_ => panic!("Test should end in error"),
}
});
}
#[test]
fn should_ignore_non_wasm() {
wasm_test(|dir, wasm_bytes, exec| {
File::create(dir.join("README.md")).expect("Create test file");
File::create(dir.join("LICENSE")).expect("Create a test file");
fs::write(dir.join("test0.wasm"), wasm_bytes).expect("Create test file");
let scraped =
WasmOverride::scrape_overrides(dir, exec).expect("HashMap of u32 and WasmBlob");
assert_eq!(scraped.len(), 1);
});
}
}
@@ -0,0 +1,163 @@
// 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/>.
//! # WASM substitutes
use pezsc_client_api::backend;
use pezsc_executor::RuntimeVersionOf;
use pezsp_blockchain::{HeaderBackend, Result};
use pezsp_core::traits::{FetchRuntimeCode, RuntimeCode, WrappedRuntimeCode};
use pezsp_runtime::traits::{Block as BlockT, NumberFor};
use pezsp_state_machine::BasicExternalities;
use pezsp_version::RuntimeVersion;
use std::{
collections::{hash_map::DefaultHasher, HashMap},
hash::Hasher as _,
sync::Arc,
};
/// A wasm substitute for the on chain wasm.
#[derive(Debug)]
struct WasmSubstitute<Block: BlockT> {
code: Vec<u8>,
hash: Vec<u8>,
/// The block number on which we should start using the substitute.
block_number: NumberFor<Block>,
version: RuntimeVersion,
}
impl<Block: BlockT> WasmSubstitute<Block> {
fn new(code: Vec<u8>, block_number: NumberFor<Block>, version: RuntimeVersion) -> Self {
let hash = make_hash(&code);
Self { code, hash, block_number, version }
}
fn runtime_code(&self, heap_pages: Option<u64>) -> RuntimeCode<'_> {
RuntimeCode { code_fetcher: self, hash: self.hash.clone(), heap_pages }
}
/// Returns `true` when the substitute matches for the given `hash`.
fn matches(
&self,
hash: <Block as BlockT>::Hash,
backend: &impl backend::Backend<Block>,
) -> bool {
let requested_block_number = backend.blockchain().number(hash).ok().flatten();
Some(self.block_number) <= requested_block_number
}
}
/// Make a hash out of a byte string using the default rust hasher
fn make_hash<K: std::hash::Hash + ?Sized>(val: &K) -> Vec<u8> {
let mut state = DefaultHasher::new();
val.hash(&mut state);
state.finish().to_le_bytes().to_vec()
}
impl<Block: BlockT> FetchRuntimeCode for WasmSubstitute<Block> {
fn fetch_runtime_code(&self) -> Option<std::borrow::Cow<'_, [u8]>> {
Some(self.code.as_slice().into())
}
}
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum WasmSubstituteError {
#[error("Failed to get runtime version: {0}")]
VersionInvalid(String),
}
impl From<WasmSubstituteError> for pezsp_blockchain::Error {
fn from(err: WasmSubstituteError) -> Self {
Self::Application(Box::new(err))
}
}
/// Substitutes the on-chain wasm with some hard coded blobs.
#[derive(Debug)]
pub struct WasmSubstitutes<Block: BlockT, Executor, Backend> {
/// spec_version -> WasmSubstitute
substitutes: Arc<HashMap<u32, WasmSubstitute<Block>>>,
executor: Arc<Executor>,
backend: Arc<Backend>,
}
impl<Block: BlockT, Executor: Clone, Backend> Clone for WasmSubstitutes<Block, Executor, Backend> {
fn clone(&self) -> Self {
Self {
substitutes: self.substitutes.clone(),
executor: self.executor.clone(),
backend: self.backend.clone(),
}
}
}
impl<Executor, Backend, Block> WasmSubstitutes<Block, Executor, Backend>
where
Executor: RuntimeVersionOf,
Backend: backend::Backend<Block>,
Block: BlockT,
{
/// Create a new instance.
pub fn new(
substitutes: HashMap<NumberFor<Block>, Vec<u8>>,
executor: Arc<Executor>,
backend: Arc<Backend>,
) -> Result<Self> {
let substitutes = substitutes
.into_iter()
.map(|(block_number, code)| {
let runtime_code = RuntimeCode {
code_fetcher: &WrappedRuntimeCode((&code).into()),
heap_pages: None,
hash: make_hash(&code),
};
let version = Self::runtime_version(&executor, &runtime_code)?;
let spec_version = version.spec_version;
let substitute = WasmSubstitute::new(code, block_number, version);
Ok((spec_version, substitute))
})
.collect::<Result<HashMap<_, _>>>()?;
Ok(Self { executor, substitutes: Arc::new(substitutes), backend })
}
/// Get a substitute.
///
/// Returns `None` if there isn't any substitute required.
pub fn get(
&self,
spec: u32,
pages: Option<u64>,
hash: Block::Hash,
) -> Option<(RuntimeCode<'_>, RuntimeVersion)> {
let s = self.substitutes.get(&spec)?;
s.matches(hash, &*self.backend)
.then(|| (s.runtime_code(pages), s.version.clone()))
}
fn runtime_version(executor: &Executor, code: &RuntimeCode) -> Result<RuntimeVersion> {
let mut ext = BasicExternalities::default();
executor
.runtime_version(&mut ext, code)
.map_err(|e| WasmSubstituteError::VersionInvalid(e.to_string()).into())
}
}
+371
View File
@@ -0,0 +1,371 @@
// 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/>.
//! Service configuration.
pub use jsonrpsee::server::BatchRequestConfig as RpcBatchRequestConfig;
use prometheus_endpoint::Registry;
use pezsc_chain_spec::ChainSpec;
pub use pezsc_client_db::{BlocksPruning, Database, DatabaseSource, PruningMode};
pub use pezsc_executor::{WasmExecutionMethod, WasmtimeInstantiationStrategy};
pub use pezsc_network::{
config::{
MultiaddrWithPeerId, NetworkConfiguration, NodeKeyConfig, NonDefaultSetConfig, ProtocolId,
Role, SetConfig, SyncMode, TransportConfig,
},
request_responses::{
IncomingRequest, OutgoingResponse, ProtocolConfig as RequestResponseConfig,
},
Multiaddr,
};
pub use pezsc_rpc_server::{
IpNetwork, RpcEndpoint, RpcMethods, SubscriptionIdProvider as RpcSubscriptionIdProvider,
};
pub use pezsc_telemetry::TelemetryEndpoints;
pub use pezsc_transaction_pool::TransactionPoolOptions;
use pezsp_core::crypto::SecretString;
use std::{
io, iter,
net::SocketAddr,
num::NonZeroU32,
path::{Path, PathBuf},
};
use tempfile::TempDir;
/// Service configuration.
#[derive(Debug)]
pub struct Configuration {
/// Implementation name
pub impl_name: String,
/// Implementation version (see sc-cli to see an example of format)
pub impl_version: String,
/// Node role.
pub role: Role,
/// Handle to the tokio runtime. Will be used to spawn futures by the task manager.
pub tokio_handle: tokio::runtime::Handle,
/// Extrinsic pool configuration.
pub transaction_pool: TransactionPoolOptions,
/// Network configuration.
pub network: NetworkConfiguration,
/// Configuration for the keystore.
pub keystore: KeystoreConfig,
/// Configuration for the database.
pub database: DatabaseSource,
/// Maximum size of internal trie cache in bytes.
///
/// If `None` is given the cache is disabled.
pub trie_cache_maximum_size: Option<usize>,
/// Force the trie cache to be in memory.
pub warm_up_trie_cache: Option<TrieCacheWarmUpStrategy>,
/// State pruning settings.
pub state_pruning: Option<PruningMode>,
/// Number of blocks to keep in the db.
///
/// NOTE: only finalized blocks are subject for removal!
pub blocks_pruning: BlocksPruning,
/// Chain configuration.
pub chain_spec: Box<dyn ChainSpec>,
/// Runtime executor configuration.
pub executor: ExecutorConfiguration,
/// Directory where local WASM runtimes live. These runtimes take precedence
/// over on-chain runtimes when the spec version matches. Set to `None` to
/// disable overrides (default).
pub wasm_runtime_overrides: Option<PathBuf>,
/// RPC configuration.
pub rpc: RpcConfiguration,
/// Prometheus endpoint configuration. `None` if disabled.
pub prometheus_config: Option<PrometheusConfig>,
/// Telemetry service URL. `None` if disabled.
pub telemetry_endpoints: Option<TelemetryEndpoints>,
/// Should offchain workers be executed.
pub offchain_worker: OffchainWorkerConfig,
/// Enable authoring even when offline.
pub force_authoring: bool,
/// Disable GRANDPA when running in validator mode
pub disable_grandpa: bool,
/// Development key seed.
///
/// When running in development mode, the seed will be used to generate authority keys by the
/// keystore.
///
/// Should only be set when `node` is running development mode.
pub dev_key_seed: Option<String>,
/// Tracing targets
pub tracing_targets: Option<String>,
/// Tracing receiver
pub tracing_receiver: pezsc_tracing::TracingReceiver,
/// Announce block automatically after they have been imported
pub announce_block: bool,
/// Data path root for the configured chain.
pub data_path: PathBuf,
/// Base path of the configuration. This is shared between chains.
pub base_path: BasePath,
}
/// Warmup strategy for the trie cache.
#[derive(Debug, Clone, Copy)]
pub enum TrieCacheWarmUpStrategy {
/// Warm up the cache in a non-blocking way.
NonBlocking,
/// Warm up the cache in a blocking way.
Blocking,
}
impl TrieCacheWarmUpStrategy {
/// Returns true if the warmup strategy is blocking.
pub(crate) fn is_blocking(&self) -> bool {
matches!(self, Self::Blocking)
}
}
/// Type for tasks spawned by the executor.
#[derive(PartialEq)]
pub enum TaskType {
/// Regular non-blocking futures. Polling the task is expected to be a lightweight operation.
Async,
/// The task might perform a lot of expensive CPU operations and/or call `thread::sleep`.
Blocking,
}
/// Configuration of the client keystore.
#[derive(Debug, Clone)]
pub enum KeystoreConfig {
/// Keystore at a path on-disk. Recommended for native nodes.
Path {
/// The path of the keystore.
path: PathBuf,
/// Node keystore's password.
password: Option<SecretString>,
},
/// In-memory keystore. Recommended for in-browser nodes.
InMemory,
}
impl KeystoreConfig {
/// Returns the path for the keystore.
pub fn path(&self) -> Option<&Path> {
match self {
Self::Path { path, .. } => Some(path),
Self::InMemory => None,
}
}
}
/// Configuration of the database of the client.
#[derive(Debug, Clone, Default)]
pub struct OffchainWorkerConfig {
/// If this is allowed.
pub enabled: bool,
/// allow writes from the runtime to the offchain worker database.
pub indexing_enabled: bool,
}
/// Configuration of the Prometheus endpoint.
#[derive(Debug, Clone)]
pub struct PrometheusConfig {
/// Port to use.
pub port: SocketAddr,
/// A metrics registry to use. Useful for setting the metric prefix.
pub registry: Registry,
}
impl PrometheusConfig {
/// Create a new config using the default registry.
pub fn new_with_default_registry(port: SocketAddr, chain_id: String) -> Self {
let param = iter::once((String::from("chain"), chain_id)).collect();
Self {
port,
registry: Registry::new_custom(None, Some(param))
.expect("this can only fail if the prefix is empty"),
}
}
}
impl Configuration {
/// Returns a string displaying the node role.
pub fn display_role(&self) -> String {
self.role.to_string()
}
/// Returns the prometheus metrics registry, if available.
pub fn prometheus_registry(&self) -> Option<&Registry> {
self.prometheus_config.as_ref().map(|config| &config.registry)
}
/// Returns the network protocol id from the chain spec, or the default.
pub fn protocol_id(&self) -> ProtocolId {
let protocol_id_full = match self.chain_spec.protocol_id() {
Some(pid) => pid,
None => {
log::warn!(
"Using default protocol ID {:?} because none is configured in the \
chain specs",
crate::DEFAULT_PROTOCOL_ID
);
crate::DEFAULT_PROTOCOL_ID
},
};
ProtocolId::from(protocol_id_full)
}
/// Returns true if the genesis state writing will be skipped while initializing the genesis
/// block.
pub fn no_genesis(&self) -> bool {
matches!(self.network.sync_mode, SyncMode::LightState { .. } | SyncMode::Warp { .. })
}
/// Returns the database config for creating the backend.
pub fn db_config(&self) -> pezsc_client_db::DatabaseSettings {
pezsc_client_db::DatabaseSettings {
trie_cache_maximum_size: self.trie_cache_maximum_size,
state_pruning: self.state_pruning.clone(),
source: self.database.clone(),
blocks_pruning: self.blocks_pruning,
metrics_registry: self.prometheus_registry().cloned(),
}
}
}
#[static_init::dynamic(drop, lazy)]
static mut BASE_PATH_TEMP: Option<TempDir> = None;
/// The base path that is used for everything that needs to be written on disk to run a node.
#[derive(Clone, Debug)]
pub struct BasePath {
path: PathBuf,
}
impl BasePath {
/// Create a `BasePath` instance using a temporary directory prefixed with "bizinikiwi" and use
/// it as base path.
///
/// Note: The temporary directory will be created automatically and deleted when the program
/// exits. Every call to this function will return the same path for the lifetime of the
/// program.
pub fn new_temp_dir() -> io::Result<BasePath> {
let mut temp = BASE_PATH_TEMP.write();
match &*temp {
Some(p) => Ok(Self::new(p.path())),
None => {
let temp_dir = tempfile::Builder::new().prefix("bizinikiwi").tempdir()?;
let path = PathBuf::from(temp_dir.path());
*temp = Some(temp_dir);
Ok(Self::new(path))
},
}
}
/// Create a `BasePath` instance based on an existing path on disk.
///
/// Note: this function will not ensure that the directory exist nor create the directory. It
/// will also not delete the directory when the instance is dropped.
pub fn new<P: Into<PathBuf>>(path: P) -> BasePath {
Self { path: path.into() }
}
/// Create a base path from values describing the project.
pub fn from_project(qualifier: &str, organization: &str, application: &str) -> BasePath {
BasePath::new(
directories::ProjectDirs::from(qualifier, organization, application)
.expect("app directories exist on all supported platforms; qed")
.data_local_dir(),
)
}
/// Retrieve the base path.
pub fn path(&self) -> &Path {
&self.path
}
/// Returns the configuration directory inside this base path.
///
/// The path looks like `$base_path/chains/$chain_id`
pub fn config_dir(&self, chain_id: &str) -> PathBuf {
self.path().join("chains").join(chain_id)
}
}
impl From<PathBuf> for BasePath {
fn from(path: PathBuf) -> Self {
BasePath::new(path)
}
}
/// RPC configuration.
#[derive(Debug)]
pub struct RpcConfiguration {
/// JSON-RPC server endpoints.
pub addr: Option<Vec<RpcEndpoint>>,
/// Maximum number of connections for JSON-RPC server.
pub max_connections: u32,
/// CORS settings for HTTP & WS servers. `None` if all origins are allowed.
pub cors: Option<Vec<String>>,
/// RPC methods to expose (by default only a safe subset or all of them).
pub methods: RpcMethods,
/// Maximum payload of a rpc request
pub max_request_size: u32,
/// Maximum payload of a rpc response.
pub max_response_size: u32,
/// Custom JSON-RPC subscription ID provider.
///
/// Default: [`crate::RandomStringSubscriptionId`].
pub id_provider: Option<Box<dyn RpcSubscriptionIdProvider>>,
/// Maximum allowed subscriptions per rpc connection
pub max_subs_per_conn: u32,
/// JSON-RPC server default port.
pub port: u16,
/// The number of messages the JSON-RPC server is allowed to keep in memory.
pub message_buffer_capacity: u32,
/// JSON-RPC server batch config.
pub batch_config: RpcBatchRequestConfig,
/// RPC rate limit per minute.
pub rate_limit: Option<NonZeroU32>,
/// RPC rate limit whitelisted ip addresses.
pub rate_limit_whitelisted_ips: Vec<IpNetwork>,
/// RPC rate limit trust proxy headers.
pub rate_limit_trust_proxy_headers: bool,
/// RPC logger capacity (default: 1024).
pub request_logger_limit: u32,
}
/// Runtime executor configuration.
#[derive(Debug, Clone)]
pub struct ExecutorConfiguration {
/// Wasm execution method.
pub wasm_method: WasmExecutionMethod,
/// The size of the instances cache.
///
/// The default value is 8.
pub max_runtime_instances: usize,
/// The default number of 64KB pages to allocate for Wasm execution
pub default_heap_pages: Option<u64>,
/// Maximum number of different runtime versions that can be cached.
pub runtime_cache_size: u8,
}
impl Default for ExecutorConfiguration {
fn default() -> Self {
Self {
wasm_method: WasmExecutionMethod::default(),
max_runtime_instances: 8,
default_heap_pages: None,
runtime_cache_size: 2,
}
}
}
+77
View File
@@ -0,0 +1,77 @@
// 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/>.
//! Errors that can occur during the service operation.
use pezsc_keystore;
use pezsp_blockchain;
use pezsp_consensus;
/// Service Result typedef.
pub type Result<T> = std::result::Result<T, Error>;
/// Service errors.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Client(#[from] pezsp_blockchain::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Consensus(#[from] pezsp_consensus::Error),
#[error(transparent)]
Network(#[from] pezsc_network::error::Error),
#[error(transparent)]
Keystore(#[from] pezsc_keystore::Error),
#[error(transparent)]
Telemetry(#[from] pezsc_telemetry::Error),
#[error("Best chain selection strategy (SelectChain) is not provided.")]
SelectChainRequired,
#[error("Tasks executor hasn't been provided.")]
TaskExecutorRequired,
#[error("Prometheus metrics error: {0}")]
Prometheus(#[from] prometheus_endpoint::PrometheusError),
#[error("Application: {0}")]
Application(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("Other: {0}")]
Other(String),
}
impl<'a> From<&'a str> for Error {
fn from(s: &'a str) -> Self {
Error::Other(s.into())
}
}
impl From<String> for Error {
fn from(s: String) -> Self {
Error::Other(s)
}
}
+620
View File
@@ -0,0 +1,620 @@
// 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 service. Starts a thread that spins up the network, client, and extrinsic pool.
//! Manages communication between them.
#![warn(missing_docs)]
#![recursion_limit = "1024"]
pub mod chain_ops;
pub mod client;
pub mod config;
pub mod error;
mod builder;
mod metrics;
mod task_manager;
use crate::config::Multiaddr;
use std::{
collections::HashMap,
net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
};
use codec::{Decode, Encode};
use futures::{pin_mut, FutureExt, StreamExt};
use jsonrpsee::RpcModule;
use log::{debug, error, trace, warn};
use pezsc_client_api::{blockchain::HeaderBackend, BlockBackend, BlockchainEvents, ProofProvider};
use pezsc_network::{
config::MultiaddrWithPeerId, service::traits::NetworkService, NetworkBackend, NetworkBlock,
NetworkPeers, NetworkStateInfo,
};
use pezsc_network_sync::SyncingService;
use pezsc_network_types::PeerId;
use pezsc_rpc_server::Server;
use pezsc_utils::mpsc::TracingUnboundedReceiver;
use pezsp_blockchain::HeaderMetadata;
use pezsp_consensus::SyncOracle;
use pezsp_runtime::traits::{Block as BlockT, Header as HeaderT};
pub use self::{
builder::{
build_default_block_downloader, build_default_syncing_engine, build_network,
build_network_advanced, build_pezkuwi_syncing_strategy, gen_rpc_module, init_telemetry,
new_client, new_db_backend, new_full_client, new_full_parts, new_full_parts_record_import,
new_full_parts_with_genesis_builder, new_wasm_executor,
propagate_transaction_notifications, spawn_tasks, BuildNetworkAdvancedParams,
BuildNetworkParams, DefaultSyncingEngineConfig, KeystoreContainer, SpawnTasksParams,
TFullBackend, TFullCallExecutor, TFullClient,
},
client::{ClientConfig, LocalCallExecutor},
error::Error,
metrics::MetricsService,
};
#[allow(deprecated)]
pub use builder::new_native_or_wasm_executor;
pub use pezsc_chain_spec::{
construct_genesis_block, resolve_state_version_from_wasm, BuildGenesisBlock,
GenesisBlockBuilder,
};
pub use config::{
BasePath, BlocksPruning, Configuration, DatabaseSource, PruningMode, Role, RpcMethods, TaskType,
};
pub use pezsc_chain_spec::{
ChainSpec, ChainType, Extension as ChainSpecExtension, GenericChainSpec, NoExtension,
Properties,
};
use crate::config::RpcConfiguration;
use prometheus_endpoint::Registry;
pub use pezsc_consensus::ImportQueue;
pub use pezsc_executor::NativeExecutionDispatch;
pub use pezsc_network_sync::WarpSyncConfig;
#[doc(hidden)]
pub use pezsc_network_transactions::config::{TransactionImport, TransactionImportFuture};
pub use pezsc_rpc::{RandomIntegerSubscriptionId, RandomStringSubscriptionId};
pub use pezsc_tracing::TracingReceiver;
pub use pezsc_transaction_pool::TransactionPoolOptions;
pub use pezsc_transaction_pool_api::{error::IntoPoolError, InPoolTransaction, TransactionPool};
#[doc(hidden)]
pub use std::{ops::Deref, result::Result, sync::Arc};
pub use task_manager::{
SpawnEssentialTaskHandle, SpawnTaskHandle, Task, TaskManager, TaskRegistry, DEFAULT_GROUP_NAME,
};
use tokio::runtime::Handle;
const DEFAULT_PROTOCOL_ID: &str = "sup";
/// A running RPC service that can perform in-memory RPC queries.
#[derive(Clone)]
pub struct RpcHandlers {
// This is legacy and may be removed at some point, it was for WASM stuff before smoldot was a
// thing. https://github.com/pezkuwichain/kurdistan-sdk/issues/121#discussion_r1694971805
rpc_module: Arc<RpcModule<()>>,
// This can be used to introspect the port the RPC server is listening on. SDK consumers are
// depending on this and it should be supported even if in-memory query support is removed.
listen_addresses: Vec<Multiaddr>,
}
impl RpcHandlers {
/// Create PRC handlers instance.
pub fn new(rpc_module: Arc<RpcModule<()>>, listen_addresses: Vec<Multiaddr>) -> Self {
Self { rpc_module, listen_addresses }
}
/// Starts an RPC query.
///
/// The query is passed as a string and must be valid JSON-RPC request object.
///
/// Returns a response and a stream if the call successful, fails if the
/// query could not be decoded as a JSON-RPC request object.
///
/// If the request subscribes you to events, the `stream` can be used to
/// retrieve the events.
pub async fn rpc_query(
&self,
json_query: &str,
) -> Result<(String, tokio::sync::mpsc::Receiver<String>), serde_json::Error> {
// Because `tokio::sync::mpsc::channel` is used under the hood
// it will panic if it's set to usize::MAX.
//
// This limit is used to prevent panics and is large enough.
const TOKIO_MPSC_MAX_SIZE: usize = tokio::sync::Semaphore::MAX_PERMITS;
self.rpc_module.raw_json_request(json_query, TOKIO_MPSC_MAX_SIZE).await
}
/// Provides access to the underlying `RpcModule`
pub fn handle(&self) -> Arc<RpcModule<()>> {
self.rpc_module.clone()
}
/// Provides access to listen addresses
pub fn listen_addresses(&self) -> &[Multiaddr] {
&self.listen_addresses[..]
}
}
/// An incomplete set of chain components, but enough to run the chain ops subcommands.
pub struct PartialComponents<Client, Backend, SelectChain, ImportQueue, TransactionPool, Other> {
/// A shared client instance.
pub client: Arc<Client>,
/// A shared backend instance.
pub backend: Arc<Backend>,
/// The chain task manager.
pub task_manager: TaskManager,
/// A keystore container instance.
pub keystore_container: KeystoreContainer,
/// A chain selection algorithm instance.
pub select_chain: SelectChain,
/// An import queue.
pub import_queue: ImportQueue,
/// A shared transaction pool.
pub transaction_pool: Arc<TransactionPool>,
/// Everything else that needs to be passed into the main build function.
pub other: Other,
}
/// Builds a future that continuously polls the network.
async fn build_network_future<
B: BlockT,
C: BlockchainEvents<B>
+ HeaderBackend<B>
+ BlockBackend<B>
+ HeaderMetadata<B, Error = pezsp_blockchain::Error>
+ ProofProvider<B>
+ Send
+ Sync
+ 'static,
H: pezsc_network_common::ExHashT,
N: NetworkBackend<B, <B as BlockT>::Hash>,
>(
network: N,
client: Arc<C>,
sync_service: Arc<SyncingService<B>>,
announce_imported_blocks: bool,
) {
let mut imported_blocks_stream = client.import_notification_stream().fuse();
// Stream of finalized blocks reported by the client.
let mut finality_notification_stream = client.finality_notification_stream().fuse();
let network_run = network.run().fuse();
pin_mut!(network_run);
loop {
futures::select! {
// List of blocks that the client has imported.
notification = imported_blocks_stream.next() => {
let notification = match notification {
Some(n) => n,
// If this stream is shut down, that means the client has shut down, and the
// most appropriate thing to do for the network future is to shut down too.
None => {
debug!("Block import stream has terminated, shutting down the network future.");
return
},
};
if announce_imported_blocks {
sync_service.announce_block(notification.hash, None);
}
if notification.is_new_best {
sync_service.new_best_block_imported(
notification.hash,
*notification.header.number(),
);
}
}
// List of blocks that the client has finalized.
notification = finality_notification_stream.select_next_some() => {
sync_service.on_block_finalized(notification.hash, notification.header);
}
// Drive the network. Shut down the network future if `NetworkWorker` has terminated.
_ = network_run => {
debug!("`NetworkWorker` has terminated, shutting down the network future.");
return
}
}
}
}
/// Builds a future that processes system RPC requests.
pub async fn build_system_rpc_future<
B: BlockT,
C: BlockchainEvents<B>
+ HeaderBackend<B>
+ BlockBackend<B>
+ HeaderMetadata<B, Error = pezsp_blockchain::Error>
+ ProofProvider<B>
+ Send
+ Sync
+ 'static,
H: pezsc_network_common::ExHashT,
>(
role: Role,
network_service: Arc<dyn NetworkService>,
sync_service: Arc<SyncingService<B>>,
client: Arc<C>,
mut rpc_rx: TracingUnboundedReceiver<pezsc_rpc::system::Request<B>>,
should_have_peers: bool,
) {
// Current best block at initialization, to report to the RPC layer.
let starting_block = client.info().best_number;
loop {
// Answer incoming RPC requests.
let Some(req) = rpc_rx.next().await else {
debug!("RPC requests stream has terminated, shutting down the system RPC future.");
return;
};
match req {
pezsc_rpc::system::Request::Health(sender) => match sync_service.peers_info().await {
Ok(info) => {
let _ = sender.send(pezsc_rpc::system::Health {
peers: info.len(),
is_syncing: sync_service.is_major_syncing(),
should_have_peers,
});
},
Err(_) => log::error!("`SyncingEngine` shut down"),
},
pezsc_rpc::system::Request::LocalPeerId(sender) => {
let _ = sender.send(network_service.local_peer_id().to_base58());
},
pezsc_rpc::system::Request::LocalListenAddresses(sender) => {
let peer_id = (network_service.local_peer_id()).into();
let p2p_proto_suffix = pezsc_network::multiaddr::Protocol::P2p(peer_id);
let addresses = network_service
.listen_addresses()
.iter()
.map(|addr| addr.clone().with(p2p_proto_suffix.clone()).to_string())
.collect();
let _ = sender.send(addresses);
},
pezsc_rpc::system::Request::Peers(sender) => match sync_service.peers_info().await {
Ok(info) => {
let _ = sender.send(
info.into_iter()
.map(|(peer_id, p)| pezsc_rpc::system::PeerInfo {
peer_id: peer_id.to_base58(),
roles: format!("{:?}", p.roles),
best_hash: p.best_hash,
best_number: p.best_number,
})
.collect(),
);
},
Err(_) => log::error!("`SyncingEngine` shut down"),
},
pezsc_rpc::system::Request::NetworkState(sender) => {
let network_state = network_service.network_state().await;
if let Ok(network_state) = network_state {
if let Ok(network_state) = serde_json::to_value(network_state) {
let _ = sender.send(network_state);
}
} else {
break;
}
},
pezsc_rpc::system::Request::NetworkAddReservedPeer(peer_addr, sender) => {
let result = match MultiaddrWithPeerId::try_from(peer_addr) {
Ok(peer) => network_service.add_reserved_peer(peer),
Err(err) => Err(err.to_string()),
};
let x = result.map_err(pezsc_rpc::system::error::Error::MalformattedPeerArg);
let _ = sender.send(x);
},
pezsc_rpc::system::Request::NetworkRemoveReservedPeer(peer_id, sender) => {
let _ = match peer_id.parse::<PeerId>() {
Ok(peer_id) => {
network_service.remove_reserved_peer(peer_id);
sender.send(Ok(()))
},
Err(e) => sender.send(Err(pezsc_rpc::system::error::Error::MalformattedPeerArg(
e.to_string(),
))),
};
},
pezsc_rpc::system::Request::NetworkReservedPeers(sender) => {
let Ok(reserved_peers) = network_service.reserved_peers().await else {
break;
};
let _ =
sender.send(reserved_peers.iter().map(|peer_id| peer_id.to_base58()).collect());
},
pezsc_rpc::system::Request::NodeRoles(sender) => {
use pezsc_rpc::system::NodeRole;
let node_role = match role {
Role::Authority { .. } => NodeRole::Authority,
Role::Full => NodeRole::Full,
};
let _ = sender.send(vec![node_role]);
},
pezsc_rpc::system::Request::SyncState(sender) => {
use pezsc_rpc::system::SyncState;
match sync_service.status().await.map(|status| status.best_seen_block) {
Ok(best_seen_block) => {
let best_number = client.info().best_number;
let _ = sender.send(SyncState {
starting_block,
current_block: best_number,
highest_block: best_seen_block.unwrap_or(best_number),
});
},
Err(_) => log::error!("`SyncingEngine` shut down"),
}
},
}
}
debug!("`NetworkWorker` has terminated, shutting down the system RPC future.");
}
/// Starts RPC servers.
pub fn start_rpc_servers<R>(
rpc_configuration: &RpcConfiguration,
registry: Option<&Registry>,
tokio_handle: &Handle,
gen_rpc_module: R,
rpc_id_provider: Option<Box<dyn pezsc_rpc_server::SubscriptionIdProvider>>,
) -> Result<Server, error::Error>
where
R: Fn() -> Result<RpcModule<()>, Error>,
{
let endpoints: Vec<pezsc_rpc_server::RpcEndpoint> = if let Some(endpoints) =
rpc_configuration.addr.as_ref()
{
endpoints.clone()
} else {
let ipv6 =
SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::LOCALHOST, rpc_configuration.port, 0, 0));
let ipv4 = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, rpc_configuration.port));
vec![
pezsc_rpc_server::RpcEndpoint {
batch_config: rpc_configuration.batch_config,
cors: rpc_configuration.cors.clone(),
listen_addr: ipv4,
max_buffer_capacity_per_connection: rpc_configuration.message_buffer_capacity,
max_connections: rpc_configuration.max_connections,
max_payload_in_mb: rpc_configuration.max_request_size,
max_payload_out_mb: rpc_configuration.max_response_size,
max_subscriptions_per_connection: rpc_configuration.max_subs_per_conn,
rpc_methods: rpc_configuration.methods.into(),
rate_limit: rpc_configuration.rate_limit,
rate_limit_trust_proxy_headers: rpc_configuration.rate_limit_trust_proxy_headers,
rate_limit_whitelisted_ips: rpc_configuration.rate_limit_whitelisted_ips.clone(),
retry_random_port: true,
is_optional: false,
},
pezsc_rpc_server::RpcEndpoint {
batch_config: rpc_configuration.batch_config,
cors: rpc_configuration.cors.clone(),
listen_addr: ipv6,
max_buffer_capacity_per_connection: rpc_configuration.message_buffer_capacity,
max_connections: rpc_configuration.max_connections,
max_payload_in_mb: rpc_configuration.max_request_size,
max_payload_out_mb: rpc_configuration.max_response_size,
max_subscriptions_per_connection: rpc_configuration.max_subs_per_conn,
rpc_methods: rpc_configuration.methods.into(),
rate_limit: rpc_configuration.rate_limit,
rate_limit_trust_proxy_headers: rpc_configuration.rate_limit_trust_proxy_headers,
rate_limit_whitelisted_ips: rpc_configuration.rate_limit_whitelisted_ips.clone(),
retry_random_port: true,
is_optional: true,
},
]
};
let metrics = pezsc_rpc_server::RpcMetrics::new(registry)?;
let rpc_api = gen_rpc_module()?;
let server_config = pezsc_rpc_server::Config {
endpoints,
rpc_api,
metrics,
id_provider: rpc_id_provider,
tokio_handle: tokio_handle.clone(),
request_logger_limit: rpc_configuration.request_logger_limit,
};
// TODO: https://github.com/pezkuwichain/kurdistan-sdk/issues/12
//
// `block_in_place` is a hack to allow callers to call `block_on` prior to
// calling `start_rpc_servers`.
match tokio::task::block_in_place(|| {
tokio_handle.block_on(pezsc_rpc_server::start_server(server_config))
}) {
Ok(server) => Ok(server),
Err(e) => Err(Error::Application(e)),
}
}
/// Transaction pool adapter.
pub struct TransactionPoolAdapter<C, P> {
pool: Arc<P>,
client: Arc<C>,
}
impl<C, P> TransactionPoolAdapter<C, P> {
/// Constructs a new instance of [`TransactionPoolAdapter`].
pub fn new(pool: Arc<P>, client: Arc<C>) -> Self {
Self { pool, client }
}
}
/// Get transactions for propagation.
///
/// Function extracted to simplify the test and prevent creating `ServiceFactory`.
fn transactions_to_propagate<Pool, B, H, E>(pool: &Pool) -> Vec<(H, Arc<B::Extrinsic>)>
where
Pool: TransactionPool<Block = B, Hash = H, Error = E>,
B: BlockT,
H: std::hash::Hash + Eq + pezsp_runtime::traits::Member + pezsp_runtime::traits::MaybeSerialize,
E: IntoPoolError + From<pezsc_transaction_pool_api::error::Error>,
{
pool.ready()
.filter(|t| t.is_propagable())
.map(|t| {
let hash = t.hash().clone();
let ex = t.data().clone();
(hash, ex)
})
.collect()
}
impl<B, H, C, Pool, E> pezsc_network_transactions::config::TransactionPool<H, B>
for TransactionPoolAdapter<C, Pool>
where
C: HeaderBackend<B>
+ BlockBackend<B>
+ HeaderMetadata<B, Error = pezsp_blockchain::Error>
+ ProofProvider<B>
+ Send
+ Sync
+ 'static,
Pool: 'static + TransactionPool<Block = B, Hash = H, Error = E>,
B: BlockT,
H: std::hash::Hash + Eq + pezsp_runtime::traits::Member + pezsp_runtime::traits::MaybeSerialize,
E: 'static + IntoPoolError + From<pezsc_transaction_pool_api::error::Error>,
{
fn transactions(&self) -> Vec<(H, Arc<B::Extrinsic>)> {
transactions_to_propagate(&*self.pool)
}
fn hash_of(&self, transaction: &B::Extrinsic) -> H {
self.pool.hash_of(transaction)
}
fn import(&self, transaction: B::Extrinsic) -> TransactionImportFuture {
let encoded = transaction.encode();
let uxt = match Decode::decode(&mut &encoded[..]) {
Ok(uxt) => uxt,
Err(e) => {
debug!(target: pezsc_transaction_pool::LOG_TARGET, "Transaction invalid: {:?}", e);
return Box::pin(futures::future::ready(TransactionImport::Bad));
},
};
let start = std::time::Instant::now();
let pool = self.pool.clone();
let client = self.client.clone();
Box::pin(async move {
match pool
.submit_one(
client.info().best_hash,
pezsc_transaction_pool_api::TransactionSource::External,
uxt,
)
.await
{
Ok(_) => {
let elapsed = start.elapsed();
trace!(target: pezsc_transaction_pool::LOG_TARGET, "import transaction: {elapsed:?}");
TransactionImport::NewGood
},
Err(e) => match e.into_pool_error() {
Ok(pezsc_transaction_pool_api::error::Error::AlreadyImported(_)) =>
TransactionImport::KnownGood,
Ok(_) => TransactionImport::Bad,
Err(_) => {
// it is not bad at least, just some internal node logic error, so peer is
// innocent.
TransactionImport::KnownGood
},
},
}
})
}
fn on_broadcasted(&self, propagations: HashMap<H, Vec<String>>) {
self.pool.on_broadcasted(propagations)
}
fn transaction(&self, hash: &H) -> Option<Arc<B::Extrinsic>> {
self.pool.ready_transaction(hash).and_then(
// Only propagable transactions should be resolved for network service.
|tx| tx.is_propagable().then(|| tx.data().clone()),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures::executor::block_on;
use pezsc_transaction_pool::BasicPool;
use pezsp_consensus::SelectChain;
use bizinikiwi_test_runtime_client::{
prelude::*,
runtime::{ExtrinsicBuilder, Transfer, TransferData},
};
#[test]
fn should_not_propagate_transactions_that_are_marked_as_such() {
// given
let (client, longest_chain) = TestClientBuilder::new().build_with_longest_chain();
let client = Arc::new(client);
let spawner = pezsp_core::testing::TaskExecutor::new();
let pool = Arc::from(BasicPool::new_full(
Default::default(),
true.into(),
None,
spawner,
client.clone(),
));
let source = pezsp_runtime::transaction_validity::TransactionSource::External;
let best = block_on(longest_chain.best_chain()).unwrap();
let transaction = Transfer {
amount: 5,
nonce: 0,
from: Sr25519Keyring::Alice.into(),
to: Sr25519Keyring::Bob.into(),
}
.into_unchecked_extrinsic();
block_on(pool.submit_one(best.hash(), source, transaction.clone())).unwrap();
block_on(pool.submit_one(
best.hash(),
source,
ExtrinsicBuilder::new_call_do_not_propagate().nonce(1).build(),
))
.unwrap();
assert_eq!(pool.status().ready, 2);
// when
let transactions = transactions_to_propagate(&*pool);
// then
assert_eq!(transactions.len(), 1);
assert!(TransferData::try_from(&*transactions[0].1).is_ok());
}
}
+293
View File
@@ -0,0 +1,293 @@
// 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 futures_timer::Delay;
use prometheus_endpoint::{register, Gauge, GaugeVec, Opts, PrometheusError, Registry, U64};
use pezsc_client_api::{ClientInfo, UsageProvider};
use pezsc_network::{config::Role, NetworkStatus, NetworkStatusProvider};
use pezsc_network_sync::{SyncStatus, SyncStatusProvider};
use pezsc_telemetry::{telemetry, TelemetryHandle, BIZINIKIWI_INFO};
use pezsc_transaction_pool_api::{MaintainedTransactionPool, PoolStatus};
use pezsc_utils::metrics::register_globals;
use pezsp_api::ProvideRuntimeApi;
use pezsp_runtime::traits::{Block, NumberFor, SaturatedConversion, UniqueSaturatedInto};
use std::{
sync::Arc,
time::{Duration, Instant, SystemTime},
};
struct PrometheusMetrics {
// generic info
block_height: GaugeVec<U64>,
number_leaves: Gauge<U64>,
ready_transactions_number: Gauge<U64>,
// I/O
database_cache: Gauge<U64>,
state_cache: Gauge<U64>,
}
impl PrometheusMetrics {
fn setup(
registry: &Registry,
name: &str,
version: &str,
roles: u64,
) -> Result<Self, PrometheusError> {
register(
Gauge::<U64>::with_opts(
Opts::new(
"bizinikiwi_build_info",
"A metric with a constant '1' value labeled by name, version",
)
.const_label("name", name)
.const_label("version", version),
)?,
registry,
)?
.set(1);
register(
Gauge::<U64>::new("bizinikiwi_node_roles", "The roles the node is running as")?,
registry,
)?
.set(roles);
register_globals(registry)?;
let start_time_since_epoch =
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default();
register(
Gauge::<U64>::new(
"bizinikiwi_process_start_time_seconds",
"Number of seconds between the UNIX epoch and the moment the process started",
)?,
registry,
)?
.set(start_time_since_epoch.as_secs());
Ok(Self {
// generic internals
block_height: register(
GaugeVec::new(
Opts::new("bizinikiwi_block_height", "Block height info of the chain"),
&["status"],
)?,
registry,
)?,
number_leaves: register(
Gauge::new("bizinikiwi_number_leaves", "Number of known chain leaves (aka forks)")?,
registry,
)?,
ready_transactions_number: register(
Gauge::new(
"bizinikiwi_ready_transactions_number",
"Number of transactions in the ready queue",
)?,
registry,
)?,
// I/ O
database_cache: register(
Gauge::new("bizinikiwi_database_cache_bytes", "RocksDB cache size in bytes")?,
registry,
)?,
state_cache: register(
Gauge::new("bizinikiwi_state_cache_bytes", "State cache size in bytes")?,
registry,
)?,
})
}
}
/// A `MetricsService` periodically sends general client and
/// network state to the telemetry as well as (optionally)
/// a Prometheus endpoint.
pub struct MetricsService {
metrics: Option<PrometheusMetrics>,
last_update: Instant,
last_total_bytes_inbound: u64,
last_total_bytes_outbound: u64,
telemetry: Option<TelemetryHandle>,
}
impl MetricsService {
/// Creates a `MetricsService` that only sends information
/// to the telemetry.
pub fn new(telemetry: Option<TelemetryHandle>) -> Self {
MetricsService {
metrics: None,
last_total_bytes_inbound: 0,
last_total_bytes_outbound: 0,
last_update: Instant::now(),
telemetry,
}
}
/// Creates a `MetricsService` that sends metrics
/// to prometheus alongside the telemetry.
pub fn with_prometheus(
telemetry: Option<TelemetryHandle>,
registry: &Registry,
role: Role,
node_name: &str,
impl_version: &str,
) -> Result<Self, PrometheusError> {
let role_bits = match role {
Role::Full => 1u64,
// 2u64 used to represent light client role
Role::Authority { .. } => 4u64,
};
PrometheusMetrics::setup(registry, node_name, impl_version, role_bits).map(|p| {
MetricsService {
metrics: Some(p),
last_total_bytes_inbound: 0,
last_total_bytes_outbound: 0,
last_update: Instant::now(),
telemetry,
}
})
}
/// Returns a never-ending `Future` that performs the
/// metric and telemetry updates with information from
/// the given sources.
pub async fn run<TBl, TExPool, TCl, TNet, TSync>(
mut self,
client: Arc<TCl>,
transactions: Arc<TExPool>,
network: TNet,
syncing: TSync,
) where
TBl: Block,
TCl: ProvideRuntimeApi<TBl> + UsageProvider<TBl>,
TExPool: MaintainedTransactionPool<Block = TBl, Hash = <TBl as Block>::Hash>,
TNet: NetworkStatusProvider,
TSync: SyncStatusProvider<TBl>,
{
let mut timer = Delay::new(Duration::from_secs(0));
let timer_interval = Duration::from_secs(5);
loop {
// Wait for the next tick of the timer.
(&mut timer).await;
// Try to get the latest network information.
let net_status = network.status().await.ok();
// Try to get the latest syncing information.
let sync_status = syncing.status().await.ok();
// Update / Send the metrics.
self.update(&client.usage_info(), &transactions.status(), net_status, sync_status);
// Schedule next tick.
timer.reset(timer_interval);
}
}
fn update<T: Block>(
&mut self,
info: &ClientInfo<T>,
txpool_status: &PoolStatus,
net_status: Option<NetworkStatus>,
sync_status: Option<SyncStatus<T>>,
) {
let now = Instant::now();
let elapsed = (now - self.last_update).as_secs();
self.last_update = now;
let best_number = info.chain.best_number.saturated_into::<u64>();
let best_hash = info.chain.best_hash;
let finalized_number: u64 = info.chain.finalized_number.saturated_into::<u64>();
// Update/send metrics that are always available.
telemetry!(
self.telemetry;
BIZINIKIWI_INFO;
"system.interval";
"height" => best_number,
"best" => ?best_hash,
"txcount" => txpool_status.ready,
"finalized_height" => finalized_number,
"finalized_hash" => ?info.chain.finalized_hash,
"used_state_cache_size" => info.usage.as_ref()
.map(|usage| usage.memory.state_cache.as_bytes())
.unwrap_or(0),
);
if let Some(metrics) = self.metrics.as_ref() {
metrics.block_height.with_label_values(&["finalized"]).set(finalized_number);
metrics.block_height.with_label_values(&["best"]).set(best_number);
if let Ok(leaves) = u64::try_from(info.chain.number_leaves) {
metrics.number_leaves.set(leaves);
}
metrics.ready_transactions_number.set(txpool_status.ready as u64);
if let Some(info) = info.usage.as_ref() {
metrics.database_cache.set(info.memory.database_cache.as_bytes() as u64);
metrics.state_cache.set(info.memory.state_cache.as_bytes() as u64);
}
}
// Update/send network status information, if any.
if let Some(net_status) = net_status {
let num_peers = net_status.num_connected_peers;
let total_bytes_inbound = net_status.total_bytes_inbound;
let total_bytes_outbound = net_status.total_bytes_outbound;
let diff_bytes_inbound = total_bytes_inbound - self.last_total_bytes_inbound;
let diff_bytes_outbound = total_bytes_outbound - self.last_total_bytes_outbound;
let (avg_bytes_per_sec_inbound, avg_bytes_per_sec_outbound) = if elapsed > 0 {
self.last_total_bytes_inbound = total_bytes_inbound;
self.last_total_bytes_outbound = total_bytes_outbound;
(diff_bytes_inbound / elapsed, diff_bytes_outbound / elapsed)
} else {
(diff_bytes_inbound, diff_bytes_outbound)
};
telemetry!(
self.telemetry;
BIZINIKIWI_INFO;
"system.interval";
"peers" => num_peers,
"bandwidth_download" => avg_bytes_per_sec_inbound,
"bandwidth_upload" => avg_bytes_per_sec_outbound,
);
}
if let Some(sync_status) = sync_status {
if let Some(metrics) = self.metrics.as_ref() {
let best_seen_block: Option<u64> =
sync_status.best_seen_block.map(|num: NumberFor<T>| {
UniqueSaturatedInto::<u64>::unique_saturated_into(num)
});
metrics
.block_height
.with_label_values(&["sync_target"])
.set(best_seen_block.unwrap_or(best_number));
}
}
}
}
@@ -0,0 +1,551 @@
// 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 service tasks management module.
use crate::{config::TaskType, Error};
use exit_future::Signal;
use futures::{
future::{pending, select, try_join_all, BoxFuture, Either},
Future, FutureExt, StreamExt,
};
use parking_lot::Mutex;
use prometheus_endpoint::{
exponential_buckets, register, CounterVec, HistogramOpts, HistogramVec, Opts, PrometheusError,
Registry, U64,
};
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
use std::{
collections::{hash_map::Entry, HashMap},
panic,
pin::Pin,
result::Result,
sync::Arc,
};
use tokio::runtime::Handle;
use tracing_futures::Instrument;
mod prometheus_future;
#[cfg(test)]
mod tests;
/// Default task group name.
pub const DEFAULT_GROUP_NAME: &str = "default";
/// The name of a group a task belongs to.
///
/// This name is passed belong-side the task name to the prometheus metrics and can be used
/// to group tasks.
pub enum GroupName {
/// Sets the group name to `default`.
Default,
/// Use the specifically given name as group name.
Specific(&'static str),
}
impl From<Option<&'static str>> for GroupName {
fn from(name: Option<&'static str>) -> Self {
match name {
Some(name) => Self::Specific(name),
None => Self::Default,
}
}
}
impl From<&'static str> for GroupName {
fn from(name: &'static str) -> Self {
Self::Specific(name)
}
}
/// An handle for spawning tasks in the service.
#[derive(Clone)]
pub struct SpawnTaskHandle {
on_exit: exit_future::Exit,
tokio_handle: Handle,
metrics: Option<Metrics>,
task_registry: TaskRegistry,
}
impl SpawnTaskHandle {
/// Spawns the given task with the given name and a group name.
/// If group is not specified `DEFAULT_GROUP_NAME` will be used.
///
/// Note that the `name` is a `&'static str`. The reason for this choice is that
/// statistics about this task are getting reported to the Prometheus endpoint (if enabled), and
/// that therefore the set of possible task names must be bounded.
///
/// In other words, it would be a bad idea for someone to do for example
/// `spawn(format!("{:?}", some_public_key))`.
pub fn spawn(
&self,
name: &'static str,
group: impl Into<GroupName>,
task: impl Future<Output = ()> + Send + 'static,
) {
self.spawn_inner(name, group, task, TaskType::Async)
}
/// Spawns the blocking task with the given name. See also `spawn`.
pub fn spawn_blocking(
&self,
name: &'static str,
group: impl Into<GroupName>,
task: impl Future<Output = ()> + Send + 'static,
) {
self.spawn_inner(name, group, task, TaskType::Blocking)
}
/// Helper function that implements the spawning logic. See `spawn` and `spawn_blocking`.
fn spawn_inner(
&self,
name: &'static str,
group: impl Into<GroupName>,
task: impl Future<Output = ()> + Send + 'static,
task_type: TaskType,
) {
let on_exit = self.on_exit.clone();
let metrics = self.metrics.clone();
let registry = self.task_registry.clone();
let group = match group.into() {
GroupName::Specific(var) => var,
// If no group is specified use default.
GroupName::Default => DEFAULT_GROUP_NAME,
};
let task_type_label = match task_type {
TaskType::Blocking => "blocking",
TaskType::Async => "async",
};
// Note that we increase the started counter here and not within the future. This way,
// we could properly visualize on Prometheus situations where the spawning doesn't work.
if let Some(metrics) = &self.metrics {
metrics.tasks_spawned.with_label_values(&[name, group, task_type_label]).inc();
// We do a dummy increase in order for the task to show up in metrics.
metrics
.tasks_ended
.with_label_values(&[name, "finished", group, task_type_label])
.inc_by(0);
}
let future = async move {
// Register the task and keep the "token" alive until the task is ended. Then this
// "token" will unregister this task.
let _registry_token = registry.register_task(name, group);
if let Some(metrics) = metrics {
// Add some wrappers around `task`.
let task = {
let poll_duration =
metrics.poll_duration.with_label_values(&[name, group, task_type_label]);
let poll_start =
metrics.poll_start.with_label_values(&[name, group, task_type_label]);
let inner =
prometheus_future::with_poll_durations(poll_duration, poll_start, task);
// The logic of `AssertUnwindSafe` here is ok considering that we throw
// away the `Future` after it has panicked.
panic::AssertUnwindSafe(inner).catch_unwind()
};
futures::pin_mut!(task);
match select(on_exit, task).await {
Either::Right((Err(payload), _)) => {
metrics
.tasks_ended
.with_label_values(&[name, "panic", group, task_type_label])
.inc();
panic::resume_unwind(payload)
},
Either::Right((Ok(()), _)) => {
metrics
.tasks_ended
.with_label_values(&[name, "finished", group, task_type_label])
.inc();
},
Either::Left(((), _)) => {
// The `on_exit` has triggered.
metrics
.tasks_ended
.with_label_values(&[name, "interrupted", group, task_type_label])
.inc();
},
}
} else {
futures::pin_mut!(task);
let _ = select(on_exit, task).await;
}
}
.in_current_span();
match task_type {
TaskType::Async => {
self.tokio_handle.spawn(future);
},
TaskType::Blocking => {
let handle = self.tokio_handle.clone();
self.tokio_handle.spawn_blocking(move || {
handle.block_on(future);
});
},
}
}
}
impl pezsp_core::traits::SpawnNamed for SpawnTaskHandle {
fn spawn_blocking(
&self,
name: &'static str,
group: Option<&'static str>,
future: BoxFuture<'static, ()>,
) {
self.spawn_inner(name, group, future, TaskType::Blocking)
}
fn spawn(
&self,
name: &'static str,
group: Option<&'static str>,
future: BoxFuture<'static, ()>,
) {
self.spawn_inner(name, group, future, TaskType::Async)
}
}
/// A wrapper over `SpawnTaskHandle` that will notify a receiver whenever any
/// task spawned through it fails. The service should be on the receiver side
/// and will shut itself down whenever it receives any message, i.e. an
/// essential task has failed.
#[derive(Clone)]
pub struct SpawnEssentialTaskHandle {
essential_failed_tx: TracingUnboundedSender<()>,
inner: SpawnTaskHandle,
}
impl SpawnEssentialTaskHandle {
/// Creates a new `SpawnEssentialTaskHandle`.
pub fn new(
essential_failed_tx: TracingUnboundedSender<()>,
spawn_task_handle: SpawnTaskHandle,
) -> SpawnEssentialTaskHandle {
SpawnEssentialTaskHandle { essential_failed_tx, inner: spawn_task_handle }
}
/// Spawns the given task with the given name.
///
/// See also [`SpawnTaskHandle::spawn`].
pub fn spawn(
&self,
name: &'static str,
group: impl Into<GroupName>,
task: impl Future<Output = ()> + Send + 'static,
) {
self.spawn_inner(name, group, task, TaskType::Async)
}
/// Spawns the blocking task with the given name.
///
/// See also [`SpawnTaskHandle::spawn_blocking`].
pub fn spawn_blocking(
&self,
name: &'static str,
group: impl Into<GroupName>,
task: impl Future<Output = ()> + Send + 'static,
) {
self.spawn_inner(name, group, task, TaskType::Blocking)
}
fn spawn_inner(
&self,
name: &'static str,
group: impl Into<GroupName>,
task: impl Future<Output = ()> + Send + 'static,
task_type: TaskType,
) {
let essential_failed = self.essential_failed_tx.clone();
let essential_task = std::panic::AssertUnwindSafe(task).catch_unwind().map(move |_| {
log::error!("Essential task `{}` failed. Shutting down service.", name);
let _ = essential_failed.close();
});
let _ = self.inner.spawn_inner(name, group, essential_task, task_type);
}
}
impl pezsp_core::traits::SpawnEssentialNamed for SpawnEssentialTaskHandle {
fn spawn_essential_blocking(
&self,
name: &'static str,
group: Option<&'static str>,
future: BoxFuture<'static, ()>,
) {
self.spawn_blocking(name, group, future);
}
fn spawn_essential(
&self,
name: &'static str,
group: Option<&'static str>,
future: BoxFuture<'static, ()>,
) {
self.spawn(name, group, future);
}
}
/// Helper struct to manage background/async tasks in Service.
pub struct TaskManager {
/// A future that resolves when the service has exited, this is useful to
/// make sure any internally spawned futures stop when the service does.
on_exit: exit_future::Exit,
/// A signal that makes the exit future above resolve, fired on drop.
_signal: Signal,
/// Tokio runtime handle that is used to spawn futures.
tokio_handle: Handle,
/// Prometheus metric where to report the polling times.
metrics: Option<Metrics>,
/// Send a signal when a spawned essential task has concluded. The next time
/// the service future is polled it should complete with an error.
essential_failed_tx: TracingUnboundedSender<()>,
/// A receiver for spawned essential-tasks concluding.
essential_failed_rx: TracingUnboundedReceiver<()>,
/// Things to keep alive until the task manager is dropped.
keep_alive: Box<dyn std::any::Any + Send>,
/// A list of other `TaskManager`'s to terminate and gracefully shutdown when the parent
/// terminates and gracefully shutdown. Also ends the parent `future()` if a child's essential
/// task fails.
children: Vec<TaskManager>,
/// The registry of all running tasks.
task_registry: TaskRegistry,
}
impl TaskManager {
/// If a Prometheus registry is passed, it will be used to report statistics about the
/// service tasks.
pub fn new(
tokio_handle: Handle,
prometheus_registry: Option<&Registry>,
) -> Result<Self, PrometheusError> {
let (signal, on_exit) = exit_future::signal();
// A side-channel for essential tasks to communicate shutdown.
let (essential_failed_tx, essential_failed_rx) =
tracing_unbounded("mpsc_essential_tasks", 100);
let metrics = prometheus_registry.map(Metrics::register).transpose()?;
Ok(Self {
on_exit,
_signal: signal,
tokio_handle,
metrics,
essential_failed_tx,
essential_failed_rx,
keep_alive: Box::new(()),
children: Vec::new(),
task_registry: Default::default(),
})
}
/// Get a handle for spawning tasks.
pub fn spawn_handle(&self) -> SpawnTaskHandle {
SpawnTaskHandle {
on_exit: self.on_exit.clone(),
tokio_handle: self.tokio_handle.clone(),
metrics: self.metrics.clone(),
task_registry: self.task_registry.clone(),
}
}
/// Get a handle for spawning essential tasks.
pub fn spawn_essential_handle(&self) -> SpawnEssentialTaskHandle {
SpawnEssentialTaskHandle::new(self.essential_failed_tx.clone(), self.spawn_handle())
}
/// Return a future that will end with success if the signal to terminate was sent
/// (`self.terminate()`) or with an error if an essential task fails.
///
/// # Warning
///
/// This function will not wait until the end of the remaining task.
pub fn future<'a>(
&'a mut self,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
Box::pin(async move {
let mut t1 = self.essential_failed_rx.next().fuse();
let mut t2 = self.on_exit.clone().fuse();
let mut t3 = try_join_all(
self.children
.iter_mut()
.map(|x| x.future())
// Never end this future if there is no error because if there is no children,
// it must not stop
.chain(std::iter::once(pending().boxed())),
)
.fuse();
futures::select! {
_ = t1 => Err(Error::Other("Essential task failed.".into())),
_ = t2 => Ok(()),
res = t3 => Err(res.map(|_| ()).expect_err("this future never ends; qed")),
}
})
}
/// Set what the task manager should keep alive, can be called multiple times.
pub fn keep_alive<T: 'static + Send>(&mut self, to_keep_alive: T) {
// allows this fn to safely called multiple times.
use std::mem;
let old = mem::replace(&mut self.keep_alive, Box::new(()));
self.keep_alive = Box::new((to_keep_alive, old));
}
/// Register another TaskManager to terminate and gracefully shutdown when the parent
/// terminates and gracefully shutdown. Also ends the parent `future()` if a child's essential
/// task fails. (But don't end the parent if a child's normal task fails.)
pub fn add_child(&mut self, child: TaskManager) {
self.children.push(child);
}
/// Consume `self` and return the [`TaskRegistry`].
///
/// This [`TaskRegistry`] can be used to check for still running tasks after this task manager
/// was dropped.
pub fn into_task_registry(self) -> TaskRegistry {
self.task_registry
}
}
#[derive(Clone)]
struct Metrics {
// This list is ordered alphabetically
poll_duration: HistogramVec,
poll_start: CounterVec<U64>,
tasks_spawned: CounterVec<U64>,
tasks_ended: CounterVec<U64>,
}
impl Metrics {
fn register(registry: &Registry) -> Result<Self, PrometheusError> {
Ok(Self {
poll_duration: register(HistogramVec::new(
HistogramOpts {
common_opts: Opts::new(
"bizinikiwi_tasks_polling_duration",
"Duration in seconds of each invocation of Future::poll"
),
buckets: exponential_buckets(0.001, 4.0, 9)
.expect("function parameters are constant and always valid; qed"),
},
&["task_name", "task_group", "kind"]
)?, registry)?,
poll_start: register(CounterVec::new(
Opts::new(
"bizinikiwi_tasks_polling_started_total",
"Total number of times we started invoking Future::poll"
),
&["task_name", "task_group", "kind"]
)?, registry)?,
tasks_spawned: register(CounterVec::new(
Opts::new(
"bizinikiwi_tasks_spawned_total",
"Total number of tasks that have been spawned on the Service"
),
&["task_name", "task_group", "kind"]
)?, registry)?,
tasks_ended: register(CounterVec::new(
Opts::new(
"bizinikiwi_tasks_ended_total",
"Total number of tasks for which Future::poll has returned Ready(()) or panicked"
),
&["task_name", "reason", "task_group", "kind"]
)?, registry)?,
})
}
}
/// Ensures that a [`Task`] is unregistered when this object is dropped.
struct UnregisterOnDrop {
task: Task,
registry: TaskRegistry,
}
impl Drop for UnregisterOnDrop {
fn drop(&mut self) {
let mut tasks = self.registry.tasks.lock();
if let Entry::Occupied(mut entry) = (*tasks).entry(self.task.clone()) {
*entry.get_mut() -= 1;
if *entry.get() == 0 {
entry.remove();
}
}
}
}
/// Represents a running async task in the [`TaskManager`].
///
/// As a task is identified by a name and a group, it is totally valid that there exists multiple
/// tasks with the same name and group.
#[derive(Clone, Hash, Eq, PartialEq)]
pub struct Task {
/// The name of the task.
pub name: &'static str,
/// The group this task is associated to.
pub group: &'static str,
}
impl Task {
/// Returns if the `group` is the [`DEFAULT_GROUP_NAME`].
pub fn is_default_group(&self) -> bool {
self.group == DEFAULT_GROUP_NAME
}
}
/// Keeps track of all running [`Task`]s in [`TaskManager`].
#[derive(Clone, Default)]
pub struct TaskRegistry {
tasks: Arc<Mutex<HashMap<Task, usize>>>,
}
impl TaskRegistry {
/// Register a task with the given `name` and `group`.
///
/// Returns [`UnregisterOnDrop`] that ensures that the task is unregistered when this value is
/// dropped.
fn register_task(&self, name: &'static str, group: &'static str) -> UnregisterOnDrop {
let task = Task { name, group };
{
let mut tasks = self.tasks.lock();
*(*tasks).entry(task.clone()).or_default() += 1;
}
UnregisterOnDrop { task, registry: self.clone() }
}
/// Returns the running tasks.
///
/// As a task is only identified by its `name` and `group`, there can be duplicate tasks. The
/// number per task represents the concurrently running tasks with the same identifier.
pub fn running_tasks(&self) -> HashMap<Task, usize> {
(*self.tasks.lock()).clone()
}
}
@@ -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/>.
//! Wrapper around a `Future` that reports statistics about when the `Future` is polled.
use futures::prelude::*;
use prometheus_endpoint::{Counter, Histogram, U64};
use std::{
fmt,
pin::Pin,
task::{Context, Poll},
};
/// Wraps around a `Future`. Report the polling duration to the `Histogram` and when the polling
/// starts to the `Counter`.
pub fn with_poll_durations<T>(
poll_duration: Histogram,
poll_start: Counter<U64>,
inner: T,
) -> PrometheusFuture<T> {
PrometheusFuture { inner, poll_duration, poll_start }
}
/// Wraps around `Future` and adds diagnostics to it.
#[pin_project::pin_project]
#[derive(Clone)]
pub struct PrometheusFuture<T> {
/// The inner future doing the actual work.
#[pin]
inner: T,
poll_duration: Histogram,
poll_start: Counter<U64>,
}
impl<T> Future for PrometheusFuture<T>
where
T: Future,
{
type Output = T::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
let this = self.project();
this.poll_start.inc();
let _timer = this.poll_duration.start_timer();
Future::poll(this.inner, cx)
// `_timer` is dropped here and will observe the duration
}
}
impl<T> fmt::Debug for PrometheusFuture<T>
where
T: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(&self.inner, f)
}
}
@@ -0,0 +1,252 @@
// 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::task_manager::TaskManager;
use futures::{future::FutureExt, pin_mut, select};
use parking_lot::Mutex;
use std::{any::Any, sync::Arc, time::Duration};
#[derive(Clone, Debug)]
struct DropTester(Arc<Mutex<usize>>);
struct DropTesterRef(DropTester);
impl DropTester {
fn new() -> DropTester {
DropTester(Arc::new(Mutex::new(0)))
}
fn new_ref(&self) -> DropTesterRef {
*self.0.lock() += 1;
DropTesterRef(self.clone())
}
fn wait_on_drop(&self) {
while *self != 0 {
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
}
impl PartialEq<usize> for DropTester {
fn eq(&self, other: &usize) -> bool {
&*self.0.lock() == other
}
}
impl Drop for DropTesterRef {
fn drop(&mut self) {
*(self.0).0.lock() -= 1;
}
}
#[test]
fn ensure_drop_tester_working() {
let drop_tester = DropTester::new();
assert_eq!(drop_tester, 0);
let drop_tester_ref_1 = drop_tester.new_ref();
assert_eq!(drop_tester, 1);
let drop_tester_ref_2 = drop_tester.new_ref();
assert_eq!(drop_tester, 2);
drop(drop_tester_ref_1);
assert_eq!(drop_tester, 1);
drop(drop_tester_ref_2);
assert_eq!(drop_tester, 0);
}
async fn run_background_task(_keep_alive: impl Any) {
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
async fn run_background_task_blocking(duration: Duration, _keep_alive: impl Any) {
loop {
// block for X sec (not interruptible)
std::thread::sleep(duration);
// await for 1 sec (interruptible)
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
fn new_task_manager(tokio_handle: tokio::runtime::Handle) -> TaskManager {
TaskManager::new(tokio_handle, None).unwrap()
}
#[test]
fn ensure_tasks_are_awaited_on_shutdown() {
let drop_tester = DropTester::new();
{
let runtime = tokio::runtime::Runtime::new().unwrap();
let handle = runtime.handle().clone();
let task_manager = new_task_manager(handle);
let spawn_handle = task_manager.spawn_handle();
spawn_handle.spawn("task1", None, run_background_task(drop_tester.new_ref()));
spawn_handle.spawn("task2", None, run_background_task(drop_tester.new_ref()));
assert_eq!(drop_tester, 2);
// allow the tasks to even start
runtime.block_on(async { tokio::time::sleep(Duration::from_secs(1)).await });
assert_eq!(drop_tester, 2);
}
drop_tester.wait_on_drop();
}
#[test]
fn ensure_keep_alive_during_shutdown() {
let drop_tester = DropTester::new();
{
let runtime = tokio::runtime::Runtime::new().unwrap();
let handle = runtime.handle().clone();
let mut task_manager = new_task_manager(handle);
let spawn_handle = task_manager.spawn_handle();
task_manager.keep_alive(drop_tester.new_ref());
spawn_handle.spawn("task1", None, run_background_task(()));
assert_eq!(drop_tester, 1);
// allow the tasks to even start
runtime.block_on(async { tokio::time::sleep(Duration::from_secs(1)).await });
assert_eq!(drop_tester, 1);
}
drop_tester.wait_on_drop();
}
#[test]
fn ensure_blocking_futures_are_awaited_on_shutdown() {
let drop_tester = DropTester::new();
{
let runtime = tokio::runtime::Runtime::new().unwrap();
let handle = runtime.handle().clone();
let task_manager = new_task_manager(handle);
let spawn_handle = task_manager.spawn_handle();
spawn_handle.spawn(
"task1",
None,
run_background_task_blocking(Duration::from_secs(3), drop_tester.new_ref()),
);
spawn_handle.spawn(
"task2",
None,
run_background_task_blocking(Duration::from_secs(3), drop_tester.new_ref()),
);
assert_eq!(drop_tester, 2);
// allow the tasks to even start
runtime.block_on(async { tokio::time::sleep(Duration::from_secs(1)).await });
assert_eq!(drop_tester, 2);
}
assert_eq!(drop_tester, 0);
}
#[test]
fn ensure_task_manager_future_ends_with_error_when_essential_task_fails() {
let drop_tester = DropTester::new();
{
let runtime = tokio::runtime::Runtime::new().unwrap();
let handle = runtime.handle().clone();
let mut task_manager = new_task_manager(handle);
let spawn_handle = task_manager.spawn_handle();
let spawn_essential_handle = task_manager.spawn_essential_handle();
spawn_handle.spawn("task1", None, run_background_task(drop_tester.new_ref()));
spawn_handle.spawn("task2", None, run_background_task(drop_tester.new_ref()));
assert_eq!(drop_tester, 2);
// allow the tasks to even start
runtime.block_on(async { tokio::time::sleep(Duration::from_secs(1)).await });
assert_eq!(drop_tester, 2);
spawn_essential_handle.spawn("task3", None, async { panic!("task failed") });
runtime
.block_on(task_manager.future())
.expect_err("future()'s Result must be Err");
assert_eq!(drop_tester, 2);
}
drop_tester.wait_on_drop();
}
#[test]
fn ensure_task_manager_future_ends_with_error_when_childs_essential_task_fails() {
let drop_tester = DropTester::new();
{
let runtime = tokio::runtime::Runtime::new().unwrap();
let handle = runtime.handle().clone();
let mut task_manager = new_task_manager(handle.clone());
let child_1 = new_task_manager(handle.clone());
let spawn_handle_child_1 = child_1.spawn_handle();
let spawn_essential_handle_child_1 = child_1.spawn_essential_handle();
let child_2 = new_task_manager(handle.clone());
let spawn_handle_child_2 = child_2.spawn_handle();
task_manager.add_child(child_1);
task_manager.add_child(child_2);
let spawn_handle = task_manager.spawn_handle();
spawn_handle.spawn("task1", None, run_background_task(drop_tester.new_ref()));
spawn_handle.spawn("task2", None, run_background_task(drop_tester.new_ref()));
spawn_handle_child_1.spawn("task3", None, run_background_task(drop_tester.new_ref()));
spawn_handle_child_2.spawn("task4", None, run_background_task(drop_tester.new_ref()));
assert_eq!(drop_tester, 4);
// allow the tasks to even start
runtime.block_on(async { tokio::time::sleep(Duration::from_secs(1)).await });
assert_eq!(drop_tester, 4);
spawn_essential_handle_child_1.spawn("task5", None, async { panic!("task failed") });
runtime
.block_on(task_manager.future())
.expect_err("future()'s Result must be Err");
assert_eq!(drop_tester, 4);
}
drop_tester.wait_on_drop();
}
#[test]
fn ensure_task_manager_future_continues_when_childs_not_essential_task_fails() {
let drop_tester = DropTester::new();
{
let runtime = tokio::runtime::Runtime::new().unwrap();
let handle = runtime.handle().clone();
let mut task_manager = new_task_manager(handle.clone());
let child_1 = new_task_manager(handle.clone());
let spawn_handle_child_1 = child_1.spawn_handle();
let child_2 = new_task_manager(handle.clone());
let spawn_handle_child_2 = child_2.spawn_handle();
task_manager.add_child(child_1);
task_manager.add_child(child_2);
let spawn_handle = task_manager.spawn_handle();
spawn_handle.spawn("task1", None, run_background_task(drop_tester.new_ref()));
spawn_handle.spawn("task2", None, run_background_task(drop_tester.new_ref()));
spawn_handle_child_1.spawn("task3", None, run_background_task(drop_tester.new_ref()));
spawn_handle_child_2.spawn("task4", None, run_background_task(drop_tester.new_ref()));
assert_eq!(drop_tester, 4);
// allow the tasks to even start
runtime.block_on(async { tokio::time::sleep(Duration::from_secs(1)).await });
assert_eq!(drop_tester, 4);
spawn_handle_child_1.spawn("task5", None, async { panic!("task failed") });
runtime.block_on(async {
let t1 = task_manager.future().fuse();
let t2 = tokio::time::sleep(Duration::from_secs(3)).fuse();
pin_mut!(t1, t2);
select! {
res = t1 => panic!("task should not have stopped: {:?}", res),
_ = t2 => {},
}
});
assert_eq!(drop_tester, 4);
}
drop_tester.wait_on_drop();
}
+67
View File
@@ -0,0 +1,67 @@
[package]
name = "pezsc-service-test"
version = "2.0.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
publish = false
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
array-bytes = { workspace = true, default-features = true }
async-channel = { workspace = true }
codec = { workspace = true, default-features = true }
fdlimit = { workspace = true }
futures = { workspace = true }
log = { workspace = true, default-features = true }
parking_lot = { workspace = true, default-features = true }
pezsc-block-builder = { workspace = true, default-features = true }
pezsc-client-api = { workspace = true, default-features = true }
pezsc-client-db = { workspace = true }
pezsc-consensus = { workspace = true, default-features = true }
pezsc-executor = { workspace = true, default-features = true }
pezsc-network = { workspace = true, default-features = true }
pezsc-network-sync = { workspace = true, default-features = true }
pezsc-service = { workspace = true, default-features = true }
pezsc-transaction-pool-api = { workspace = true, default-features = true }
pezsp-api = { workspace = true, default-features = true }
pezsp-blockchain = { workspace = true, default-features = true }
pezsp-consensus = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-io = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
pezsp-storage = { workspace = true, default-features = true }
pezsp-tracing = { workspace = true, default-features = true }
bizinikiwi-test-runtime = { workspace = true }
bizinikiwi-test-runtime-client = { workspace = true }
tempfile = { workspace = true }
tokio = { features = ["time"], workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezsc-block-builder/runtime-benchmarks",
"pezsc-client-api/runtime-benchmarks",
"pezsc-client-db/runtime-benchmarks",
"pezsc-consensus/runtime-benchmarks",
"pezsc-executor/runtime-benchmarks",
"pezsc-network-sync/runtime-benchmarks",
"pezsc-network/runtime-benchmarks",
"pezsc-service/runtime-benchmarks",
"pezsc-transaction-pool-api/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-blockchain/runtime-benchmarks",
"pezsp-consensus/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
"bizinikiwi-test-runtime-client/runtime-benchmarks",
"bizinikiwi-test-runtime/runtime-benchmarks",
]
@@ -0,0 +1,58 @@
// 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::offchain::{storage::InMemOffchainStorage, OffchainStorage};
use std::sync::Arc;
type TestBackend = pezsc_client_api::in_mem::Backend<bizinikiwi_test_runtime::Block>;
#[test]
fn test_leaves_with_complex_block_tree() {
let backend = Arc::new(TestBackend::new());
bizinikiwi_test_runtime_client::trait_tests::test_leaves_for_backend(backend);
}
#[test]
fn test_blockchain_query_by_number_gets_canonical() {
let backend = Arc::new(TestBackend::new());
bizinikiwi_test_runtime_client::trait_tests::test_blockchain_query_by_number_gets_canonical(
backend,
);
}
#[test]
fn in_memory_offchain_storage() {
let mut storage = InMemOffchainStorage::default();
assert_eq!(storage.get(b"A", b"B"), None);
assert_eq!(storage.get(b"B", b"A"), None);
storage.set(b"A", b"B", b"C");
assert_eq!(storage.get(b"A", b"B"), Some(b"C".to_vec()));
assert_eq!(storage.get(b"B", b"A"), None);
storage.compare_and_set(b"A", b"B", Some(b"X"), b"D");
assert_eq!(storage.get(b"A", b"B"), Some(b"C".to_vec()));
storage.compare_and_set(b"A", b"B", Some(b"C"), b"D");
assert_eq!(storage.get(b"A", b"B"), Some(b"D".to_vec()));
assert!(!storage.compare_and_set(b"B", b"A", Some(b""), b"Y"));
assert!(storage.compare_and_set(b"B", b"A", None, b"X"));
assert_eq!(storage.get(b"B", b"A"), Some(b"X".to_vec()));
}
File diff suppressed because it is too large Load Diff
+571
View File
@@ -0,0 +1,571 @@
// 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/>.
//! Service integration test utils.
use futures::{task::Poll, Future, TryFutureExt as _};
use log::{debug, info};
use parking_lot::Mutex;
use pezsc_client_api::{Backend, CallExecutor};
use pezsc_network::{
config::{MultiaddrWithPeerId, NetworkConfiguration, TransportConfig},
multiaddr, NetworkBlock, NetworkPeers, NetworkStateInfo,
};
use pezsc_network_sync::SyncingService;
use pezsc_service::{
client::Client,
config::{
BasePath, DatabaseSource, ExecutorConfiguration, KeystoreConfig, RpcBatchRequestConfig,
RpcConfiguration,
},
BlocksPruning, ChainSpecExtension, Configuration, Error, GenericChainSpec, Role,
SpawnTaskHandle, TaskManager,
};
use pezsc_transaction_pool_api::TransactionPool;
use pezsp_blockchain::HeaderBackend;
use pezsp_runtime::traits::Block as BlockT;
use std::{iter, net::Ipv4Addr, pin::Pin, sync::Arc, task::Context, time::Duration};
use tempfile::TempDir;
use tokio::{runtime::Runtime, time};
#[cfg(test)]
mod client;
/// Maximum duration of single wait call.
const MAX_WAIT_TIME: Duration = Duration::from_secs(60 * 3);
struct TestNet<E, F, U> {
runtime: Runtime,
authority_nodes: Vec<(usize, F, U, MultiaddrWithPeerId)>,
full_nodes: Vec<(usize, F, U, MultiaddrWithPeerId)>,
chain_spec: GenericChainSpec<E>,
base_port: u16,
nodes: usize,
}
impl<E, F, U> Drop for TestNet<E, F, U> {
fn drop(&mut self) {
// Drop the nodes before dropping the runtime, as the runtime otherwise waits for all
// futures to be ended and we run into a dead lock.
self.full_nodes.drain(..);
self.authority_nodes.drain(..);
}
}
pub trait TestNetNode: Clone + Future<Output = Result<(), Error>> + Send + 'static {
type Block: BlockT;
type Backend: Backend<Self::Block>;
type Executor: CallExecutor<Self::Block> + Send + Sync;
type RuntimeApi: Send + Sync;
type TransactionPool: TransactionPool<Block = Self::Block>;
fn client(&self) -> Arc<Client<Self::Backend, Self::Executor, Self::Block, Self::RuntimeApi>>;
fn transaction_pool(&self) -> Arc<Self::TransactionPool>;
fn network(&self) -> Arc<dyn pezsc_network::service::traits::NetworkService>;
fn sync(&self) -> &Arc<SyncingService<Self::Block>>;
fn spawn_handle(&self) -> SpawnTaskHandle;
}
pub struct TestNetComponents<TBl: BlockT, TBackend, TExec, TRtApi, TExPool> {
task_manager: Arc<Mutex<TaskManager>>,
client: Arc<Client<TBackend, TExec, TBl, TRtApi>>,
transaction_pool: Arc<TExPool>,
network: Arc<dyn pezsc_network::service::traits::NetworkService>,
sync: Arc<SyncingService<TBl>>,
}
impl<TBl: BlockT, TBackend, TExec, TRtApi, TExPool>
TestNetComponents<TBl, TBackend, TExec, TRtApi, TExPool>
{
pub fn new(
task_manager: TaskManager,
client: Arc<Client<TBackend, TExec, TBl, TRtApi>>,
network: Arc<dyn pezsc_network::service::traits::NetworkService>,
sync: Arc<SyncingService<TBl>>,
transaction_pool: Arc<TExPool>,
) -> Self {
Self {
client,
sync,
transaction_pool,
network,
task_manager: Arc::new(Mutex::new(task_manager)),
}
}
}
impl<TBl: BlockT, TBackend, TExec, TRtApi, TExPool> Clone
for TestNetComponents<TBl, TBackend, TExec, TRtApi, TExPool>
{
fn clone(&self) -> Self {
Self {
task_manager: self.task_manager.clone(),
client: self.client.clone(),
transaction_pool: self.transaction_pool.clone(),
network: self.network.clone(),
sync: self.sync.clone(),
}
}
}
impl<TBl: BlockT, TBackend, TExec, TRtApi, TExPool> Future
for TestNetComponents<TBl, TBackend, TExec, TRtApi, TExPool>
{
type Output = Result<(), Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
Pin::new(&mut self.task_manager.lock().future()).poll(cx)
}
}
impl<TBl, TBackend, TExec, TRtApi, TExPool> TestNetNode
for TestNetComponents<TBl, TBackend, TExec, TRtApi, TExPool>
where
TBl: BlockT,
TBackend: pezsc_client_api::Backend<TBl> + Send + Sync + 'static,
TExec: CallExecutor<TBl> + Send + Sync + 'static,
TRtApi: Send + Sync + 'static,
TExPool: TransactionPool<Block = TBl> + Send + Sync + 'static,
{
type Block = TBl;
type Backend = TBackend;
type Executor = TExec;
type RuntimeApi = TRtApi;
type TransactionPool = TExPool;
fn client(&self) -> Arc<Client<Self::Backend, Self::Executor, Self::Block, Self::RuntimeApi>> {
self.client.clone()
}
fn transaction_pool(&self) -> Arc<Self::TransactionPool> {
self.transaction_pool.clone()
}
fn network(&self) -> Arc<dyn pezsc_network::service::traits::NetworkService> {
self.network.clone()
}
fn sync(&self) -> &Arc<SyncingService<Self::Block>> {
&self.sync
}
fn spawn_handle(&self) -> SpawnTaskHandle {
self.task_manager.lock().spawn_handle()
}
}
impl<E, F, U> TestNet<E, F, U>
where
F: Clone + Send + 'static,
U: Clone + Send + 'static,
{
pub fn run_until_all_full<FP>(&mut self, full_predicate: FP)
where
FP: Send + Fn(usize, &F) -> bool + 'static,
{
let full_nodes = self.full_nodes.clone();
let future = async move {
let mut interval = time::interval(Duration::from_millis(100));
loop {
interval.tick().await;
if full_nodes.iter().all(|(id, service, _, _)| full_predicate(*id, service)) {
break;
}
}
};
if self
.runtime
.block_on(async move { time::timeout(MAX_WAIT_TIME, future).await })
.is_err()
{
panic!("Waited for too long");
}
}
}
fn node_config<E: ChainSpecExtension + Clone + 'static + Send + Sync>(
index: usize,
spec: &GenericChainSpec<E>,
role: Role,
tokio_handle: tokio::runtime::Handle,
key_seed: Option<String>,
base_port: u16,
root: &TempDir,
) -> Configuration {
let root = root.path().join(format!("node-{}", index));
let mut network_config = NetworkConfiguration::new(
format!("Node {}", index),
"network/test/0.1",
Default::default(),
None,
);
network_config.allow_non_globals_in_dht = true;
network_config.listen_addresses.push(
iter::once(multiaddr::Protocol::Ip4(Ipv4Addr::new(127, 0, 0, 1)))
.chain(iter::once(multiaddr::Protocol::Tcp(base_port + index as u16)))
.collect(),
);
network_config.transport =
TransportConfig::Normal { enable_mdns: false, allow_private_ip: true };
Configuration {
impl_name: String::from("network-test-impl"),
impl_version: String::from("0.1"),
role,
tokio_handle,
transaction_pool: Default::default(),
network: network_config,
keystore: KeystoreConfig::Path { path: root.join("key"), password: None },
database: DatabaseSource::RocksDb { path: root.join("db"), cache_size: 128 },
trie_cache_maximum_size: Some(16 * 1024 * 1024),
warm_up_trie_cache: None,
state_pruning: Default::default(),
blocks_pruning: BlocksPruning::KeepFinalized,
chain_spec: Box::new((*spec).clone()),
executor: ExecutorConfiguration::default(),
wasm_runtime_overrides: Default::default(),
rpc: RpcConfiguration {
addr: Default::default(),
max_connections: Default::default(),
cors: None,
methods: Default::default(),
max_request_size: Default::default(),
max_response_size: Default::default(),
id_provider: Default::default(),
max_subs_per_conn: Default::default(),
port: 9944,
message_buffer_capacity: Default::default(),
batch_config: RpcBatchRequestConfig::Unlimited,
rate_limit: None,
rate_limit_whitelisted_ips: Default::default(),
rate_limit_trust_proxy_headers: Default::default(),
request_logger_limit: 1024,
},
prometheus_config: None,
telemetry_endpoints: None,
offchain_worker: Default::default(),
force_authoring: false,
disable_grandpa: false,
dev_key_seed: key_seed,
tracing_targets: None,
tracing_receiver: Default::default(),
announce_block: true,
base_path: BasePath::new(root.clone()),
data_path: root,
}
}
impl<E, F, U> TestNet<E, F, U>
where
F: TestNetNode,
E: ChainSpecExtension + Clone + 'static + Send + Sync,
{
fn new(
temp: &TempDir,
spec: GenericChainSpec<E>,
full: impl Iterator<Item = impl FnOnce(Configuration) -> Result<(F, U), Error>>,
authorities: impl Iterator<Item = (String, impl FnOnce(Configuration) -> Result<(F, U), Error>)>,
base_port: u16,
) -> TestNet<E, F, U> {
pezsp_tracing::try_init_simple();
fdlimit::raise_fd_limit().unwrap();
let runtime = Runtime::new().expect("Error creating tokio runtime");
let mut net = TestNet {
runtime,
authority_nodes: Default::default(),
full_nodes: Default::default(),
chain_spec: spec,
base_port,
nodes: 0,
};
net.insert_nodes(temp, full, authorities);
net
}
fn insert_nodes(
&mut self,
temp: &TempDir,
full: impl Iterator<Item = impl FnOnce(Configuration) -> Result<(F, U), Error>>,
authorities: impl Iterator<Item = (String, impl FnOnce(Configuration) -> Result<(F, U), Error>)>,
) {
self.runtime.block_on(async {
let handle = self.runtime.handle().clone();
for (key, authority) in authorities {
let node_config = node_config(
self.nodes,
&self.chain_spec,
Role::Authority,
handle.clone(),
Some(key),
self.base_port,
temp,
);
let addr = node_config.network.listen_addresses.first().unwrap().clone();
let (service, user_data) =
authority(node_config).expect("Error creating test node service");
handle.spawn(service.clone().map_err(|_| ()));
let addr = MultiaddrWithPeerId {
multiaddr: addr,
peer_id: service.network().local_peer_id(),
};
self.authority_nodes.push((self.nodes, service, user_data, addr));
self.nodes += 1;
}
for full in full {
let node_config = node_config(
self.nodes,
&self.chain_spec,
Role::Full,
handle.clone(),
None,
self.base_port,
temp,
);
let addr = node_config.network.listen_addresses.first().unwrap().clone();
let (service, user_data) =
full(node_config).expect("Error creating test node service");
handle.spawn(service.clone().map_err(|_| ()));
let addr = MultiaddrWithPeerId {
multiaddr: addr,
peer_id: service.network().local_peer_id(),
};
self.full_nodes.push((self.nodes, service, user_data, addr));
self.nodes += 1;
}
});
}
}
fn tempdir_with_prefix(prefix: &str) -> TempDir {
tempfile::Builder::new()
.prefix(prefix)
.tempdir()
.expect("Error creating test dir")
}
pub fn connectivity<E, Fb, F>(spec: GenericChainSpec<E>, full_builder: Fb)
where
E: ChainSpecExtension + Clone + 'static + Send + Sync,
Fb: Fn(Configuration) -> Result<F, Error>,
F: TestNetNode,
{
const NUM_FULL_NODES: usize = 5;
let expected_full_connections = NUM_FULL_NODES - 1;
{
let temp = tempdir_with_prefix("bizinikiwi-connectivity-test");
{
let mut network = TestNet::new(
&temp,
spec.clone(),
(0..NUM_FULL_NODES).map(|_| |cfg| full_builder(cfg).map(|s| (s, ()))),
// Note: this iterator is empty but we can't just use `iter::empty()`, otherwise
// the type of the closure cannot be inferred.
(0..0).map(|_| (String::new(), { |cfg| full_builder(cfg).map(|s| (s, ())) })),
30400,
);
info!("Checking star topology");
let first_address = network.full_nodes[0].3.clone();
for (_, service, _, _) in network.full_nodes.iter().skip(1) {
service
.network()
.add_reserved_peer(first_address.clone())
.expect("Error adding reserved peer");
}
network.run_until_all_full(move |_index, service| {
let connected = service.network().sync_num_connected();
debug!("Got {}/{} full connections...", connected, expected_full_connections);
connected == expected_full_connections
});
};
temp.close().expect("Error removing temp dir");
}
{
let temp = tempdir_with_prefix("bizinikiwi-connectivity-test");
{
let mut network = TestNet::new(
&temp,
spec,
(0..NUM_FULL_NODES).map(|_| |cfg| full_builder(cfg).map(|s| (s, ()))),
// Note: this iterator is empty but we can't just use `iter::empty()`, otherwise
// the type of the closure cannot be inferred.
(0..0).map(|_| (String::new(), { |cfg| full_builder(cfg).map(|s| (s, ())) })),
30400,
);
info!("Checking linked topology");
let mut address = network.full_nodes[0].3.clone();
for i in 0..NUM_FULL_NODES {
if i != 0 {
if let Some((_, service, _, node_id)) = network.full_nodes.get(i) {
service
.network()
.add_reserved_peer(address)
.expect("Error adding reserved peer");
address = node_id.clone();
}
}
}
network.run_until_all_full(move |_index, service| {
let connected = service.network().sync_num_connected();
debug!("Got {}/{} full connections...", connected, expected_full_connections);
connected == expected_full_connections
});
}
temp.close().expect("Error removing temp dir");
}
}
pub fn sync<E, Fb, F, B, ExF, U>(
spec: GenericChainSpec<E>,
full_builder: Fb,
mut make_block_and_import: B,
mut extrinsic_factory: ExF,
) where
Fb: Fn(Configuration) -> Result<(F, U), Error>,
F: TestNetNode,
B: FnMut(&F, &mut U),
ExF: FnMut(&F, &U) -> <F::Block as BlockT>::Extrinsic,
U: Clone + Send + 'static,
E: ChainSpecExtension + Clone + 'static + Send + Sync,
{
const NUM_FULL_NODES: usize = 10;
const NUM_BLOCKS: usize = 512;
let temp = tempdir_with_prefix("bizinikiwi-sync-test");
let mut network = TestNet::new(
&temp,
spec,
(0..NUM_FULL_NODES).map(|_| |cfg| full_builder(cfg)),
// Note: this iterator is empty but we can't just use `iter::empty()`, otherwise
// the type of the closure cannot be inferred.
(0..0).map(|_| (String::new(), { |cfg| full_builder(cfg) })),
30500,
);
info!("Checking block sync");
let first_address = {
let &mut (_, ref first_service, ref mut first_user_data, _) = &mut network.full_nodes[0];
for i in 0..NUM_BLOCKS {
if i % 128 == 0 {
info!("Generating #{}", i + 1);
}
make_block_and_import(first_service, first_user_data);
}
let info = network.full_nodes[0].1.client().info();
network.full_nodes[0]
.1
.sync()
.new_best_block_imported(info.best_hash, info.best_number);
network.full_nodes[0].3.clone()
};
info!("Running sync");
for (_, service, _, _) in network.full_nodes.iter().skip(1) {
service
.network()
.add_reserved_peer(first_address.clone())
.expect("Error adding reserved peer");
}
network.run_until_all_full(|_index, service| {
service.client().info().best_number == (NUM_BLOCKS as u32).into()
});
info!("Checking extrinsic propagation");
let first_service = network.full_nodes[0].1.clone();
let first_user_data = &network.full_nodes[0].2;
let best_block = first_service.client().info().best_hash;
let extrinsic = extrinsic_factory(&first_service, first_user_data);
let source = pezsc_transaction_pool_api::TransactionSource::External;
futures::executor::block_on(
first_service.transaction_pool().submit_one(best_block, source, extrinsic),
)
.expect("failed to submit extrinsic");
network.run_until_all_full(|_index, service| service.transaction_pool().ready().count() == 1);
}
pub fn consensus<E, Fb, F>(
spec: GenericChainSpec<E>,
full_builder: Fb,
authorities: impl IntoIterator<Item = String>,
) where
Fb: Fn(Configuration) -> Result<F, Error>,
F: TestNetNode,
E: ChainSpecExtension + Clone + 'static + Send + Sync,
{
const NUM_FULL_NODES: usize = 10;
const NUM_BLOCKS: usize = 10; // 10 * 2 sec block production time = ~20 seconds
let temp = tempdir_with_prefix("bizinikiwi-consensus-test");
let mut network = TestNet::new(
&temp,
spec,
(0..NUM_FULL_NODES / 2).map(|_| |cfg| full_builder(cfg).map(|s| (s, ()))),
authorities
.into_iter()
.map(|key| (key, { |cfg| full_builder(cfg).map(|s| (s, ())) })),
30600,
);
info!("Checking consensus");
let first_address = network.authority_nodes[0].3.clone();
for (_, service, _, _) in network.full_nodes.iter() {
service
.network()
.add_reserved_peer(first_address.clone())
.expect("Error adding reserved peer");
}
for (_, service, _, _) in network.authority_nodes.iter().skip(1) {
service
.network()
.add_reserved_peer(first_address.clone())
.expect("Error adding reserved peer");
}
network.run_until_all_full(|_index, service| {
service.client().info().finalized_number >= (NUM_BLOCKS as u32 / 2).into()
});
info!("Adding more peers");
network.insert_nodes(
&temp,
(0..NUM_FULL_NODES / 2).map(|_| |cfg| full_builder(cfg).map(|s| (s, ()))),
// Note: this iterator is empty but we can't just use `iter::empty()`, otherwise
// the type of the closure cannot be inferred.
(0..0).map(|_| (String::new(), { |cfg| full_builder(cfg).map(|s| (s, ())) })),
);
for (_, service, _, _) in network.full_nodes.iter() {
service
.network()
.add_reserved_peer(first_address.clone())
.expect("Error adding reserved peer");
}
network.run_until_all_full(|_index, service| {
service.client().info().finalized_number >= (NUM_BLOCKS as u32).into()
});
}