feat: Rebrand Polkadot/Substrate references to PezkuwiChain

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

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

This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
2025-12-14 00:04:10 +03:00
parent 286de54384
commit 1c0e57d984
9084 changed files with 997839 additions and 997557 deletions
@@ -0,0 +1,391 @@
// 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 api required for the transaction pool.
use crate::{
common::{sliding_stat::DurationSlidingStats, STAT_SLIDING_WINDOW},
graph::ValidateTransactionPriority,
insert_and_log_throttled, LOG_TARGET, LOG_TARGET_STAT,
};
use async_trait::async_trait;
use codec::Encode;
use futures::future::{Future, FutureExt};
use prometheus_endpoint::Registry as PrometheusRegistry;
use pezsc_client_api::{blockchain::HeaderBackend, BlockBackend};
use pezsp_api::{ApiExt, ProvideRuntimeApi};
use pezsp_blockchain::{HeaderMetadata, TreeRoute};
use pezsp_core::traits::SpawnEssentialNamed;
use pezsp_runtime::{
generic::BlockId,
traits::{self, Block as BlockT, BlockIdTo},
transaction_validity::{TransactionSource, TransactionValidity},
};
use pezsp_transaction_pool::runtime_api::TaggedTransactionQueue;
use std::{
marker::PhantomData,
pin::Pin,
sync::Arc,
time::{Duration, Instant},
};
use tokio::sync::{mpsc, oneshot, Mutex};
use super::{
error::{self, Error},
metrics::{ApiMetrics, ApiMetricsExt},
};
use crate::graph;
use tracing::{trace, warn, Level};
/// The transaction pool logic for full client.
pub struct FullChainApi<Client, Block> {
client: Arc<Client>,
_marker: PhantomData<Block>,
metrics: Option<Arc<ApiMetrics>>,
validation_pool_normal: mpsc::Sender<Pin<Box<dyn Future<Output = ()> + Send>>>,
validation_pool_maintained: mpsc::Sender<Pin<Box<dyn Future<Output = ()> + Send>>>,
validate_transaction_normal_stats: DurationSlidingStats,
validate_transaction_maintained_stats: DurationSlidingStats,
}
/// Spawn a validation task that will be used by the transaction pool to validate transactions.
fn spawn_validation_pool_task(
name: &'static str,
receiver_normal: Arc<Mutex<mpsc::Receiver<Pin<Box<dyn Future<Output = ()> + Send>>>>>,
receiver_maintained: Arc<Mutex<mpsc::Receiver<Pin<Box<dyn Future<Output = ()> + Send>>>>>,
spawner: &impl SpawnEssentialNamed,
stats: DurationSlidingStats,
blocking_stats: DurationSlidingStats,
) {
spawner.spawn_essential_blocking(
name,
Some("transaction-pool"),
async move {
loop {
let start = Instant::now();
let task = {
let receiver_maintained = receiver_maintained.clone();
let receiver_normal = receiver_normal.clone();
tokio::select! {
Some(task) = async {
receiver_maintained.lock().await.recv().await
} => { task }
Some(task) = async {
receiver_normal.lock().await.recv().await
} => { task }
else => {
return
}
}
};
let blocking_duration = {
let start = Instant::now();
task.await;
start.elapsed()
};
insert_and_log_throttled!(
Level::DEBUG,
target:LOG_TARGET_STAT,
prefix:format!("validate_transaction_inner_stats"),
stats,
start.elapsed().into()
);
insert_and_log_throttled!(
Level::DEBUG,
target:LOG_TARGET_STAT,
prefix:format!("validate_transaction_blocking_stats"),
blocking_stats,
blocking_duration.into()
);
trace!(target:LOG_TARGET, duration=?start.elapsed(), "spawn_validation_pool_task");
}
}
.boxed(),
);
}
impl<Client, Block> FullChainApi<Client, Block> {
/// Create new transaction pool logic.
pub fn new(
client: Arc<Client>,
prometheus: Option<&PrometheusRegistry>,
spawner: &impl SpawnEssentialNamed,
) -> Self {
let stats = DurationSlidingStats::new(Duration::from_secs(STAT_SLIDING_WINDOW));
let blocking_stats = DurationSlidingStats::new(Duration::from_secs(STAT_SLIDING_WINDOW));
let metrics = prometheus.map(ApiMetrics::register).and_then(|r| match r {
Err(error) => {
warn!(
target: LOG_TARGET,
?error,
"Failed to register transaction pool API Prometheus metrics"
);
None
},
Ok(api) => Some(Arc::new(api)),
});
let (sender, receiver) = mpsc::channel(1);
let (sender_maintained, receiver_maintained) = mpsc::channel(1);
let receiver = Arc::new(Mutex::new(receiver));
let receiver_maintained = Arc::new(Mutex::new(receiver_maintained));
spawn_validation_pool_task(
"transaction-pool-task-0",
receiver.clone(),
receiver_maintained.clone(),
spawner,
stats.clone(),
blocking_stats.clone(),
);
spawn_validation_pool_task(
"transaction-pool-task-1",
receiver,
receiver_maintained,
spawner,
stats.clone(),
blocking_stats.clone(),
);
FullChainApi {
client,
validation_pool_normal: sender,
validation_pool_maintained: sender_maintained,
_marker: Default::default(),
metrics,
validate_transaction_normal_stats: DurationSlidingStats::new(Duration::from_secs(
STAT_SLIDING_WINDOW,
)),
validate_transaction_maintained_stats: DurationSlidingStats::new(Duration::from_secs(
STAT_SLIDING_WINDOW,
)),
}
}
}
#[async_trait]
impl<Client, Block> graph::ChainApi for FullChainApi<Client, Block>
where
Block: BlockT,
Client: ProvideRuntimeApi<Block>
+ BlockBackend<Block>
+ BlockIdTo<Block>
+ HeaderBackend<Block>
+ HeaderMetadata<Block, Error = pezsp_blockchain::Error>,
Client: Send + Sync + 'static,
Client::Api: TaggedTransactionQueue<Block>,
{
type Block = Block;
type Error = error::Error;
async fn block_body(
&self,
hash: Block::Hash,
) -> Result<Option<Vec<<Self::Block as BlockT>::Extrinsic>>, Self::Error> {
self.client.block_body(hash).map_err(error::Error::from)
}
async fn validate_transaction(
&self,
at: <Self::Block as BlockT>::Hash,
source: TransactionSource,
uxt: graph::ExtrinsicFor<Self>,
validation_priority: ValidateTransactionPriority,
) -> Result<TransactionValidity, Self::Error> {
let start = Instant::now();
let (tx, rx) = oneshot::channel();
let client = self.client.clone();
let (stats, validation_pool, prefix) =
if validation_priority == ValidateTransactionPriority::Maintained {
(
self.validate_transaction_maintained_stats.clone(),
self.validation_pool_maintained.clone(),
"validate_transaction_maintained_stats",
)
} else {
(
self.validate_transaction_normal_stats.clone(),
self.validation_pool_normal.clone(),
"validate_transaction_stats",
)
};
let metrics = self.metrics.clone();
metrics.report(|m| m.validations_scheduled.inc());
{
validation_pool
.send(
async move {
let res = validate_transaction_blocking(&*client, at, source, uxt);
let _ = tx.send(res);
metrics.report(|m| m.validations_finished.inc());
}
.boxed(),
)
.await
.map_err(|e| Error::RuntimeApi(format!("Validation pool down: {:?}", e)))?;
}
let validity = match rx.await {
Ok(r) => r,
Err(_) => Err(Error::RuntimeApi("Validation was canceled".into())),
};
insert_and_log_throttled!(
Level::DEBUG,
target:LOG_TARGET_STAT,
prefix:prefix,
stats,
start.elapsed().into()
);
validity
}
/// Validates a transaction by calling into the runtime.
///
/// Same as `validate_transaction` but blocks the current thread when performing validation.
fn validate_transaction_blocking(
&self,
at: Block::Hash,
source: TransactionSource,
uxt: graph::ExtrinsicFor<Self>,
) -> Result<TransactionValidity, Self::Error> {
validate_transaction_blocking(&*self.client, at, source, uxt)
}
fn block_id_to_number(
&self,
at: &BlockId<Self::Block>,
) -> Result<Option<graph::NumberFor<Self>>, Self::Error> {
self.client.to_number(at).map_err(|e| Error::BlockIdConversion(e.to_string()))
}
fn block_id_to_hash(
&self,
at: &BlockId<Self::Block>,
) -> Result<Option<graph::BlockHash<Self>>, Self::Error> {
self.client.to_hash(at).map_err(|e| Error::BlockIdConversion(e.to_string()))
}
fn hash_and_length(
&self,
ex: &graph::RawExtrinsicFor<Self>,
) -> (graph::ExtrinsicHash<Self>, usize) {
ex.using_encoded(|x| (<traits::HashingFor<Block> as traits::Hash>::hash(x), x.len()))
}
fn block_header(
&self,
hash: <Self::Block as BlockT>::Hash,
) -> Result<Option<<Self::Block as BlockT>::Header>, Self::Error> {
self.client.header(hash).map_err(Into::into)
}
fn tree_route(
&self,
from: <Self::Block as BlockT>::Hash,
to: <Self::Block as BlockT>::Hash,
) -> Result<TreeRoute<Self::Block>, Self::Error> {
pezsp_blockchain::tree_route::<Block, Client>(&*self.client, from, to).map_err(Into::into)
}
}
/// Helper function to validate a transaction using a full chain API.
/// This method will call into the runtime to perform the validation.
fn validate_transaction_blocking<Client, Block>(
client: &Client,
at: Block::Hash,
source: TransactionSource,
uxt: graph::ExtrinsicFor<FullChainApi<Client, Block>>,
) -> error::Result<TransactionValidity>
where
Block: BlockT,
Client: ProvideRuntimeApi<Block>
+ BlockBackend<Block>
+ BlockIdTo<Block>
+ HeaderBackend<Block>
+ HeaderMetadata<Block, Error = pezsp_blockchain::Error>,
Client: Send + Sync + 'static,
Client::Api: TaggedTransactionQueue<Block>,
{
let s = std::time::Instant::now();
let tx_hash = uxt.using_encoded(|x| <traits::HashingFor<Block> as traits::Hash>::hash(x));
let result = pezsp_tracing::within_span!(pezsp_tracing::Level::TRACE, "validate_transaction";
{
let runtime_api = client.runtime_api();
let api_version = pezsp_tracing::within_span! { pezsp_tracing::Level::TRACE, "check_version";
runtime_api
.api_version::<dyn TaggedTransactionQueue<Block>>(at)
.map_err(|e| Error::RuntimeApi(e.to_string()))?
.ok_or_else(|| Error::RuntimeApi(
format!("Could not find `TaggedTransactionQueue` api for block `{:?}`.", at)
))
}?;
use pezsp_api::Core;
pezsp_tracing::within_span!(
pezsp_tracing::Level::TRACE, "runtime::validate_transaction";
{
if api_version >= 3 {
runtime_api.validate_transaction(at, source, (*uxt).clone(), at)
.map_err(|e| Error::RuntimeApi(e.to_string()))
} else {
let block_number = client.to_number(&BlockId::Hash(at))
.map_err(|e| Error::RuntimeApi(e.to_string()))?
.ok_or_else(||
Error::RuntimeApi(format!("Could not get number for block `{:?}`.", at))
)?;
// The old versions require us to call `initialize_block` before.
runtime_api.initialize_block(at, &pezsp_runtime::traits::Header::new(
block_number + pezsp_runtime::traits::One::one(),
Default::default(),
Default::default(),
at,
Default::default()),
).map_err(|e| Error::RuntimeApi(e.to_string()))?;
if api_version == 2 {
#[allow(deprecated)] // old validate_transaction
runtime_api.validate_transaction_before_version_3(at, source, (*uxt).clone())
.map_err(|e| Error::RuntimeApi(e.to_string()))
} else {
#[allow(deprecated)] // old validate_transaction
runtime_api.validate_transaction_before_version_2(at, (*uxt).clone())
.map_err(|e| Error::RuntimeApi(e.to_string()))
}
}
})
});
trace!(
target: LOG_TARGET,
?tx_hash,
?at,
duration = ?s.elapsed(),
"validate_transaction_blocking"
);
result
}
@@ -0,0 +1,701 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! Bizinikiwi transaction pool implementation.
use crate::LOG_TARGET;
use pezsc_transaction_pool_api::ChainEvent;
use pezsp_blockchain::TreeRoute;
use pezsp_runtime::traits::{Block as BlockT, NumberFor, Saturating};
use tracing::{debug, trace};
/// The threshold since the last update where we will skip any maintenance for blocks.
///
/// This includes tracking re-orgs and sending out certain notifications. In general this shouldn't
/// happen and may only happen when the node is doing a full sync.
const SKIP_MAINTENANCE_THRESHOLD: u16 = 20;
/// Helper struct for keeping track of the current state of processed new best
/// block and finalized events. The main purpose of keeping track of this state
/// is to figure out which phases (enactment / finalization) of transaction pool
/// maintenance are needed.
///
/// Example: given the following chain:
///
/// B1-C1-D1-E1
/// /
/// A
/// \
/// B2-C2-D2-E2
///
/// the list presents scenarios and expected behavior for sequence of `NewBestBlock` (`nbb`)
/// and `Finalized` (`f`) events. true/false means if enactiment is required:
///
/// - `nbb(C1)`, `f(C1)` -> false (enactment was already performed in `nbb(C1))`
/// - `f(C1)`, `nbb(C1)` -> false (enactment was already performed in `f(C1))`
/// - `f(C1)`, `nbb(D2)` -> false (enactment was already performed in `f(C1)`,
/// we should not retract finalized block)
/// - `f(C1)`, `f(C2)`, `nbb(C1)` -> false
/// - `nbb(C1)`, `nbb(C2)` -> true (switching fork is OK)
/// - `nbb(B1)`, `nbb(B2)` -> true
/// - `nbb(B1)`, `nbb(C1)`, `f(C1)` -> false (enactment was already performed in `nbb(B1)`)
/// - `nbb(C1)`, `f(B1)` -> false (enactment was already performed in `nbb(B2)`)
pub struct EnactmentState<Block>
where
Block: BlockT,
{
recent_best_block: Block::Hash,
recent_finalized_block: Block::Hash,
}
/// Enactment action that should be performed after processing the `ChainEvent`
#[derive(Debug)]
pub enum EnactmentAction<Block: BlockT> {
/// Both phases of maintenance shall be skipped
Skip,
/// Both phases of maintenance shall be performed
HandleEnactment(TreeRoute<Block>),
/// Enactment phase of maintenance shall be skipped
HandleFinalization,
}
impl<Block> EnactmentState<Block>
where
Block: BlockT,
{
/// Returns a new `EnactmentState` initialized with the given parameters.
pub fn new(recent_best_block: Block::Hash, recent_finalized_block: Block::Hash) -> Self {
EnactmentState { recent_best_block, recent_finalized_block }
}
/// Returns the recently finalized block.
pub fn recent_finalized_block(&self) -> Block::Hash {
self.recent_finalized_block
}
/// Updates the state according to the given `ChainEvent`, returning
/// `Some(tree_route)` with a tree route including the blocks that need to
/// be enacted/retracted. If no enactment is needed then `None` is returned.
pub fn update<TreeRouteF, BlockNumberF>(
&mut self,
event: &ChainEvent<Block>,
tree_route: &TreeRouteF,
hash_to_number: &BlockNumberF,
) -> Result<EnactmentAction<Block>, String>
where
TreeRouteF: Fn(Block::Hash, Block::Hash) -> Result<TreeRoute<Block>, String>,
BlockNumberF: Fn(Block::Hash) -> Result<Option<NumberFor<Block>>, String>,
{
let new_hash = event.hash();
let finalized = event.is_finalized();
// do not proceed with txpool maintain if block distance is too high
let skip_maintenance =
match (hash_to_number(new_hash), hash_to_number(self.recent_best_block)) {
(Ok(Some(new)), Ok(Some(current))) =>
new.saturating_sub(current) > SKIP_MAINTENANCE_THRESHOLD.into(),
_ => true,
};
if skip_maintenance {
debug!(target: LOG_TARGET, "skip maintain: tree_route would be too long");
self.force_update(event);
return Ok(EnactmentAction::Skip);
}
// block was already finalized
if self.recent_finalized_block == new_hash {
trace!(target: LOG_TARGET, "handle_enactment: block already finalized");
return Ok(EnactmentAction::Skip);
}
// compute actual tree route from best_block to notified block, and use
// it instead of tree_route provided with event
let tree_route = tree_route(self.recent_best_block, new_hash)?;
trace!(
target: LOG_TARGET,
?new_hash,
?finalized,
common_block = ?tree_route.common_block(),
last_block = ?tree_route.last(),
best_block = ?self.recent_best_block,
finalized_block = ?self.recent_finalized_block,
"resolve hash"
);
// check if recently finalized block is on retracted path. this could be
// happening if we first received a finalization event and then a new
// best event for some old stale best head.
if tree_route.retracted().iter().any(|x| x.hash == self.recent_finalized_block) {
trace!(
target: LOG_TARGET,
recent_finalized_block = ?self.recent_finalized_block,
?new_hash,
"Recently finalized block would be retracted by ChainEvent, skipping"
);
return Ok(EnactmentAction::Skip);
}
if finalized {
self.recent_finalized_block = new_hash;
// if there are no enacted blocks in best_block -> hash tree_route,
// it means that block being finalized was already enacted (this
// case also covers best_block == new_hash), recent_best_block
// remains valid.
if tree_route.enacted().is_empty() {
trace!(target: LOG_TARGET, "handle_enactment: no newly enacted blocks since recent best block");
return Ok(EnactmentAction::HandleFinalization);
}
// otherwise enacted finalized block becomes best block...
}
self.recent_best_block = new_hash;
Ok(EnactmentAction::HandleEnactment(tree_route))
}
/// Forces update of the state according to the given `ChainEvent`. Intended to be used as a
/// fallback when tree_route cannot be computed.
pub fn force_update(&mut self, event: &ChainEvent<Block>) {
match event {
ChainEvent::NewBestBlock { hash, .. } => self.recent_best_block = *hash,
ChainEvent::Finalized { hash, .. } => self.recent_finalized_block = *hash,
};
trace!(
target: LOG_TARGET,
recent_best_block = ?self.recent_best_block,
recent_finalized_block = ?self.recent_finalized_block,
"forced update"
);
}
}
#[cfg(test)]
mod enactment_state_tests {
use super::{EnactmentAction, EnactmentState};
use pezsc_transaction_pool_api::ChainEvent;
use pezsp_blockchain::{HashAndNumber, TreeRoute};
use pezsp_runtime::traits::NumberFor;
use std::sync::Arc;
use bizinikiwi_test_runtime_client::runtime::{Block, Hash};
// some helpers for convenient blocks' hash naming
fn a() -> HashAndNumber<Block> {
HashAndNumber { number: 1, hash: Hash::from([0xAA; 32]) }
}
fn b1() -> HashAndNumber<Block> {
HashAndNumber { number: 2, hash: Hash::from([0xB1; 32]) }
}
fn c1() -> HashAndNumber<Block> {
HashAndNumber { number: 3, hash: Hash::from([0xC1; 32]) }
}
fn d1() -> HashAndNumber<Block> {
HashAndNumber { number: 4, hash: Hash::from([0xD1; 32]) }
}
fn e1() -> HashAndNumber<Block> {
HashAndNumber { number: 5, hash: Hash::from([0xE1; 32]) }
}
fn x1() -> HashAndNumber<Block> {
HashAndNumber { number: 22, hash: Hash::from([0x1E; 32]) }
}
fn b2() -> HashAndNumber<Block> {
HashAndNumber { number: 2, hash: Hash::from([0xB2; 32]) }
}
fn c2() -> HashAndNumber<Block> {
HashAndNumber { number: 3, hash: Hash::from([0xC2; 32]) }
}
fn d2() -> HashAndNumber<Block> {
HashAndNumber { number: 4, hash: Hash::from([0xD2; 32]) }
}
fn e2() -> HashAndNumber<Block> {
HashAndNumber { number: 5, hash: Hash::from([0xE2; 32]) }
}
fn x2() -> HashAndNumber<Block> {
HashAndNumber { number: 22, hash: Hash::from([0x2E; 32]) }
}
fn test_chain() -> Vec<HashAndNumber<Block>> {
vec![x1(), e1(), d1(), c1(), b1(), a(), b2(), c2(), d2(), e2(), x2()]
}
fn block_hash_to_block_number(hash: Hash) -> Result<Option<NumberFor<Block>>, String> {
Ok(test_chain().iter().find(|x| x.hash == hash).map(|x| x.number))
}
/// mock tree_route computing function for simple two-forks chain
fn tree_route(from: Hash, to: Hash) -> Result<TreeRoute<Block>, String> {
let chain = test_chain();
let pivot = chain.iter().position(|x| x.number == a().number).unwrap();
let from = chain
.iter()
.position(|bn| bn.hash == from)
.ok_or("existing block should be given")?;
let to = chain
.iter()
.position(|bn| bn.hash == to)
.ok_or("existing block should be given")?;
// B1-C1-D1-E1-..-X1
// /
// A
// \
// B2-C2-D2-E2-..-X2
//
// [X1 E1 D1 C1 B1 A B2 C2 D2 E2 X2]
let vec: Vec<HashAndNumber<Block>> = if from < to {
chain.into_iter().skip(from).take(to - from + 1).collect()
} else {
chain.into_iter().skip(to).take(from - to + 1).rev().collect()
};
let pivot = if from <= pivot && to <= pivot {
if from < to {
to - from
} else {
0
}
} else if from >= pivot && to >= pivot {
if from < to {
0
} else {
from - to
}
} else {
if from < to {
pivot - from
} else {
from - pivot
}
};
TreeRoute::new(vec, pivot)
}
mod mock_tree_route_tests {
use super::*;
/// asserts that tree routes are equal
fn assert_tree_route_eq(
expected: Result<TreeRoute<Block>, String>,
result: Result<TreeRoute<Block>, String>,
) {
let expected = expected.unwrap();
let result = result.unwrap();
assert_eq!(result.common_block().hash, expected.common_block().hash);
assert_eq!(result.enacted().len(), expected.enacted().len());
assert_eq!(result.retracted().len(), expected.retracted().len());
assert!(result
.enacted()
.iter()
.zip(expected.enacted().iter())
.all(|(a, b)| a.hash == b.hash));
assert!(result
.retracted()
.iter()
.zip(expected.retracted().iter())
.all(|(a, b)| a.hash == b.hash));
}
// some tests for mock tree_route function
#[test]
fn tree_route_mock_test_01() {
let result = tree_route(b1().hash, a().hash);
let expected = TreeRoute::new(vec![b1(), a()], 1);
assert_tree_route_eq(result, expected);
}
#[test]
fn tree_route_mock_test_02() {
let result = tree_route(a().hash, b1().hash);
let expected = TreeRoute::new(vec![a(), b1()], 0);
assert_tree_route_eq(result, expected);
}
#[test]
fn tree_route_mock_test_03() {
let result = tree_route(a().hash, c2().hash);
let expected = TreeRoute::new(vec![a(), b2(), c2()], 0);
assert_tree_route_eq(result, expected);
}
#[test]
fn tree_route_mock_test_04() {
let result = tree_route(e2().hash, a().hash);
let expected = TreeRoute::new(vec![e2(), d2(), c2(), b2(), a()], 4);
assert_tree_route_eq(result, expected);
}
#[test]
fn tree_route_mock_test_05() {
let result = tree_route(d1().hash, b1().hash);
let expected = TreeRoute::new(vec![d1(), c1(), b1()], 2);
assert_tree_route_eq(result, expected);
}
#[test]
fn tree_route_mock_test_06() {
let result = tree_route(d2().hash, b2().hash);
let expected = TreeRoute::new(vec![d2(), c2(), b2()], 2);
assert_tree_route_eq(result, expected);
}
#[test]
fn tree_route_mock_test_07() {
let result = tree_route(b1().hash, d1().hash);
let expected = TreeRoute::new(vec![b1(), c1(), d1()], 0);
assert_tree_route_eq(result, expected);
}
#[test]
fn tree_route_mock_test_08() {
let result = tree_route(b2().hash, d2().hash);
let expected = TreeRoute::new(vec![b2(), c2(), d2()], 0);
assert_tree_route_eq(result, expected);
}
#[test]
fn tree_route_mock_test_09() {
let result = tree_route(e2().hash, e1().hash);
let expected =
TreeRoute::new(vec![e2(), d2(), c2(), b2(), a(), b1(), c1(), d1(), e1()], 4);
assert_tree_route_eq(result, expected);
}
#[test]
fn tree_route_mock_test_10() {
let result = tree_route(e1().hash, e2().hash);
let expected =
TreeRoute::new(vec![e1(), d1(), c1(), b1(), a(), b2(), c2(), d2(), e2()], 4);
assert_tree_route_eq(result, expected);
}
#[test]
fn tree_route_mock_test_11() {
let result = tree_route(b1().hash, c2().hash);
let expected = TreeRoute::new(vec![b1(), a(), b2(), c2()], 1);
assert_tree_route_eq(result, expected);
}
#[test]
fn tree_route_mock_test_12() {
let result = tree_route(d2().hash, b1().hash);
let expected = TreeRoute::new(vec![d2(), c2(), b2(), a(), b1()], 3);
assert_tree_route_eq(result, expected);
}
#[test]
fn tree_route_mock_test_13() {
let result = tree_route(c2().hash, e1().hash);
let expected = TreeRoute::new(vec![c2(), b2(), a(), b1(), c1(), d1(), e1()], 2);
assert_tree_route_eq(result, expected);
}
#[test]
fn tree_route_mock_test_14() {
let result = tree_route(b1().hash, b1().hash);
let expected = TreeRoute::new(vec![b1()], 0);
assert_tree_route_eq(result, expected);
}
#[test]
fn tree_route_mock_test_15() {
let result = tree_route(b2().hash, b2().hash);
let expected = TreeRoute::new(vec![b2()], 0);
assert_tree_route_eq(result, expected);
}
#[test]
fn tree_route_mock_test_16() {
let result = tree_route(a().hash, a().hash);
let expected = TreeRoute::new(vec![a()], 0);
assert_tree_route_eq(result, expected);
}
#[test]
fn tree_route_mock_test_17() {
let result = tree_route(x2().hash, b1().hash);
let expected = TreeRoute::new(vec![x2(), e2(), d2(), c2(), b2(), a(), b1()], 5);
assert_tree_route_eq(result, expected);
}
}
fn trigger_new_best_block(
state: &mut EnactmentState<Block>,
from: HashAndNumber<Block>,
acted_on: HashAndNumber<Block>,
) -> EnactmentAction<Block> {
let (from, acted_on) = (from.hash, acted_on.hash);
let event_tree_route = tree_route(from, acted_on).expect("Tree route exists");
state
.update(
&ChainEvent::NewBestBlock {
hash: acted_on,
tree_route: Some(Arc::new(event_tree_route)),
},
&tree_route,
&block_hash_to_block_number,
)
.unwrap()
}
fn trigger_finalized(
state: &mut EnactmentState<Block>,
from: HashAndNumber<Block>,
acted_on: HashAndNumber<Block>,
) -> EnactmentAction<Block> {
let (from, acted_on) = (from.hash, acted_on.hash);
let v = tree_route(from, acted_on)
.expect("Tree route exists")
.enacted()
.iter()
.map(|h| h.hash)
.collect::<Vec<_>>();
state
.update(
&ChainEvent::Finalized { hash: acted_on, tree_route: v.into() },
&tree_route,
&block_hash_to_block_number,
)
.unwrap()
}
fn assert_es_eq(
es: &EnactmentState<Block>,
expected_best_block: HashAndNumber<Block>,
expected_finalized_block: HashAndNumber<Block>,
) {
assert_eq!(es.recent_best_block, expected_best_block.hash);
assert_eq!(es.recent_finalized_block, expected_finalized_block.hash);
}
#[test]
fn test_enactment_helper() {
pezsp_tracing::try_init_simple();
let mut es = EnactmentState::new(a().hash, a().hash);
// B1-C1-D1-E1
// /
// A
// \
// B2-C2-D2-E2
let result = trigger_new_best_block(&mut es, a(), d1());
assert!(matches!(result, EnactmentAction::HandleEnactment { .. }));
assert_es_eq(&es, d1(), a());
let result = trigger_new_best_block(&mut es, d1(), e1());
assert!(matches!(result, EnactmentAction::HandleEnactment { .. }));
assert_es_eq(&es, e1(), a());
let result = trigger_finalized(&mut es, a(), d2());
assert!(matches!(result, EnactmentAction::HandleEnactment { .. }));
assert_es_eq(&es, d2(), d2());
let result = trigger_new_best_block(&mut es, d2(), e1());
assert!(matches!(result, EnactmentAction::Skip));
assert_es_eq(&es, d2(), d2());
let result = trigger_finalized(&mut es, a(), b2());
assert!(matches!(result, EnactmentAction::Skip));
assert_es_eq(&es, d2(), d2());
let result = trigger_finalized(&mut es, a(), b1());
assert!(matches!(result, EnactmentAction::Skip));
assert_es_eq(&es, d2(), d2());
let result = trigger_new_best_block(&mut es, a(), d2());
assert!(matches!(result, EnactmentAction::Skip));
assert_es_eq(&es, d2(), d2());
let result = trigger_finalized(&mut es, a(), d2());
assert!(matches!(result, EnactmentAction::Skip));
assert_es_eq(&es, d2(), d2());
let result = trigger_new_best_block(&mut es, a(), c2());
assert!(matches!(result, EnactmentAction::Skip));
assert_es_eq(&es, d2(), d2());
let result = trigger_new_best_block(&mut es, a(), c1());
assert!(matches!(result, EnactmentAction::Skip));
assert_es_eq(&es, d2(), d2());
let result = trigger_new_best_block(&mut es, d2(), e2());
assert!(matches!(result, EnactmentAction::HandleEnactment { .. }));
assert_es_eq(&es, e2(), d2());
let result = trigger_finalized(&mut es, d2(), e2());
assert!(matches!(result, EnactmentAction::HandleFinalization));
assert_es_eq(&es, e2(), e2());
}
#[test]
fn test_enactment_helper_2() {
pezsp_tracing::try_init_simple();
let mut es = EnactmentState::new(a().hash, a().hash);
// A-B1-C1-D1-E1
let result = trigger_new_best_block(&mut es, a(), b1());
assert!(matches!(result, EnactmentAction::HandleEnactment { .. }));
assert_es_eq(&es, b1(), a());
let result = trigger_new_best_block(&mut es, b1(), c1());
assert!(matches!(result, EnactmentAction::HandleEnactment { .. }));
assert_es_eq(&es, c1(), a());
let result = trigger_new_best_block(&mut es, c1(), d1());
assert!(matches!(result, EnactmentAction::HandleEnactment { .. }));
assert_es_eq(&es, d1(), a());
let result = trigger_new_best_block(&mut es, d1(), e1());
assert!(matches!(result, EnactmentAction::HandleEnactment { .. }));
assert_es_eq(&es, e1(), a());
let result = trigger_finalized(&mut es, a(), c1());
assert!(matches!(result, EnactmentAction::HandleFinalization));
assert_es_eq(&es, e1(), c1());
let result = trigger_finalized(&mut es, c1(), e1());
assert!(matches!(result, EnactmentAction::HandleFinalization));
assert_es_eq(&es, e1(), e1());
}
#[test]
fn test_enactment_helper_3() {
pezsp_tracing::try_init_simple();
let mut es = EnactmentState::new(a().hash, a().hash);
// A-B1-C1-D1-E1
let result = trigger_new_best_block(&mut es, a(), e1());
assert!(matches!(result, EnactmentAction::HandleEnactment { .. }));
assert_es_eq(&es, e1(), a());
let result = trigger_finalized(&mut es, a(), b1());
assert!(matches!(result, EnactmentAction::HandleFinalization));
assert_es_eq(&es, e1(), b1());
}
#[test]
fn test_enactment_helper_4() {
pezsp_tracing::try_init_simple();
let mut es = EnactmentState::new(a().hash, a().hash);
// A-B1-C1-D1-E1
let result = trigger_finalized(&mut es, a(), e1());
assert!(matches!(result, EnactmentAction::HandleEnactment { .. }));
assert_es_eq(&es, e1(), e1());
let result = trigger_finalized(&mut es, e1(), b1());
assert!(matches!(result, EnactmentAction::Skip));
assert_es_eq(&es, e1(), e1());
}
#[test]
fn test_enactment_helper_5() {
pezsp_tracing::try_init_simple();
let mut es = EnactmentState::new(a().hash, a().hash);
// B1-C1-D1-E1
// /
// A
// \
// B2-C2-D2-E2
let result = trigger_finalized(&mut es, a(), e1());
assert!(matches!(result, EnactmentAction::HandleEnactment { .. }));
assert_es_eq(&es, e1(), e1());
let result = trigger_finalized(&mut es, e1(), e2());
assert!(matches!(result, EnactmentAction::Skip));
assert_es_eq(&es, e1(), e1());
}
#[test]
fn test_enactment_helper_6() {
pezsp_tracing::try_init_simple();
let mut es = EnactmentState::new(a().hash, a().hash);
// A-B1-C1-D1-E1
let result = trigger_new_best_block(&mut es, a(), b1());
assert!(matches!(result, EnactmentAction::HandleEnactment { .. }));
assert_es_eq(&es, b1(), a());
let result = trigger_finalized(&mut es, a(), d1());
assert!(matches!(result, EnactmentAction::HandleEnactment { .. }));
assert_es_eq(&es, d1(), d1());
let result = trigger_new_best_block(&mut es, a(), e1());
assert!(matches!(result, EnactmentAction::HandleEnactment { .. }));
assert_es_eq(&es, e1(), d1());
let result = trigger_new_best_block(&mut es, a(), c1());
assert!(matches!(result, EnactmentAction::Skip));
assert_es_eq(&es, e1(), d1());
}
#[test]
fn test_enactment_forced_update_best_block() {
pezsp_tracing::try_init_simple();
let mut es = EnactmentState::new(a().hash, a().hash);
es.force_update(&ChainEvent::NewBestBlock { hash: b1().hash, tree_route: None });
assert_es_eq(&es, b1(), a());
}
#[test]
fn test_enactment_forced_update_finalize() {
pezsp_tracing::try_init_simple();
let mut es = EnactmentState::new(a().hash, a().hash);
es.force_update(&ChainEvent::Finalized { hash: b1().hash, tree_route: Arc::from([]) });
assert_es_eq(&es, a(), b1());
}
#[test]
fn test_enactment_skip_long_enacted_path() {
pezsp_tracing::try_init_simple();
let mut es = EnactmentState::new(a().hash, a().hash);
// A-B1-C1-..-X1
let result = trigger_new_best_block(&mut es, a(), x1());
assert!(matches!(result, EnactmentAction::Skip));
assert_es_eq(&es, x1(), a());
}
#[test]
fn test_enactment_proceed_with_enacted_path_at_threshold() {
pezsp_tracing::try_init_simple();
let mut es = EnactmentState::new(b1().hash, b1().hash);
// A-B1-C1-..-X1
let result = trigger_new_best_block(&mut es, b1(), x1());
assert!(matches!(result, EnactmentAction::HandleEnactment { .. }));
assert_es_eq(&es, x1(), b1());
}
}
@@ -0,0 +1,50 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! Transaction pool error.
use pezsc_transaction_pool_api::error::Error as TxPoolError;
/// Transaction pool result.
pub type Result<T> = std::result::Result<T, Error>;
/// Transaction pool error type.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("Transaction pool error: {0}")]
Pool(#[from] TxPoolError),
#[error("Blockchain error: {0}")]
Blockchain(#[from] pezsp_blockchain::Error),
#[error("Block conversion error: {0}")]
BlockIdConversion(String),
#[error("Runtime error: {0}")]
RuntimeApi(String),
}
impl pezsc_transaction_pool_api::error::IntoPoolError for Error {
fn into_pool_error(self) -> std::result::Result<TxPoolError, Self> {
match self {
Error::Pool(e) => Ok(e),
e => Err(e),
}
}
}
@@ -0,0 +1,109 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! Transaction pool Prometheus metrics for implementation of Chain API.
use prometheus_endpoint::{register, Counter, PrometheusError, Registry, U64};
use std::sync::Arc;
use crate::LOG_TARGET;
/// Provides interface to register the specific metrics in the Prometheus register.
pub(crate) trait MetricsRegistrant {
/// Registers the metrics at given Prometheus registry.
fn register(registry: &Registry) -> Result<Box<Self>, PrometheusError>;
}
/// Generic structure to keep a link to metrics register.
pub(crate) struct GenericMetricsLink<M: MetricsRegistrant>(Arc<Option<Box<M>>>);
impl<M: MetricsRegistrant> Default for GenericMetricsLink<M> {
fn default() -> Self {
Self(Arc::from(None))
}
}
impl<M: MetricsRegistrant> Clone for GenericMetricsLink<M> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<M: MetricsRegistrant> GenericMetricsLink<M> {
pub fn new(registry: Option<&Registry>) -> Self {
Self(Arc::new(registry.and_then(|registry| {
M::register(registry)
.map_err(|error| {
tracing::warn!(
target: LOG_TARGET,
%error,
"Failed to register prometheus metrics"
);
})
.ok()
})))
}
pub fn report(&self, do_this: impl FnOnce(&M)) {
if let Some(metrics) = self.0.as_ref() {
do_this(&**metrics);
}
}
}
/// Transaction pool api Prometheus metrics.
pub struct ApiMetrics {
pub validations_scheduled: Counter<U64>,
pub validations_finished: Counter<U64>,
}
impl ApiMetrics {
/// Register the metrics at the given Prometheus registry.
pub fn register(registry: &Registry) -> Result<Self, PrometheusError> {
Ok(Self {
validations_scheduled: register(
Counter::new(
"bizinikiwi_sub_txpool_validations_scheduled",
"Total number of transactions scheduled for validation",
)?,
registry,
)?,
validations_finished: register(
Counter::new(
"bizinikiwi_sub_txpool_validations_finished",
"Total number of transactions that finished validation",
)?,
registry,
)?,
})
}
}
/// An extension trait for [`ApiMetrics`].
pub trait ApiMetricsExt {
/// Report an event to the metrics.
fn report(&self, report: impl FnOnce(&ApiMetrics));
}
impl ApiMetricsExt for Option<Arc<ApiMetrics>> {
fn report(&self, report: impl FnOnce(&ApiMetrics)) {
if let Some(metrics) = self.as_ref() {
report(metrics)
}
}
}
@@ -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/>.
//! Common components re-used across different txpool implementations.
pub(crate) mod api;
pub(crate) mod enactment_state;
pub(crate) mod error;
pub(crate) mod metrics;
pub(crate) mod sliding_stat;
#[cfg(test)]
pub(crate) mod tests;
pub(crate) mod tracing_log_xt;
use futures::StreamExt;
use std::sync::Arc;
/// Stat sliding window, in seconds for per-transaction activities.
pub(crate) const STAT_SLIDING_WINDOW: u64 = 3;
/// Inform the transaction pool about imported and finalized blocks.
pub async fn notification_future<Client, Pool, Block>(client: Arc<Client>, txpool: Arc<Pool>)
where
Block: pezsp_runtime::traits::Block,
Client: pezsc_client_api::BlockchainEvents<Block>,
Pool: pezsc_transaction_pool_api::MaintainedTransactionPool<Block = Block>,
{
let import_stream = client
.import_notification_stream()
.filter_map(|n| futures::future::ready(n.try_into().ok()))
.fuse();
let finality_stream = client.finality_notification_stream().map(Into::into).fuse();
futures::stream::select(import_stream, finality_stream)
.for_each(|evt| txpool.maintain(evt))
.await
}
@@ -0,0 +1,617 @@
// 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/>.
//! Logging helper. Sliding window statistics with retention-based pruning.
//!
//! `SlidingStats<T>` tracks timestamped values and computes statistical summaries
//! (min, max, average, percentiles, count) over a rolling time window.
//!
//! Old entries are automatically pruned based on a configurable retention `Duration`.
//! Values can be logged periodically using `insert_with_log` or the `insert_and_log_throttled!`
//! macro.
use std::{
collections::{BTreeSet, HashMap, VecDeque},
fmt::Display,
sync::Arc,
time::{Duration, Instant},
};
use tokio::sync::RwLock;
mod sealed {
pub trait HasDefaultStatFormatter {}
}
impl sealed::HasDefaultStatFormatter for u32 {}
impl sealed::HasDefaultStatFormatter for i64 {}
pub trait StatFormatter {
fn format_stat(value: f64) -> String;
}
impl<T> StatFormatter for T
where
T: Display + sealed::HasDefaultStatFormatter,
{
fn format_stat(value: f64) -> String {
format!("{value:.2}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct StatDuration(pub std::time::Duration);
impl Into<f64> for StatDuration {
fn into(self) -> f64 {
self.0.as_secs_f64()
}
}
impl Into<StatDuration> for Duration {
fn into(self) -> StatDuration {
StatDuration(self)
}
}
impl std::fmt::Display for StatDuration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl StatFormatter for StatDuration {
fn format_stat(value: f64) -> String {
format!("{:?}", Duration::from_secs_f64(value))
}
}
/// Sliding window statistics collector.
///
/// `SlidingStats<T>` maintains a rolling buffer of values with timestamps,
/// automatically pruning values older than the configured `retention` period.
/// It provides percentile queries (e.g., p50, p95), min/max, average, and count.
pub struct SlidingStats<T> {
inner: Arc<RwLock<Inner<T>>>,
}
/// Sync version of `SlidingStats`
pub struct SyncSlidingStats<T> {
inner: Arc<parking_lot::RwLock<Inner<T>>>,
}
/// A type alias for `SlidingStats` specialized for durations with human-readable formatting.
///
/// Wraps `std::time::Duration` values using `StatDuration`, allowing for statistical summaries
/// (e.g. p50, p95, average) to be displayed in units like nanoseconds, milliseconds, or seconds.
pub type DurationSlidingStats = SlidingStats<StatDuration>;
/// Sync version of `DurationSlidingStats`
pub type SyncDurationSlidingStats = SyncSlidingStats<StatDuration>;
/// Internal state of the statistics buffer.
pub struct Inner<T> {
/// How long to retain items after insertion.
retention: Duration,
/// Counter to assign unique ids to each entry.
next_id: usize,
/// Maps id to actual value + timestamp.
entries: HashMap<usize, Entry<T>>,
/// Queue of IDs in insertion order for expiration.
by_time: VecDeque<usize>,
/// Set of values with ids, ordered by value.
by_value: BTreeSet<(T, usize)>,
/// The time stamp of most recent insertion with log.
///
/// Used to throttle debug messages.
last_log: Option<Instant>,
}
impl<T> Default for Inner<T> {
fn default() -> Self {
Self {
retention: Default::default(),
next_id: Default::default(),
entries: Default::default(),
by_time: Default::default(),
by_value: Default::default(),
last_log: None,
}
}
}
impl<T> Display for Inner<T>
where
T: Ord + Copy + Into<f64> + std::fmt::Display + StatFormatter,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut parts = Vec::new();
parts.push(format!("count={}", self.count()));
if let Some(min) = self.min() {
parts.push(format!("min={}", min));
}
if let Some(max) = self.max() {
parts.push(format!("max={}", max));
}
if let Some(avg) = self.avg() {
parts.push(format!("avg={}", <T as StatFormatter>::format_stat(avg)));
}
for p in [50, 90, 95, 99] {
let val = self.percentile(p);
if val.is_finite() {
parts.push(format!("p{}={}", p, <T as StatFormatter>::format_stat(val)));
}
}
parts.push(format!("span={:?}", self.retention));
write!(f, "{}", parts.join(", "))
}
}
/// A value inserted into the buffer, along with its insertion time.
#[derive(Clone, Copy)]
struct Entry<T> {
timestamp: Instant,
value: T,
}
impl<T> SlidingStats<T>
where
T: Ord + Copy,
{
/// Creates a new `SlidingStats` with the given retention duration.
pub fn new(retention: Duration) -> Self {
Self { inner: Arc::new(RwLock::new(Inner { retention, ..Default::default() })) }
}
/// Inserts a value into the buffer, timestamped with `Instant::now()`.
///
/// May trigger pruning of old items.
#[cfg(test)]
pub async fn insert(&self, value: T) {
self.inner.write().await.insert(value)
}
/// Inserts a value into the buffer with provided timestamp.
///
/// May trigger pruning of old items.
#[cfg(test)]
pub async fn insert_using_timestamp(&self, value: T, now: Instant) {
self.inner.write().await.insert_using_timestamp(value, now)
}
#[cfg(test)]
pub async fn len(&self) -> usize {
self.inner.read().await.len()
}
/// Grants temporary read-only access to the locked inner structure,
/// passing it into the provided closure.
///
/// Intended to dump stats and prune inner based on current timestamp.
#[cfg(test)]
pub async fn with_inner<R>(&self, f: impl FnOnce(&mut Inner<T>) -> R) -> R {
let mut guard = self.inner.write().await;
f(&mut *guard)
}
}
impl<T> SyncSlidingStats<T>
where
T: Ord + Copy,
{
/// Creates a new `SlidingStats` with the given retention duration.
pub fn new(retention: Duration) -> Self {
Self {
inner: Arc::new(parking_lot::RwLock::new(Inner { retention, ..Default::default() })),
}
}
}
impl<T> SlidingStats<T>
where
T: Ord + Copy + Into<f64> + std::fmt::Display + StatFormatter,
{
/// Inserts a value and optionally returns a formatted log string of the current stats.
///
/// If enough time has passed since the last log (determined by `log_interval` or retention),
/// this method returns `Some(log_string)`, otherwise it returns `None`.
///
/// This method performs:
/// - Automatic pruning of expired entries
/// - Throttling via `last_log` timestamp
///
/// Note: The newly inserted value may not be included in the returned summary.
pub async fn insert_with_log(
&self,
value: T,
log_interval: Option<Duration>,
now: Instant,
) -> Option<String> {
let mut inner = self.inner.write().await;
inner.insert_with_log(value, log_interval, now)
}
}
impl<T> SyncSlidingStats<T>
where
T: Ord + Copy + Into<f64> + std::fmt::Display + StatFormatter,
{
pub fn insert_with_log(
&self,
value: T,
log_interval: Option<Duration>,
now: Instant,
) -> Option<String> {
let mut inner = self.inner.write();
inner.insert_with_log(value, log_interval, now)
}
}
impl<T> Inner<T>
where
T: Ord + Copy,
{
#[cfg(test)]
fn insert(&mut self, value: T) {
self.insert_using_timestamp(value, Instant::now())
}
/// Refer to [`SlidingStats::insert_using_timestamp`]
fn insert_using_timestamp(&mut self, value: T, now: Instant) {
let id = self.next_id;
self.next_id += 1;
let entry = Entry { timestamp: now, value };
self.entries.insert(id, entry);
self.by_time.push_back(id);
self.by_value.insert((value, id));
self.prune(now);
}
/// Returns the minimum value in the current window.
pub fn min(&self) -> Option<T> {
self.by_value.first().map(|(v, _)| *v)
}
/// Returns the maximum value in the current window.
pub fn max(&self) -> Option<T> {
self.by_value.last().map(|(v, _)| *v)
}
/// Returns the number of items currently retained.
pub fn count(&self) -> usize {
self.len()
}
/// Explicitly prunes expired items from the buffer.
///
/// This is also called automatically during insertions.
pub fn prune(&mut self, now: Instant) {
let cutoff = now - self.retention;
while let Some(&oldest_id) = self.by_time.front() {
let expired = match self.entries.get(&oldest_id) {
Some(entry) => entry.timestamp < cutoff,
None => {
debug_assert!(false);
true
},
};
if !expired {
break;
}
if let Some(entry) = self.entries.remove(&oldest_id) {
self.by_value.remove(&(entry.value, oldest_id));
} else {
debug_assert!(false);
}
self.by_time.pop_front();
}
}
pub fn len(&self) -> usize {
debug_assert_eq!(self.entries.len(), self.by_time.len());
debug_assert_eq!(self.entries.len(), self.by_value.len());
self.entries.len()
}
}
impl<T> Inner<T>
where
T: Ord + Copy + Into<f64>,
{
/// Returns the average (mean) of values in the current window.
pub fn avg(&self) -> Option<f64> {
let len = self.len();
if len == 0 {
None
} else {
Some(self.entries.values().map(|e| e.value.into()).sum::<f64>() / len as f64)
}
}
/// Returns the value at the given percentile (e.g., 0.5 for p50).
///
/// Returns `None` if the buffer is empty.
// note: copied from: https://docs.rs/statrs/0.18.0/src/statrs/statistics/slice_statistics.rs.html#164-182
pub fn percentile(&self, percentile: usize) -> f64 {
if self.len() == 0 || percentile > 100 {
return f64::NAN;
}
let tau = percentile as f64 / 100.0;
let len = self.len();
let h = (len as f64 + 1.0 / 3.0) * tau + 1.0 / 3.0;
let hf = h as i64;
if hf <= 0 || percentile == 0 {
return self.min().map(|v| v.into()).unwrap_or(f64::NAN);
}
if hf >= len as i64 || percentile == 100 {
return self.max().map(|v| v.into()).unwrap_or(f64::NAN);
}
let mut iter = self.by_value.iter().map(|(v, _)| (*v).into());
let a = iter.nth((hf as usize).saturating_sub(1)).unwrap_or(f64::NAN);
let b = iter.next().unwrap_or(f64::NAN);
a + (h - hf as f64) * (b - a)
}
}
impl<T> Inner<T>
where
T: Ord + Copy + Into<f64> + std::fmt::Display + StatFormatter,
{
/// Refer to [`SlidingStats::insert_with_log`]
pub fn insert_with_log(
&mut self,
value: T,
log_interval: Option<Duration>,
now: Instant,
) -> Option<String> {
let Some(last_log) = self.last_log else {
self.last_log = Some(now);
self.insert_using_timestamp(value, now);
return None;
};
let log_interval = log_interval.unwrap_or(self.retention);
let should_log = now.duration_since(last_log) >= log_interval;
let result = should_log.then(|| {
self.last_log = Some(now);
format!("{self}")
});
self.insert_using_timestamp(value, now);
result
}
}
impl<T> Clone for SlidingStats<T> {
fn clone(&self) -> Self {
Self { inner: Arc::clone(&self.inner) }
}
}
impl<T> Clone for SyncSlidingStats<T> {
fn clone(&self) -> Self {
Self { inner: Arc::clone(&self.inner) }
}
}
/// Inserts a value into a `SlidingStats` and conditionally logs the current stats using `tracing`.
///
/// This macro inserts the given `$value` into the `$stats` collector only if tracing is enabled
/// for the given `$target` and `$level`. The log will be emiited only if enough time has passed
/// since the last logged output (as tracked by the internal last_log timestamp).
///
/// The macro respects throttling: stats will not be logged more frequently than either the
/// explicitly provided `log_interval` or the stats' retention period (if no interval is given).
///
/// Note that:
/// - Logging is skipped unless `tracing::enabled!` returns true for the target and level.
/// - All entries older than the retention period will be logged and pruned,
/// - The newly inserted value may not be included in the logged statistics output (it is inserted
/// *after* the log decision).
#[macro_export]
macro_rules! insert_and_log_throttled {
(
$level:expr,
target: $target:expr,
log_interval: $log_interval:expr,
prefix: $prefix:expr,
$stats:expr,
$value:expr
) => {{
if tracing::enabled!(target: $target, $level) {
let now = Instant::now();
if let Some(msg) = $stats.insert_with_log($value, Some($log_interval), now).await {
tracing::event!(target: $target, $level, "{}: {}", $prefix, msg);
}
}
}};
(
$level:expr,
target: $target:expr,
prefix: $prefix:expr,
$stats:expr,
$value:expr
) => {{
if tracing::enabled!(target: $target, $level) {
let now = std::time::Instant::now();
if let Some(msg) = $stats.insert_with_log($value, None, now).await {
tracing::event!(target: $target, $level, "{}: {}", $prefix, msg);
}
}
}};
}
/// Sync version of `insert_and_log_throttled`
#[macro_export]
macro_rules! insert_and_log_throttled_sync {
(
$level:expr,
target: $target:literal,
prefix: $prefix:expr,
$stats:expr,
$value:expr
) => {{
if tracing::enabled!(target: $target, $level) {
let now = std::time::Instant::now();
if let Some(msg) = $stats.insert_with_log($value, None, now){
tracing::event!(target: $target, $level, "{}: {}", $prefix, msg);
}
}
}};
}
#[cfg(test)]
mod test {
use super::*;
use std::time::{Duration, Instant};
#[tokio::test]
async fn retention_prunes_old_items() {
let stats = SlidingStats::<u64>::new(Duration::from_secs(10));
let base = Instant::now();
for i in 0..5 {
stats.insert_using_timestamp(i * 10, base + Duration::from_secs(i * 5)).await;
}
assert_eq!(stats.len().await, 3);
stats.insert_using_timestamp(999, base + Duration::from_secs(26)).await;
assert_eq!(stats.len().await, 2);
}
#[tokio::test]
async fn retention_prunes_old_items2() {
let stats = SlidingStats::<u64>::new(Duration::from_secs(10));
let base = Instant::now();
for i in 0..100 {
stats.insert_using_timestamp(i * 10, base + Duration::from_secs(5)).await;
}
assert_eq!(stats.len().await, 100);
stats.insert_using_timestamp(999, base + Duration::from_secs(16)).await;
let len = stats.len().await;
assert_eq!(len, 1);
}
#[tokio::test]
async fn insert_with_log_message_contains_all_old_items() {
let stats = SlidingStats::<u32>::new(Duration::from_secs(100));
let base = Instant::now();
for _ in 0..10 {
stats.insert_with_log(1, None, base + Duration::from_secs(5)).await;
}
assert_eq!(stats.len().await, 10);
let output = stats.insert_with_log(1, None, base + Duration::from_secs(200)).await.unwrap();
assert!(output.contains("count=10"));
let len = stats.len().await;
assert_eq!(len, 1);
}
#[tokio::test]
async fn insert_with_log_message_prunes_all_old_items() {
let stats = SlidingStats::<u32>::new(Duration::from_secs(25));
let base = Instant::now();
for i in 0..10 {
stats.insert_with_log(1, None, base + Duration::from_secs(i * 5)).await;
}
assert_eq!(stats.len().await, 6);
let output = stats.insert_with_log(1, None, base + Duration::from_secs(200)).await.unwrap();
assert!(output.contains("count=6"));
let len = stats.len().await;
assert_eq!(len, 1);
}
#[tokio::test]
async fn test_avg_min_max() {
let stats = SlidingStats::<u32>::new(Duration::from_secs(100));
let base = Instant::now();
stats.insert_using_timestamp(10, base).await;
stats.insert_using_timestamp(20, base + Duration::from_secs(1)).await;
stats.insert_using_timestamp(30, base + Duration::from_secs(2)).await;
stats
.with_inner(|inner| {
assert_eq!(inner.count(), 3);
assert_eq!(inner.avg(), Some(20.0));
assert_eq!(inner.min(), Some(10));
assert_eq!(inner.max(), Some(30));
})
.await;
}
#[tokio::test]
async fn duration_format() {
let stats = SlidingStats::<StatDuration>::new(Duration::from_secs(100));
stats.insert(Duration::from_nanos(100).into()).await;
let output = stats.with_inner(|i| format!("{i}")).await;
assert!(output.contains("max=100ns"));
let stats = SlidingStats::<StatDuration>::new(Duration::from_secs(100));
stats.insert(Duration::from_micros(100).into()).await;
let output = stats.with_inner(|i| format!("{i}")).await;
assert!(output.contains("max=100µs"));
let stats = SlidingStats::<StatDuration>::new(Duration::from_secs(100));
stats.insert(Duration::from_millis(100).into()).await;
let output = stats.with_inner(|i| format!("{i}")).await;
assert!(output.contains("max=100ms"));
let stats = SlidingStats::<StatDuration>::new(Duration::from_secs(100));
stats.insert(Duration::from_secs(100).into()).await;
let output = stats.with_inner(|i| format!("{i}")).await;
assert!(output.contains("max=100s"));
let stats = SlidingStats::<StatDuration>::new(Duration::from_secs(100));
stats.insert(Duration::from_nanos(100).into()).await;
stats.insert(Duration::from_micros(100).into()).await;
stats.insert(Duration::from_millis(100).into()).await;
stats.insert(Duration::from_secs(100).into()).await;
let output = stats.with_inner(|i| format!("{i}")).await;
println!("{output}");
assert_eq!(output, "count=4, min=100ns, max=100s, avg=25.025025025s, p50=50.05ms, p90=100s, p95=100s, p99=100s, span=100s");
}
}
@@ -0,0 +1,235 @@
// 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/>.
//! Testing related primitives for internal usage in this crate.
use crate::{
graph::{BlockHash, ChainApi, ExtrinsicFor, NumberFor, RawExtrinsicFor},
ValidateTransactionPriority,
};
use async_trait::async_trait;
use codec::Encode;
use parking_lot::Mutex;
use pezsc_transaction_pool_api::error;
use pezsp_blockchain::{HashAndNumber, TreeRoute};
use pezsp_runtime::{
generic::BlockId,
traits::{Block as BlockT, Hash},
transaction_validity::{
InvalidTransaction, TransactionSource, TransactionValidity, ValidTransaction,
},
};
use std::{collections::HashSet, sync::Arc};
use bizinikiwi_test_runtime::{
bizinikiwi_test_pallet::pallet::Call as PalletCall, BalancesCall, Block, BlockNumber, Extrinsic,
ExtrinsicBuilder, Hashing, RuntimeCall, Transfer, TransferData, H256,
};
type Pool<Api> = crate::graph::Pool<Api, ()>;
pub(crate) const INVALID_NONCE: u64 = 254;
/// Test api that implements [`ChainApi`].
#[derive(Clone, Debug, Default)]
pub(crate) struct TestApi {
pub delay: Arc<Mutex<Option<std::sync::mpsc::Receiver<()>>>>,
pub invalidate: Arc<Mutex<HashSet<H256>>>,
pub clear_requirements: Arc<Mutex<HashSet<H256>>>,
pub add_requirements: Arc<Mutex<HashSet<H256>>>,
pub validation_requests: Arc<Mutex<Vec<Extrinsic>>>,
}
impl TestApi {
/// Query validation requests received.
pub fn validation_requests(&self) -> Vec<Extrinsic> {
self.validation_requests.lock().clone()
}
/// Helper function for mapping block number to hash. Use if mapping shall not fail.
pub fn expect_hash_from_number(&self, n: BlockNumber) -> H256 {
self.block_id_to_hash(&BlockId::Number(n)).unwrap().unwrap()
}
pub fn expect_hash_and_number(&self, n: BlockNumber) -> HashAndNumber<Block> {
HashAndNumber { hash: self.expect_hash_from_number(n), number: n }
}
}
#[async_trait]
impl ChainApi for TestApi {
type Block = Block;
type Error = error::Error;
/// Verify extrinsic at given block.
async fn validate_transaction(
&self,
at: <Self::Block as BlockT>::Hash,
_source: TransactionSource,
uxt: ExtrinsicFor<Self>,
_: ValidateTransactionPriority,
) -> Result<TransactionValidity, Self::Error> {
let uxt = (*uxt).clone();
self.validation_requests.lock().push(uxt.clone());
let hash = self.hash_and_length(&uxt).0;
let block_number = self.block_id_to_number(&BlockId::Hash(at)).unwrap().unwrap();
let res = match uxt {
Extrinsic {
function: RuntimeCall::Balances(BalancesCall::transfer_allow_death { .. }),
..
} => {
let TransferData { nonce, .. } = (&uxt).try_into().unwrap();
// This is used to control the test flow.
if nonce > 0 {
let opt = self.delay.lock().take();
if let Some(delay) = opt {
if delay.recv().is_err() {
println!("Error waiting for delay!");
}
}
}
if self.invalidate.lock().contains(&hash) {
InvalidTransaction::Custom(0).into()
} else if nonce < block_number {
InvalidTransaction::Stale.into()
} else {
let mut transaction = ValidTransaction {
priority: 4,
requires: if nonce > block_number {
vec![vec![nonce as u8 - 1]]
} else {
vec![]
},
provides: if nonce == INVALID_NONCE {
vec![]
} else {
vec![vec![nonce as u8]]
},
longevity: 3,
propagate: true,
};
if self.clear_requirements.lock().contains(&hash) {
transaction.requires.clear();
}
if self.add_requirements.lock().contains(&hash) {
transaction.requires.push(vec![128]);
}
Ok(transaction)
}
},
Extrinsic {
function: RuntimeCall::BizinikiwiTest(PalletCall::include_data { .. }),
..
} => Ok(ValidTransaction {
priority: 9001,
requires: vec![],
provides: vec![vec![42]],
longevity: 9001,
propagate: false,
}),
Extrinsic {
function: RuntimeCall::BizinikiwiTest(PalletCall::indexed_call { .. }),
..
} => Ok(ValidTransaction {
priority: 9001,
requires: vec![],
provides: vec![vec![43]],
longevity: 9001,
propagate: false,
}),
_ => unimplemented!(),
};
Ok(res)
}
fn validate_transaction_blocking(
&self,
_at: <Self::Block as BlockT>::Hash,
_source: TransactionSource,
_uxt: Arc<<Self::Block as BlockT>::Extrinsic>,
) -> Result<TransactionValidity, Self::Error> {
unimplemented!();
}
/// Returns a block number given the block id.
fn block_id_to_number(
&self,
at: &BlockId<Self::Block>,
) -> Result<Option<NumberFor<Self>>, Self::Error> {
Ok(match at {
BlockId::Number(num) => Some(*num),
BlockId::Hash(hash) if *hash == H256::from_low_u64_be(hash.to_low_u64_be()) =>
Some(hash.to_low_u64_be()),
BlockId::Hash(_) => None,
})
}
/// Returns a block hash given the block id.
fn block_id_to_hash(
&self,
at: &BlockId<Self::Block>,
) -> Result<Option<<Self::Block as BlockT>::Hash>, Self::Error> {
Ok(match at {
BlockId::Number(num) => Some(H256::from_low_u64_be(*num)).into(),
BlockId::Hash(hash) => Some(*hash),
})
}
/// Hash the extrinsic.
fn hash_and_length(&self, uxt: &RawExtrinsicFor<Self>) -> (BlockHash<Self>, usize) {
let encoded = uxt.encode();
let len = encoded.len();
(Hashing::hash(&encoded), len)
}
async fn block_body(
&self,
_id: <Self::Block as BlockT>::Hash,
) -> Result<Option<Vec<<Self::Block as BlockT>::Extrinsic>>, Self::Error> {
Ok(None)
}
fn block_header(
&self,
_: <Self::Block as BlockT>::Hash,
) -> Result<Option<<Self::Block as BlockT>::Header>, Self::Error> {
Ok(None)
}
fn tree_route(
&self,
_from: <Self::Block as BlockT>::Hash,
_to: <Self::Block as BlockT>::Hash,
) -> Result<TreeRoute<Self::Block>, Self::Error> {
unimplemented!()
}
}
pub(crate) fn uxt(transfer: Transfer) -> Extrinsic {
ExtrinsicBuilder::new_transfer(transfer).build()
}
pub(crate) fn pool() -> (Pool<TestApi>, Arc<TestApi>) {
let api = Arc::new(TestApi::default());
(Pool::new_with_staticly_sized_rotator(Default::default(), true.into(), api.clone()), api)
}
@@ -0,0 +1,82 @@
// 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/>.
//! Utility for logging transaction collections with tracing crate.
/// Logs every transaction from given `tx_collection` with given level.
macro_rules! log_xt {
(data: hash, target: $target:expr, $level:expr, $tx_collection:expr, $text_with_format:expr) => {
for tx_hash in $tx_collection {
tracing::event!(
target: $target,
$level,
?tx_hash,
$text_with_format,
);
}
};
(data: hash, target: $target:expr, $level:expr, $tx_collection:expr, $text_with_format:expr, $($arg:expr),*) => {
for tx_hash in $tx_collection {
tracing::event!(
target: $target,
$level,
?tx_hash,
$text_with_format,
$($arg),*
);
}
};
(data: tuple, target: $target:expr, $level:expr, $tx_collection:expr, $text_with_format:expr) => {
for (tx_hash, arg) in $tx_collection {
tracing::event!(
target: $target,
$level,
?tx_hash,
$text_with_format,
arg
);
}
};
}
macro_rules! log_xt_debug {
(data: $datatype:ident, target: $target:expr, $($arg:tt)+) => {
$crate::common::tracing_log_xt::log_xt!(data: $datatype, target: $target, tracing::Level::DEBUG, $($arg)+);
};
(target: $target:expr, $tx_collection:expr, $text_with_format:expr) => {
$crate::common::tracing_log_xt::log_xt!(data: hash, target: $target, tracing::Level::DEBUG, $tx_collection, $text_with_format);
};
(target: $target:expr, $tx_collection:expr, $text_with_format:expr, $($arg:expr)*) => {
$crate::common::tracing_log_xt::log_xt!(data: hash, target: $target, tracing::Level::DEBUG, $tx_collection, $text_with_format, $($arg)*);
};
}
macro_rules! log_xt_trace {
(data: $datatype:ident, target: $target:expr, $($arg:tt)+) => {
$crate::common::tracing_log_xt::log_xt!(data: $datatype, target: $target, tracing::Level::TRACE, $($arg)+);
};
(target: $target:expr, $tx_collection:expr, $text_with_format:expr) => {
$crate::common::tracing_log_xt::log_xt!(data: hash, target: $target, tracing::Level::TRACE, $tx_collection, $text_with_format);
};
(target: $target:expr, $tx_collection:expr, $text_with_format:expr, $($arg:expr)*) => {
$crate::common::tracing_log_xt::log_xt!(data: hash, target: $target, tracing::Level::TRACE, $tx_collection, $text_with_format, $($arg)*);
};
}
pub(crate) use log_xt;
pub(crate) use log_xt_debug;
pub(crate) use log_xt_trace;