feat: Rebrand Polkadot/Substrate references to PezkuwiChain
This commit systematically rebrands various references from Parity Technologies' Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk. Key changes include: - Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks. - Modified internal documentation and code comments to reflect PezkuwiChain naming and structure. - Replaced direct references to with or specific paths within the for XCM, Pezkuwi, and other modules. - Cleaned up deprecated issue and PR references in various and files, particularly in and modules. - Adjusted image and logo URLs in documentation to point to PezkuwiChain assets. - Removed or rephrased comments related to external Polkadot/Substrate PRs and issues. This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
@@ -0,0 +1,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;
|
||||
Reference in New Issue
Block a user