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,251 @@
// 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 building bizinikiwi transaction pool trait object.
use crate::{
common::api::FullChainApi,
fork_aware_txpool::ForkAwareTxPool as ForkAwareFullPool,
graph::{base_pool::Transaction, ChainApi, ExtrinsicFor, ExtrinsicHash, IsValidator, Options},
single_state_txpool::BasicPool as SingleStateFullPool,
TransactionPoolWrapper, LOG_TARGET,
};
use prometheus_endpoint::Registry as PrometheusRegistry;
use pezsc_transaction_pool_api::{LocalTransactionPool, MaintainedTransactionPool};
use pezsp_core::traits::SpawnEssentialNamed;
use pezsp_runtime::traits::Block as BlockT;
use std::{marker::PhantomData, sync::Arc, time::Duration};
/// The type of transaction pool.
#[derive(Debug, Clone)]
pub enum TransactionPoolType {
/// Single-state transaction pool
SingleState,
/// Fork-aware transaction pool
ForkAware,
}
/// Transaction pool options.
#[derive(Debug, Clone)]
pub struct TransactionPoolOptions {
txpool_type: TransactionPoolType,
options: Options,
}
impl Default for TransactionPoolOptions {
fn default() -> Self {
Self { txpool_type: TransactionPoolType::SingleState, options: Default::default() }
}
}
impl TransactionPoolOptions {
/// Creates the options for the transaction pool using given parameters.
pub fn new_with_params(
pool_limit: usize,
pool_bytes: usize,
tx_ban_seconds: Option<u64>,
txpool_type: TransactionPoolType,
is_dev: bool,
) -> TransactionPoolOptions {
let mut options = Options::default();
// ready queue
options.ready.count = pool_limit;
options.ready.total_bytes = pool_bytes;
// future queue
let factor = 10;
options.future.count = pool_limit / factor;
options.future.total_bytes = pool_bytes / factor;
options.ban_time = if let Some(ban_seconds) = tx_ban_seconds {
Duration::from_secs(ban_seconds)
} else if is_dev {
Duration::from_secs(0)
} else {
Duration::from_secs(30 * 60)
};
TransactionPoolOptions { options, txpool_type }
}
/// Creates predefined options for benchmarking
pub fn new_for_benchmarks() -> TransactionPoolOptions {
TransactionPoolOptions {
options: Options {
ready: crate::graph::base_pool::Limit {
count: 100_000,
total_bytes: 100 * 1024 * 1024,
},
future: crate::graph::base_pool::Limit {
count: 100_000,
total_bytes: 100 * 1024 * 1024,
},
reject_future_transactions: false,
ban_time: Duration::from_secs(30 * 60),
},
txpool_type: TransactionPoolType::SingleState,
}
}
}
/// `FullClientTransactionPool` is a trait that combines the functionality of
/// `MaintainedTransactionPool` and `LocalTransactionPool` for a given `Client` and `Block`.
///
/// This trait defines the requirements for a full client transaction pool, ensuring
/// that it can handle transactions submission and maintenance.
pub trait FullClientTransactionPool<Block, Client>:
MaintainedTransactionPool<
Block = Block,
Hash = ExtrinsicHash<FullChainApi<Client, Block>>,
InPoolTransaction = Transaction<
ExtrinsicHash<FullChainApi<Client, Block>>,
ExtrinsicFor<FullChainApi<Client, Block>>,
>,
Error = <FullChainApi<Client, Block> as ChainApi>::Error,
> + LocalTransactionPool<
Block = Block,
Hash = ExtrinsicHash<FullChainApi<Client, Block>>,
Error = <FullChainApi<Client, Block> as ChainApi>::Error,
>
where
Block: BlockT,
Client: pezsp_api::ProvideRuntimeApi<Block>
+ pezsc_client_api::BlockBackend<Block>
+ pezsc_client_api::blockchain::HeaderBackend<Block>
+ pezsp_runtime::traits::BlockIdTo<Block>
+ pezsp_blockchain::HeaderMetadata<Block, Error = pezsp_blockchain::Error>
+ 'static,
Client::Api: pezsp_transaction_pool::runtime_api::TaggedTransactionQueue<Block>,
{
}
impl<Block, Client, P> FullClientTransactionPool<Block, Client> for P
where
Block: BlockT,
Client: pezsp_api::ProvideRuntimeApi<Block>
+ pezsc_client_api::BlockBackend<Block>
+ pezsc_client_api::blockchain::HeaderBackend<Block>
+ pezsp_runtime::traits::BlockIdTo<Block>
+ pezsp_blockchain::HeaderMetadata<Block, Error = pezsp_blockchain::Error>
+ 'static,
Client::Api: pezsp_transaction_pool::runtime_api::TaggedTransactionQueue<Block>,
P: MaintainedTransactionPool<
Block = Block,
Hash = ExtrinsicHash<FullChainApi<Client, Block>>,
InPoolTransaction = Transaction<
ExtrinsicHash<FullChainApi<Client, Block>>,
ExtrinsicFor<FullChainApi<Client, Block>>,
>,
Error = <FullChainApi<Client, Block> as ChainApi>::Error,
> + LocalTransactionPool<
Block = Block,
Hash = ExtrinsicHash<FullChainApi<Client, Block>>,
Error = <FullChainApi<Client, Block> as ChainApi>::Error,
>,
{
}
/// The public type alias for the actual type providing the implementation of
/// `FullClientTransactionPool` with the given `Client` and `Block` types.
///
/// This handle abstracts away the specific type of the transaction pool. Should be used
/// externally to keep reference to transaction pool.
pub type TransactionPoolHandle<Block, Client> = TransactionPoolWrapper<Block, Client>;
/// Builder allowing to create specific instance of transaction pool.
pub struct Builder<'a, Block, Client> {
options: TransactionPoolOptions,
is_validator: IsValidator,
prometheus: Option<&'a PrometheusRegistry>,
client: Arc<Client>,
spawner: Box<dyn SpawnEssentialNamed>,
_phantom: PhantomData<(Client, Block)>,
}
impl<'a, Client, Block> Builder<'a, Block, Client>
where
Block: BlockT,
Client: pezsp_api::ProvideRuntimeApi<Block>
+ pezsc_client_api::BlockBackend<Block>
+ pezsc_client_api::blockchain::HeaderBackend<Block>
+ pezsp_runtime::traits::BlockIdTo<Block>
+ pezsc_client_api::ExecutorProvider<Block>
+ pezsc_client_api::UsageProvider<Block>
+ pezsp_blockchain::HeaderMetadata<Block, Error = pezsp_blockchain::Error>
+ Send
+ Sync
+ 'static,
<Block as BlockT>::Hash: std::marker::Unpin,
Client::Api: pezsp_transaction_pool::runtime_api::TaggedTransactionQueue<Block>,
{
/// Creates new instance of `Builder`
pub fn new(
spawner: impl SpawnEssentialNamed + 'static,
client: Arc<Client>,
is_validator: IsValidator,
) -> Builder<'a, Block, Client> {
Builder {
options: Default::default(),
_phantom: Default::default(),
spawner: Box::new(spawner),
client,
is_validator,
prometheus: None,
}
}
/// Sets the options used for creating a transaction pool instance.
pub fn with_options(mut self, options: TransactionPoolOptions) -> Self {
self.options = options;
self
}
/// Sets the prometheus endpoint used in a transaction pool instance.
pub fn with_prometheus(mut self, prometheus: Option<&'a PrometheusRegistry>) -> Self {
self.prometheus = prometheus;
self
}
/// Creates an instance of transaction pool.
pub fn build(self) -> TransactionPoolHandle<Block, Client> {
tracing::info!(
target: LOG_TARGET,
txpool_type = ?self.options.txpool_type,
ready = ?self.options.options.ready,
future = ?self.options.options.future,
"Creating transaction pool"
);
TransactionPoolWrapper::<Block, Client>(match self.options.txpool_type {
TransactionPoolType::SingleState => Box::new(SingleStateFullPool::new_full(
self.options.options,
self.is_validator,
self.prometheus,
self.spawner,
self.client,
)),
TransactionPoolType::ForkAware => Box::new(ForkAwareFullPool::new_full(
self.options.options,
self.is_validator,
self.prometheus,
self.spawner,
self.client,
)),
})
}
}
@@ -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;
@@ -0,0 +1,591 @@
// 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/>.
//! Multi-view pool dropped events listener provides means to combine streams from multiple pool
//! views into a single event stream. It allows management of dropped transaction events, adding new
//! views, and removing views as needed, ensuring that transactions which are no longer referenced
//! by any view are detected and properly notified.
use crate::{
common::tracing_log_xt::log_xt_trace,
fork_aware_txpool::stream_map_util::next_event,
graph::{self, BlockHash, ExtrinsicHash},
LOG_TARGET,
};
use futures::stream::StreamExt;
use pezsc_transaction_pool_api::TransactionStatus;
use pezsc_utils::mpsc;
use pezsp_runtime::traits::Block as BlockT;
use std::{
collections::{
hash_map::{Entry, OccupiedEntry},
HashMap, HashSet,
},
fmt::{self, Debug, Formatter},
pin::Pin,
};
use tokio_stream::StreamMap;
use tracing::{debug, trace};
/// Represents a transaction that was removed from the transaction pool, including the reason of its
/// removal.
#[derive(Debug, PartialEq)]
pub struct DroppedTransaction<Hash> {
/// Hash of the dropped extrinsic.
pub tx_hash: Hash,
/// Reason of the transaction being dropped.
pub reason: DroppedReason<Hash>,
}
impl<Hash> DroppedTransaction<Hash> {
/// Creates a new instance with reason set to `DroppedReason::Usurped(by)`.
pub fn new_usurped(tx_hash: Hash, by: Hash) -> Self {
Self { reason: DroppedReason::Usurped(by), tx_hash }
}
/// Creates a new instance with reason set to `DroppedReason::LimitsEnforced`.
pub fn new_enforced_by_limts(tx_hash: Hash) -> Self {
Self { reason: DroppedReason::LimitsEnforced, tx_hash }
}
/// Creates a new instance with reason set to `DroppedReason::Invalid`.
pub fn new_invalid(tx_hash: Hash) -> Self {
Self { reason: DroppedReason::Invalid, tx_hash }
}
}
/// Provides reason of why transactions was dropped.
#[derive(Debug, PartialEq)]
pub enum DroppedReason<Hash> {
/// Transaction was replaced by other transaction (e.g. because of higher priority).
Usurped(Hash),
/// Transaction was dropped because of internal pool limits being enforced.
LimitsEnforced,
/// Transaction was dropped because of being invalid.
Invalid,
}
/// Dropped-logic related event from the single view.
pub type ViewStreamEvent<C> =
crate::fork_aware_txpool::view::TransactionStatusEvent<ExtrinsicHash<C>, BlockHash<C>>;
/// Dropped-logic stream of events coming from the single view.
type ViewStream<C> = Pin<Box<dyn futures::Stream<Item = ViewStreamEvent<C>> + Send>>;
/// Stream of extrinsic hashes that were dropped by the views and have no references by existing
/// views.
pub(crate) type StreamOfDropped<C> =
Pin<Box<dyn futures::Stream<Item = DroppedTransaction<ExtrinsicHash<C>>> + Send>>;
/// A type alias for a sender used as the controller of the [`MultiViewDropWatcherContext`].
/// Used to send control commands from the [`MultiViewDroppedWatcherController`] to
/// [`MultiViewDropWatcherContext`].
type Controller<T> = mpsc::TracingUnboundedSender<T>;
/// A type alias for a receiver used as the commands receiver in the
/// [`MultiViewDropWatcherContext`].
type CommandReceiver<T> = mpsc::TracingUnboundedReceiver<T>;
/// Commands to control the instance of dropped transactions stream [`StreamOfDropped`].
enum Command<ChainApi>
where
ChainApi: graph::ChainApi,
{
/// Adds a new stream of dropped-related events originating in a view with a specific block
/// hash
AddView(BlockHash<ChainApi>, ViewStream<ChainApi>),
/// Removes an existing view's stream associated with a specific block hash.
RemoveView(BlockHash<ChainApi>),
/// Removes referencing views for given extrinsic hashes.
///
/// Intended to ba called when transactions were finalized or their finality timed out.
RemoveTransactions(Vec<ExtrinsicHash<ChainApi>>),
}
impl<ChainApi> Debug for Command<ChainApi>
where
ChainApi: graph::ChainApi,
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Command::AddView(..) => write!(f, "AddView"),
Command::RemoveView(..) => write!(f, "RemoveView"),
Command::RemoveTransactions(..) => write!(f, "RemoveTransactions"),
}
}
}
/// Manages the state and logic for handling events related to dropped transactions across multiple
/// views.
///
/// This struct maintains a mapping of active views and their corresponding streams, as well as the
/// state of each transaction with respect to these views.
struct MultiViewDropWatcherContext<ChainApi>
where
ChainApi: graph::ChainApi,
{
/// A map that associates the views identified by corresponding block hashes with their streams
/// of dropped-related events. This map is used to keep track of active views and their event
/// streams.
stream_map: StreamMap<BlockHash<ChainApi>, ViewStream<ChainApi>>,
/// A receiver for commands to control the state of the stream, allowing the addition and
/// removal of views. This is used to dynamically update which views are being tracked.
command_receiver: CommandReceiver<Command<ChainApi>>,
/// For each transaction hash we keep the set of hashes representing the views that see this
/// transaction as ready or in_block.
///
/// Even if all views referencing a ready transactions are removed, we still want to keep
/// transaction, there can be a fork which sees the transaction as ready.
///
/// Once transaction is dropped, dropping view is removed from the set.
ready_transaction_views: HashMap<ExtrinsicHash<ChainApi>, HashSet<BlockHash<ChainApi>>>,
/// For each transaction hash we keep the set of hashes representing the views that see this
/// transaction as future.
///
/// Once all views referencing a future transactions are removed, the future can be dropped.
///
/// Once transaction is dropped, dropping view is removed from the set.
future_transaction_views: HashMap<ExtrinsicHash<ChainApi>, HashSet<BlockHash<ChainApi>>>,
/// Transactions that need to be notified as dropped.
pending_dropped_transactions: Vec<ExtrinsicHash<ChainApi>>,
}
impl<C> MultiViewDropWatcherContext<C>
where
C: graph::ChainApi + 'static,
<<C as graph::ChainApi>::Block as BlockT>::Hash: Unpin,
{
/// Provides the ready or future `HashSet` containing views referencing given transaction.
fn transaction_views(
&mut self,
tx_hash: ExtrinsicHash<C>,
) -> Option<OccupiedEntry<'_, ExtrinsicHash<C>, HashSet<BlockHash<C>>>> {
if let Entry::Occupied(views_keeping_tx_valid) = self.ready_transaction_views.entry(tx_hash)
{
return Some(views_keeping_tx_valid);
}
if let Entry::Occupied(views_keeping_tx_valid) =
self.future_transaction_views.entry(tx_hash)
{
return Some(views_keeping_tx_valid);
}
None
}
/// Processes the command and updates internal state accordingly.
fn handle_command(&mut self, cmd: Command<C>) {
match cmd {
Command::AddView(key, stream) => {
trace!(
target: LOG_TARGET,
"dropped_watcher: Command::AddView {key:?} views:{:?}",
self.stream_map.keys().collect::<Vec<_>>()
);
self.stream_map.insert(key, stream);
},
Command::RemoveView(key) => {
trace!(
target: LOG_TARGET,
"dropped_watcher: Command::RemoveView {key:?} views:{:?}",
self.stream_map.keys().collect::<Vec<_>>()
);
self.stream_map.remove(&key);
self.ready_transaction_views.iter_mut().for_each(|(tx_hash, views)| {
trace!(
target: LOG_TARGET,
"[{:?}] dropped_watcher: Command::RemoveView ready views: {:?}",
tx_hash,
views
);
views.remove(&key);
});
self.future_transaction_views.iter_mut().for_each(|(tx_hash, views)| {
trace!(
target: LOG_TARGET,
"[{:?}] dropped_watcher: Command::RemoveView future views: {:?}",
tx_hash,
views
);
views.remove(&key);
if views.is_empty() {
self.pending_dropped_transactions.push(*tx_hash);
}
});
},
Command::RemoveTransactions(xts) => {
log_xt_trace!(
target: LOG_TARGET,
xts.clone(),
"dropped_watcher: finalized xt removed"
);
xts.iter().for_each(|xt| {
self.ready_transaction_views.remove(xt);
self.future_transaction_views.remove(xt);
});
},
}
}
/// Processes a `ViewStreamEvent` from a specific view and updates the internal state
/// accordingly.
///
/// If the event indicates that a transaction has been dropped and is no longer referenced by
/// any active views, the transaction hash is returned. Otherwise `None` is returned.
fn handle_event(
&mut self,
block_hash: BlockHash<C>,
event: ViewStreamEvent<C>,
) -> Option<DroppedTransaction<ExtrinsicHash<C>>> {
trace!(
target: LOG_TARGET,
"dropped_watcher: handle_event: event:{event:?} from:{block_hash:?} future_views:{:?} ready_views:{:?} stream_map views:{:?}, ",
self.future_transaction_views.get(&event.0),
self.ready_transaction_views.get(&event.0),
self.stream_map.keys().collect::<Vec<_>>(),
);
let (tx_hash, status) = event;
match status {
TransactionStatus::Future => {
// see note below:
if let Some(mut views_keeping_tx_valid) = self.transaction_views(tx_hash) {
views_keeping_tx_valid.get_mut().insert(block_hash);
} else {
self.future_transaction_views.entry(tx_hash).or_default().insert(block_hash);
}
},
TransactionStatus::Ready | TransactionStatus::InBlock(..) => {
// note: if future transaction was once seen as the ready we may want to treat it
// as ready transaction. The rationale behind this is as follows: we want to remove
// unreferenced future transactions when the last referencing view is removed (to
// avoid clogging mempool). For ready transactions we prefer to keep them in mempool
// even if no view is currently referencing them. Future transcaction once seen as
// ready is likely quite close to be included in some future fork (it is close to be
// ready, so we make exception and treat such transaction as ready).
if let Some(mut views) = self.future_transaction_views.remove(&tx_hash) {
views.insert(block_hash);
self.ready_transaction_views.insert(tx_hash, views);
} else {
self.ready_transaction_views.entry(tx_hash).or_default().insert(block_hash);
}
},
TransactionStatus::Dropped => {
if let Some(mut views_keeping_tx_valid) = self.transaction_views(tx_hash) {
views_keeping_tx_valid.get_mut().remove(&block_hash);
if views_keeping_tx_valid.get().is_empty() {
return Some(DroppedTransaction::new_enforced_by_limts(tx_hash));
}
} else {
debug!(target: LOG_TARGET, ?tx_hash, "dropped_watcher: removing (non-tracked dropped) tx");
return Some(DroppedTransaction::new_enforced_by_limts(tx_hash));
}
},
TransactionStatus::Usurped(by) =>
return Some(DroppedTransaction::new_usurped(tx_hash, by)),
TransactionStatus::Invalid => {
if let Some(mut views_keeping_tx_valid) = self.transaction_views(tx_hash) {
views_keeping_tx_valid.get_mut().remove(&block_hash);
if views_keeping_tx_valid.get().is_empty() {
return Some(DroppedTransaction::new_invalid(tx_hash));
}
} else {
debug!(target: LOG_TARGET, ?tx_hash, "dropped_watcher: removing (non-tracked invalid) tx");
return Some(DroppedTransaction::new_invalid(tx_hash));
}
},
_ => {},
};
None
}
/// Gets pending dropped transactions if any.
fn get_pending_dropped_transaction(&mut self) -> Option<DroppedTransaction<ExtrinsicHash<C>>> {
while let Some(tx_hash) = self.pending_dropped_transactions.pop() {
// never drop transaction that was seen as ready. It may not have a referencing
// view now, but such fork can appear.
if self.ready_transaction_views.get(&tx_hash).is_some() {
continue;
}
if let Some(views) = self.future_transaction_views.get(&tx_hash) {
if views.is_empty() {
self.future_transaction_views.remove(&tx_hash);
return Some(DroppedTransaction::new_enforced_by_limts(tx_hash));
}
}
}
None
}
/// Creates a new `StreamOfDropped` and its associated event stream controller.
///
/// This method initializes the internal structures and unfolds the stream of dropped
/// transactions. Returns a tuple containing this stream and the controller for managing
/// this stream.
fn event_stream() -> (StreamOfDropped<C>, Controller<Command<C>>) {
//note: 64 allows to avoid warning messages during execution of unit tests.
const CHANNEL_SIZE: usize = 64;
let (sender, command_receiver) = pezsc_utils::mpsc::tracing_unbounded::<Command<C>>(
"tx-pool-dropped-watcher-cmd-stream",
CHANNEL_SIZE,
);
let ctx = Self {
stream_map: StreamMap::new(),
command_receiver,
ready_transaction_views: Default::default(),
future_transaction_views: Default::default(),
pending_dropped_transactions: Default::default(),
};
let stream_map = futures::stream::unfold(ctx, |mut ctx| async move {
loop {
if let Some(dropped) = ctx.get_pending_dropped_transaction() {
trace!("dropped_watcher: sending out (pending): {dropped:?}");
return Some((dropped, ctx));
}
tokio::select! {
biased;
Some(event) = next_event(&mut ctx.stream_map) => {
if let Some(dropped) = ctx.handle_event(event.0, event.1) {
trace!("dropped_watcher: sending out: {dropped:?}");
return Some((dropped, ctx));
}
},
cmd = ctx.command_receiver.next() => {
ctx.handle_command(cmd?);
}
}
}
})
.boxed();
(stream_map, sender)
}
}
/// The controller for manipulating the state of the [`StreamOfDropped`].
///
/// This struct provides methods to add and remove streams associated with views to and from the
/// stream.
pub struct MultiViewDroppedWatcherController<ChainApi: graph::ChainApi> {
/// A controller allowing to update the state of the associated [`StreamOfDropped`].
controller: Controller<Command<ChainApi>>,
}
impl<ChainApi: graph::ChainApi> Clone for MultiViewDroppedWatcherController<ChainApi> {
fn clone(&self) -> Self {
Self { controller: self.controller.clone() }
}
}
impl<ChainApi> MultiViewDroppedWatcherController<ChainApi>
where
ChainApi: graph::ChainApi + 'static,
<<ChainApi as graph::ChainApi>::Block as BlockT>::Hash: Unpin,
{
/// Creates new [`StreamOfDropped`] and its controller.
pub fn new() -> (MultiViewDroppedWatcherController<ChainApi>, StreamOfDropped<ChainApi>) {
let (stream_map, ctrl) = MultiViewDropWatcherContext::<ChainApi>::event_stream();
(Self { controller: ctrl }, stream_map.boxed())
}
/// Notifies the [`StreamOfDropped`] that new view was created.
pub fn add_view(&self, key: BlockHash<ChainApi>, view: ViewStream<ChainApi>) {
let _ = self.controller.unbounded_send(Command::AddView(key, view)).map_err(|e| {
trace!(target: LOG_TARGET, "dropped_watcher: add_view {key:?} send message failed: {e}");
});
}
/// Notifies the [`StreamOfDropped`] that the view was destroyed and shall be removed the
/// stream map.
pub fn remove_view(&self, key: BlockHash<ChainApi>) {
let _ = self.controller.unbounded_send(Command::RemoveView(key)).map_err(|e| {
trace!(target: LOG_TARGET, "dropped_watcher: remove_view {key:?} send message failed: {e}");
});
}
/// Removes status info for transactions.
pub fn remove_transactions(
&self,
xts: impl IntoIterator<Item = ExtrinsicHash<ChainApi>> + Clone,
) {
let _ = self
.controller
.unbounded_send(Command::RemoveTransactions(xts.into_iter().collect()))
.map_err(|e| {
trace!(target: LOG_TARGET, "dropped_watcher: remove_transactions send message failed: {e}");
});
}
}
#[cfg(test)]
mod dropped_watcher_tests {
use super::*;
use crate::common::tests::TestApi;
use futures::{stream::pending, FutureExt, StreamExt};
use pezsp_core::H256;
type MultiViewDroppedWatcher = super::MultiViewDroppedWatcherController<TestApi>;
#[tokio::test]
async fn test01() {
pezsp_tracing::try_init_simple();
let (watcher, output_stream) = MultiViewDroppedWatcher::new();
let block_hash = H256::repeat_byte(0x01);
let tx_hash = H256::repeat_byte(0x0a);
let view_stream = futures::stream::iter(vec![
(tx_hash, TransactionStatus::Ready),
(tx_hash, TransactionStatus::Dropped),
])
.boxed();
watcher.add_view(block_hash, view_stream);
let handle = tokio::spawn(async move { output_stream.take(1).collect::<Vec<_>>().await });
assert_eq!(handle.await.unwrap(), vec![DroppedTransaction::new_enforced_by_limts(tx_hash)]);
}
#[tokio::test]
async fn test02() {
pezsp_tracing::try_init_simple();
let (watcher, mut output_stream) = MultiViewDroppedWatcher::new();
let block_hash0 = H256::repeat_byte(0x01);
let block_hash1 = H256::repeat_byte(0x02);
let tx_hash = H256::repeat_byte(0x0a);
let view_stream0 = futures::stream::iter(vec![(tx_hash, TransactionStatus::Future)])
.chain(pending())
.boxed();
let view_stream1 = futures::stream::iter(vec![
(tx_hash, TransactionStatus::Ready),
(tx_hash, TransactionStatus::Dropped),
])
.boxed();
watcher.add_view(block_hash0, view_stream0);
assert!(output_stream.next().now_or_never().is_none());
watcher.add_view(block_hash1, view_stream1);
assert!(output_stream.next().now_or_never().is_none());
}
#[tokio::test]
async fn test03() {
pezsp_tracing::try_init_simple();
let (watcher, output_stream) = MultiViewDroppedWatcher::new();
let block_hash0 = H256::repeat_byte(0x01);
let block_hash1 = H256::repeat_byte(0x02);
let tx_hash0 = H256::repeat_byte(0x0a);
let tx_hash1 = H256::repeat_byte(0x0b);
let view_stream0 = futures::stream::iter(vec![(tx_hash0, TransactionStatus::Future)])
.chain(pending())
.boxed();
let view_stream1 = futures::stream::iter(vec![
(tx_hash1, TransactionStatus::Ready),
(tx_hash1, TransactionStatus::Dropped),
])
.boxed();
watcher.add_view(block_hash0, view_stream0);
watcher.add_view(block_hash1, view_stream1);
let handle = tokio::spawn(async move { output_stream.take(1).collect::<Vec<_>>().await });
assert_eq!(
handle.await.unwrap(),
vec![DroppedTransaction::new_enforced_by_limts(tx_hash1)]
);
}
#[tokio::test]
async fn test04() {
pezsp_tracing::try_init_simple();
let (watcher, mut output_stream) = MultiViewDroppedWatcher::new();
let block_hash0 = H256::repeat_byte(0x01);
let block_hash1 = H256::repeat_byte(0x02);
let tx_hash = H256::repeat_byte(0x0b);
let view_stream0 = futures::stream::iter(vec![
(tx_hash, TransactionStatus::Future),
(tx_hash, TransactionStatus::InBlock((block_hash1, 0))),
])
.boxed();
let view_stream1 = futures::stream::iter(vec![
(tx_hash, TransactionStatus::Ready),
(tx_hash, TransactionStatus::Dropped),
])
.boxed();
watcher.add_view(block_hash0, view_stream0);
assert!(output_stream.next().now_or_never().is_none());
watcher.remove_view(block_hash0);
watcher.add_view(block_hash1, view_stream1);
let handle = tokio::spawn(async move { output_stream.take(1).collect::<Vec<_>>().await });
assert_eq!(handle.await.unwrap(), vec![DroppedTransaction::new_enforced_by_limts(tx_hash)]);
}
#[tokio::test]
async fn test05() {
pezsp_tracing::try_init_simple();
let (watcher, mut output_stream) = MultiViewDroppedWatcher::new();
assert!(output_stream.next().now_or_never().is_none());
let block_hash0 = H256::repeat_byte(0x01);
let block_hash1 = H256::repeat_byte(0x02);
let tx_hash = H256::repeat_byte(0x0b);
let view_stream0 = futures::stream::iter(vec![
(tx_hash, TransactionStatus::Future),
(tx_hash, TransactionStatus::InBlock((block_hash1, 0))),
])
.boxed();
watcher.add_view(block_hash0, view_stream0);
assert!(output_stream.next().now_or_never().is_none());
let view_stream1 = futures::stream::iter(vec![
(tx_hash, TransactionStatus::Ready),
(tx_hash, TransactionStatus::InBlock((block_hash0, 0))),
])
.boxed();
watcher.add_view(block_hash1, view_stream1);
assert!(output_stream.next().now_or_never().is_none());
assert!(output_stream.next().now_or_never().is_none());
assert!(output_stream.next().now_or_never().is_none());
assert!(output_stream.next().now_or_never().is_none());
assert!(output_stream.next().now_or_never().is_none());
let tx_hash = H256::repeat_byte(0x0c);
let view_stream2 = futures::stream::iter(vec![
(tx_hash, TransactionStatus::Future),
(tx_hash, TransactionStatus::Dropped),
])
.boxed();
let block_hash2 = H256::repeat_byte(0x03);
watcher.add_view(block_hash2, view_stream2);
let handle = tokio::spawn(async move { output_stream.take(1).collect::<Vec<_>>().await });
assert_eq!(handle.await.unwrap(), vec![DroppedTransaction::new_enforced_by_limts(tx_hash)]);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,421 @@
// 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/>.
//! Multi view import notification sink. This module provides a unified stream of transactions that
//! have been notified as ready by any of the active views maintained by the transaction pool. It
//! combines streams (`import_notification_stream`) from multiple views into a single stream. Events
//! coming from this stream are dynamically dispatched to many external watchers.
use crate::{fork_aware_txpool::stream_map_util::next_event, LOG_TARGET};
use futures::{
channel::mpsc::{channel, Receiver as EventStream, Sender as ExternalSink},
stream::StreamExt,
Future, FutureExt,
};
use parking_lot::RwLock;
use pezsc_utils::mpsc;
use std::{
collections::HashSet,
fmt::{self, Debug, Formatter},
hash::Hash,
pin::Pin,
sync::Arc,
};
use tokio_stream::StreamMap;
use tracing::trace;
/// A type alias for a pinned, boxed stream of items of type `I`.
/// This alias is particularly useful for defining the types of the incoming streams from various
/// views, and is intended to build the stream of transaction hashes that become ready.
///
/// Note: generic parameter allows better testing of all types involved.
type StreamOf<I> = Pin<Box<dyn futures::Stream<Item = I> + Send>>;
/// A type alias for a tracing unbounded sender used as the command channel controller.
/// Used to send control commands to the [`AggregatedStreamContext`].
type Controller<T> = mpsc::TracingUnboundedSender<T>;
/// A type alias for a tracing unbounded receiver used as the command channel receiver.
/// Used to receive control commands in the [`AggregatedStreamContext`].
type CommandReceiver<T> = mpsc::TracingUnboundedReceiver<T>;
/// An enum representing commands that can be sent to the multi-sinks context.
///
/// This enum contains variants that encapsulate control commands used to manage multiple streams
/// within the `AggregatedStreamContext`.
enum Command<K, I: Send + Sync> {
/// Adds a new view with a unique key and a stream of items of type `I`.
AddView(K, StreamOf<I>),
}
impl<K, I: Send + Sync> Debug for Command<K, I> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Command::AddView(..) => write!(f, "AddView"),
}
}
}
/// A context used to unfold the single stream of items aggregated from the multiple
/// streams.
///
/// The `AggregatedStreamContext` continuously monitors both the command receiver and the stream
/// map, ensuring new views can be dynamically added and events from any active view can be
/// processed.
struct AggregatedStreamContext<K, I: Send + Sync> {
/// A map of streams identified by unique keys,
stream_map: StreamMap<K, StreamOf<I>>,
/// A receiver for handling control commands, such as adding new views.
command_receiver: CommandReceiver<Command<K, I>>,
}
impl<K, I> AggregatedStreamContext<K, I>
where
K: Send + Debug + Unpin + Clone + Default + Hash + Eq + 'static,
I: Send + Sync + 'static + PartialEq + Eq + Hash + Clone + Debug,
{
/// Creates a new aggregated stream of items and its command controller.
///
/// This function sets up the initial context with an empty stream map. The aggregated output
/// stream of items (e.g. hashes of transactions that become ready) is unfolded.
///
/// It returns a tuple containing the output stream and the command controller, allowing
/// external components to control this stream.
fn event_stream() -> (StreamOf<I>, Controller<Command<K, I>>) {
let (sender, receiver) =
pezsc_utils::mpsc::tracing_unbounded::<Command<K, I>>("import-notification-sink", 16);
let ctx = Self { stream_map: StreamMap::new(), command_receiver: receiver };
let output_stream = futures::stream::unfold(ctx, |mut ctx| async move {
loop {
tokio::select! {
biased;
cmd = ctx.command_receiver.next() => {
match cmd? {
Command::AddView(key,stream) => {
trace!(
target: LOG_TARGET,
?key,
"Command::AddView"
);
ctx.stream_map.insert(key,stream);
},
}
},
Some(event) = next_event(&mut ctx.stream_map) => {
trace!(
target: LOG_TARGET,
?event,
"import_notification_sink: select_next_some"
);
return Some((event.1, ctx));
}
}
}
})
.boxed();
(output_stream, sender)
}
}
/// A struct that facilitates the relaying notifications of ready transactions from multiple views
/// to many external sinks.
///
/// `MultiViewImportNotificationSink` provides mechanisms to dynamically add new views, filter
/// notifications of imported transactions hashes and relay them to the multiple external sinks.
#[derive(Clone)]
pub struct MultiViewImportNotificationSink<K, I: Send + Sync> {
/// A controller used to send commands to the internal [`AggregatedStreamContext`].
controller: Controller<Command<K, I>>,
/// A vector of the external sinks, each receiving a copy of the merged stream of ready
/// transaction hashes.
external_sinks: Arc<RwLock<Vec<ExternalSink<I>>>>,
/// A set of already notified items, ensuring that each item (transaction hash) is only
/// sent out once.
already_notified_items: Arc<RwLock<HashSet<I>>>,
}
/// An asynchronous task responsible for dispatching aggregated import notifications to multiple
/// sinks (created by [`MultiViewImportNotificationSink::event_stream`]).
pub type ImportNotificationTask = Pin<Box<dyn Future<Output = ()> + Send>>;
impl<K, I> MultiViewImportNotificationSink<K, I>
where
K: 'static + Clone + Send + Debug + Default + Unpin + Eq + Hash,
I: 'static + Clone + Send + Debug + Sync + PartialEq + Eq + Hash,
{
/// Creates a new [`MultiViewImportNotificationSink`] along with its associated worker task.
///
/// This function initializes the sink and provides the worker task that listens for events from
/// the aggregated stream, relaying them to the external sinks. The task shall be polled by
/// caller.
///
/// Returns a tuple containing the [`MultiViewImportNotificationSink`] and the
/// [`ImportNotificationTask`].
pub fn new_with_worker() -> (MultiViewImportNotificationSink<K, I>, ImportNotificationTask) {
let (output_stream, controller) = AggregatedStreamContext::<K, I>::event_stream();
let output_stream_controller = Self {
controller,
external_sinks: Default::default(),
already_notified_items: Default::default(),
};
let external_sinks = output_stream_controller.external_sinks.clone();
let already_notified_items = output_stream_controller.already_notified_items.clone();
let import_notifcation_task = output_stream
.for_each(move |event| {
let external_sinks = external_sinks.clone();
let already_notified_items = already_notified_items.clone();
async move {
if already_notified_items.write().insert(event.clone()) {
external_sinks.write().retain_mut(|sink| {
trace!(
target: LOG_TARGET,
?event,
"import_sink_worker sending out imported"
);
if let Err(error) = sink.try_send(event.clone()) {
trace!(
target: LOG_TARGET,
%error,
"import_sink_worker sending message failed"
);
false
} else {
true
}
});
}
}
})
.boxed();
(output_stream_controller, import_notifcation_task)
}
/// Adds a new stream associated with the view identified by specified key.
///
/// The new view's stream is added to the internal aggregated stream context by sending command
/// to its `command_receiver`.
pub fn add_view(&self, key: K, view: StreamOf<I>) {
let _ =
self.controller
.unbounded_send(Command::AddView(key.clone(), view))
.map_err(|error| {
trace!(
target: LOG_TARGET,
?key,
%error,
"add_view send message failed"
);
});
}
/// Creates and returns a new external stream of ready transactions hashes notifications.
pub fn event_stream(&self) -> EventStream<I> {
const CHANNEL_BUFFER_SIZE: usize = 1024;
let (sender, receiver) = channel(CHANNEL_BUFFER_SIZE);
self.external_sinks.write().push(sender);
receiver
}
/// Removes specified items from the `already_notified_items` set.
///
/// Intended to be called once transactions are finalized.
pub fn clean_notified_items(&self, items_to_be_removed: &[I]) {
let mut already_notified_items = self.already_notified_items.write();
items_to_be_removed.iter().for_each(|i| {
already_notified_items.remove(i);
});
}
/// Lenght of the `already_notified_items` set.
///
/// Exposed for testing only.
pub fn notified_items_len(&self) -> usize {
self.already_notified_items.read().len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::time::Duration;
use tokio::task::JoinHandle;
#[derive(Debug, Clone)]
struct Event<I: Send> {
delay: u64,
value: I,
}
impl<I: Send> From<(u64, I)> for Event<I> {
fn from(event: (u64, I)) -> Self {
Self { delay: event.0, value: event.1 }
}
}
struct View<I: Send + Sync> {
scenario: Vec<Event<I>>,
sinks: Arc<RwLock<Vec<ExternalSink<I>>>>,
}
impl<I: Send + Sync + 'static + Clone + Debug> View<I> {
fn new(scenario: Vec<(u64, I)>) -> Self {
Self {
scenario: scenario.into_iter().map(Into::into).collect(),
sinks: Default::default(),
}
}
async fn event_stream(&self) -> EventStream<I> {
let (sender, receiver) = channel(32);
self.sinks.write().push(sender);
receiver
}
fn play(&mut self) -> JoinHandle<()> {
let mut scenario = self.scenario.clone();
let sinks = self.sinks.clone();
tokio::spawn(async move {
loop {
if scenario.is_empty() {
for sink in &mut *sinks.write() {
sink.close_channel();
}
break;
};
let x = scenario.remove(0);
tokio::time::sleep(Duration::from_millis(x.delay)).await;
for sink in &mut *sinks.write() {
sink.try_send(x.value.clone()).unwrap();
}
}
})
}
}
#[tokio::test]
async fn deduplicating_works() {
pezsp_tracing::try_init_simple();
let (ctrl, runnable) = MultiViewImportNotificationSink::<u64, i32>::new_with_worker();
let j0 = tokio::spawn(runnable);
let stream = ctrl.event_stream();
let mut v1 = View::new(vec![(0, 1), (0, 2), (0, 3)]);
let mut v2 = View::new(vec![(0, 1), (0, 2), (0, 6)]);
let mut v3 = View::new(vec![(0, 1), (0, 2), (0, 3)]);
let j1 = v1.play();
let j2 = v2.play();
let j3 = v3.play();
let o1 = v1.event_stream().await.boxed();
let o2 = v2.event_stream().await.boxed();
let o3 = v3.event_stream().await.boxed();
ctrl.add_view(1000, o1);
ctrl.add_view(2000, o2);
ctrl.add_view(3000, o3);
let out = stream.take(4).collect::<Vec<_>>().await;
assert!(out.iter().all(|v| vec![1, 2, 3, 6].contains(v)));
drop(ctrl);
futures::future::join_all(vec![j0, j1, j2, j3]).await;
}
#[tokio::test]
async fn dedup_filter_reset_works() {
pezsp_tracing::try_init_simple();
let (ctrl, runnable) = MultiViewImportNotificationSink::<u64, i32>::new_with_worker();
let j0 = tokio::spawn(runnable);
let stream = ctrl.event_stream();
let stream2 = ctrl.event_stream();
let mut v1 = View::new(vec![(10, 1), (10, 2), (10, 3)]);
let mut v2 = View::new(vec![(20, 1), (20, 2), (20, 6)]);
let mut v3 = View::new(vec![(20, 1), (20, 2), (20, 3)]);
let j1 = v1.play();
let j2 = v2.play();
let j3 = v3.play();
let o1 = v1.event_stream().await.boxed();
let o2 = v2.event_stream().await.boxed();
let o3 = v3.event_stream().await.boxed();
ctrl.add_view(1000, o1);
ctrl.add_view(2000, o2);
let out = stream.take(4).collect::<Vec<_>>().await;
assert_eq!(out, vec![1, 2, 3, 6]);
ctrl.clean_notified_items(&vec![1, 3]);
ctrl.add_view(3000, o3.boxed());
let out = stream2.take(6).collect::<Vec<_>>().await;
assert_eq!(out, vec![1, 2, 3, 6, 1, 3]);
drop(ctrl);
futures::future::join_all(vec![j0, j1, j2, j3]).await;
}
#[tokio::test]
async fn many_output_streams_are_supported() {
pezsp_tracing::try_init_simple();
let (ctrl, runnable) = MultiViewImportNotificationSink::<u64, i32>::new_with_worker();
let j0 = tokio::spawn(runnable);
let stream0 = ctrl.event_stream();
let stream1 = ctrl.event_stream();
let mut v1 = View::new(vec![(0, 1), (0, 2), (0, 3)]);
let mut v2 = View::new(vec![(0, 1), (0, 2), (0, 6)]);
let mut v3 = View::new(vec![(0, 1), (0, 2), (0, 3)]);
let j1 = v1.play();
let j2 = v2.play();
let j3 = v3.play();
let o1 = v1.event_stream().await.boxed();
let o2 = v2.event_stream().await.boxed();
let o3 = v3.event_stream().await.boxed();
ctrl.add_view(1000, o1);
ctrl.add_view(2000, o2);
ctrl.add_view(3000, o3);
let out0 = stream0.take(4).collect::<Vec<_>>().await;
let out1 = stream1.take(4).collect::<Vec<_>>().await;
assert!(out0.iter().all(|v| vec![1, 2, 3, 6].contains(v)));
assert!(out1.iter().all(|v| vec![1, 2, 3, 6].contains(v)));
drop(ctrl);
futures::future::join_all(vec![j0, j1, j2, j3]).await;
}
}
@@ -0,0 +1,614 @@
// 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/>.
//! Prometheus's metrics for a fork-aware transaction pool.
use super::tx_mem_pool::InsertionInfo;
use crate::{
common::metrics::{GenericMetricsLink, MetricsRegistrant},
graph::{self, BlockHash, ExtrinsicHash},
LOG_TARGET,
};
use futures::{FutureExt, StreamExt};
use prometheus_endpoint::{
exponential_buckets, histogram_opts, linear_buckets, register, Counter, Gauge, Histogram,
PrometheusError, Registry, U64,
};
#[cfg(doc)]
use pezsc_transaction_pool_api::TransactionPool;
use pezsc_transaction_pool_api::TransactionStatus;
use pezsc_utils::mpsc;
use std::{
collections::{hash_map::Entry, HashMap},
future::Future,
pin::Pin,
time::{Duration, Instant},
};
use tracing::trace;
/// A helper alias for the Prometheus's metrics endpoint.
pub type MetricsLink = GenericMetricsLink<Metrics>;
/// Transaction pool Prometheus metrics.
pub struct Metrics {
/// Total number of transactions submitted.
pub submitted_transactions: Counter<U64>,
/// Total number of currently maintained views.
pub active_views: Gauge<U64>,
/// Total number of current inactive views.
pub inactive_views: Gauge<U64>,
/// Total number of watched transactions in txpool.
pub watched_txs: Gauge<U64>,
/// Total number of unwatched transactions in txpool.
pub unwatched_txs: Gauge<U64>,
/// Total number of transactions reported as invalid.
///
/// This only includes transaction reported as invalid by the
/// [`TransactionPool::report_invalid`] method.
pub reported_invalid_txs: Counter<U64>,
/// Total number of transactions removed as invalid.
pub removed_invalid_txs: Counter<U64>,
/// Total number of transactions from imported blocks that are unknown to the pool.
pub unknown_from_block_import_txs: Counter<U64>,
/// Total number of finalized transactions.
pub finalized_txs: Counter<U64>,
/// Histogram of maintain durations.
pub maintain_duration: Histogram,
/// Total number of transactions resubmitted from retracted forks.
pub resubmitted_retracted_txs: Counter<U64>,
/// Total number of transactions submitted from mempool to views.
pub submitted_from_mempool_txs: Counter<U64>,
/// Total number of transactions found as invalid during mempool revalidation.
pub mempool_revalidation_invalid_txs: Counter<U64>,
/// Total number of transactions found as invalid during view revalidation.
pub view_revalidation_invalid_txs: Counter<U64>,
/// Total number of valid transactions processed during view revalidation.
pub view_revalidation_resubmitted_txs: Counter<U64>,
/// Histogram of view revalidation durations.
pub view_revalidation_duration: Histogram,
/// Total number of the views created w/o cloning existing view.
pub non_cloned_views: Counter<U64>,
/// Histograms to track the timing distribution of individual transaction pool events.
pub events_histograms: EventsHistograms,
}
/// Represents a collection of histogram timings for different transaction statuses.
pub struct EventsHistograms {
/// Histogram of timings for reporting `TransactionStatus::Future` event
pub future: Histogram,
/// Histogram of timings for reporting `TransactionStatus::Ready` event
pub ready: Histogram,
/// Histogram of timings for reporting `TransactionStatus::Broadcast` event
pub broadcast: Histogram,
/// Histogram of timings for reporting `TransactionStatus::InBlock` event
pub in_block: Histogram,
/// Histogram of timings for reporting `TransactionStatus::Retracted` event
pub retracted: Histogram,
/// Histogram of timings for reporting `TransactionStatus::FinalityTimeout` event
pub finality_timeout: Histogram,
/// Histogram of timings for reporting `TransactionStatus::Finalized` event
pub finalized: Histogram,
/// Histogram of timings for reporting `TransactionStatus::Usurped(Hash)` event
pub usurped: Histogram,
/// Histogram of timings for reporting `TransactionStatus::Dropped` event
pub dropped: Histogram,
/// Histogram of timings for reporting `TransactionStatus::Invalid` event
pub invalid: Histogram,
}
impl EventsHistograms {
fn register(registry: &Registry) -> Result<Self, PrometheusError> {
Ok(Self {
future: register(
Histogram::with_opts(histogram_opts!(
"bizinikiwi_sub_txpool_timing_event_future",
"Histogram of timings for reporting Future event",
exponential_buckets(0.01, 2.0, 16).unwrap()
))?,
registry,
)?,
ready: register(
Histogram::with_opts(histogram_opts!(
"bizinikiwi_sub_txpool_timing_event_ready",
"Histogram of timings for reporting Ready event",
exponential_buckets(0.01, 2.0, 16).unwrap()
))?,
registry,
)?,
broadcast: register(
Histogram::with_opts(histogram_opts!(
"bizinikiwi_sub_txpool_timing_event_broadcast",
"Histogram of timings for reporting Broadcast event",
linear_buckets(0.01, 0.25, 16).unwrap()
))?,
registry,
)?,
in_block: register(
Histogram::with_opts(
histogram_opts!(
"bizinikiwi_sub_txpool_timing_event_in_block",
"Histogram of timings for reporting InBlock event"
)
.buckets(
[
linear_buckets(0.0, 3.0, 20).unwrap(),
// requested in #9158
vec![60.0, 75.0, 90.0, 120.0, 180.0],
]
.concat(),
),
)?,
registry,
)?,
retracted: register(
Histogram::with_opts(histogram_opts!(
"bizinikiwi_sub_txpool_timing_event_retracted",
"Histogram of timings for reporting Retracted event",
linear_buckets(0.0, 3.0, 20).unwrap()
))?,
registry,
)?,
finality_timeout: register(
Histogram::with_opts(histogram_opts!(
"bizinikiwi_sub_txpool_timing_event_finality_timeout",
"Histogram of timings for reporting FinalityTimeout event",
linear_buckets(0.0, 40.0, 20).unwrap()
))?,
registry,
)?,
finalized: register(
Histogram::with_opts(
histogram_opts!(
"bizinikiwi_sub_txpool_timing_event_finalized",
"Histogram of timings for reporting Finalized event"
)
.buckets(
[
// requested in #9158
linear_buckets(0.0, 5.0, 8).unwrap(),
linear_buckets(40.0, 40.0, 19).unwrap(),
]
.concat(),
),
)?,
registry,
)?,
usurped: register(
Histogram::with_opts(
histogram_opts!(
"bizinikiwi_sub_txpool_timing_event_usurped",
"Histogram of timings for reporting Usurped event"
)
.buckets(
[
linear_buckets(0.0, 3.0, 20).unwrap(),
// requested in #9158
vec![60.0, 75.0, 90.0, 120.0, 180.0],
]
.concat(),
),
)?,
registry,
)?,
dropped: register(
Histogram::with_opts(
histogram_opts!(
"bizinikiwi_sub_txpool_timing_event_dropped",
"Histogram of timings for reporting Dropped event"
)
.buckets(
[
linear_buckets(0.0, 3.0, 20).unwrap(),
// requested in #9158
vec![60.0, 75.0, 90.0, 120.0, 180.0],
]
.concat(),
),
)?,
registry,
)?,
invalid: register(
Histogram::with_opts(
histogram_opts!(
"bizinikiwi_sub_txpool_timing_event_invalid",
"Histogram of timings for reporting Invalid event"
)
.buckets(
[
linear_buckets(0.0, 3.0, 20).unwrap(),
// requested in #9158
vec![60.0, 75.0, 90.0, 120.0, 180.0],
]
.concat(),
),
)?,
registry,
)?,
})
}
/// Records the timing for a given transaction status.
///
/// This method records the duration, representing the time elapsed since the
/// transaction was submitted until the event was reported. Based on the
/// transaction status, it utilizes the appropriate histogram to log this duration.
pub fn observe<Hash, BlockHash>(
&self,
status: TransactionStatus<Hash, BlockHash>,
duration: Duration,
) {
let duration = duration.as_secs_f64();
let histogram = match status {
TransactionStatus::Future => &self.future,
TransactionStatus::Ready => &self.ready,
TransactionStatus::Broadcast(..) => &self.broadcast,
TransactionStatus::InBlock(..) => &self.in_block,
TransactionStatus::Retracted(..) => &self.retracted,
TransactionStatus::FinalityTimeout(..) => &self.finality_timeout,
TransactionStatus::Finalized(..) => &self.finalized,
TransactionStatus::Usurped(..) => &self.usurped,
TransactionStatus::Dropped => &self.dropped,
TransactionStatus::Invalid => &self.invalid,
};
histogram.observe(duration);
}
}
impl MetricsRegistrant for Metrics {
fn register(registry: &Registry) -> Result<Box<Self>, PrometheusError> {
Ok(Box::from(Self {
submitted_transactions: register(
Counter::new(
"bizinikiwi_sub_txpool_submitted_txs_total",
"Total number of transactions submitted",
)?,
registry,
)?,
active_views: register(
Gauge::new(
"bizinikiwi_sub_txpool_active_views",
"Total number of currently maintained views.",
)?,
registry,
)?,
inactive_views: register(
Gauge::new(
"bizinikiwi_sub_txpool_inactive_views",
"Total number of current inactive views.",
)?,
registry,
)?,
watched_txs: register(
Gauge::new(
"bizinikiwi_sub_txpool_watched_txs",
"Total number of watched transactions in txpool.",
)?,
registry,
)?,
unwatched_txs: register(
Gauge::new(
"bizinikiwi_sub_txpool_unwatched_txs",
"Total number of unwatched transactions in txpool.",
)?,
registry,
)?,
reported_invalid_txs: register(
Counter::new(
"bizinikiwi_sub_txpool_reported_invalid_txs_total",
"Total number of transactions reported as invalid by external entities using TxPool API.",
)?,
registry,
)?,
removed_invalid_txs: register(
Counter::new(
"bizinikiwi_sub_txpool_removed_invalid_txs_total",
"Total number of transactions removed as invalid.",
)?,
registry,
)?,
unknown_from_block_import_txs: register(
Counter::new(
"bizinikiwi_sub_txpool_unknown_from_block_import_txs_total",
"Total number of transactions from imported blocks that are unknown to the pool.",
)?,
registry,
)?,
finalized_txs: register(
Counter::new(
"bizinikiwi_sub_txpool_finalized_txs_total",
"Total number of finalized transactions.",
)?,
registry,
)?,
maintain_duration: register(
Histogram::with_opts(histogram_opts!(
"bizinikiwi_sub_txpool_maintain_duration_seconds",
"Histogram of maintain durations.",
linear_buckets(0.0, 0.25, 13).unwrap()
))?,
registry,
)?,
resubmitted_retracted_txs: register(
Counter::new(
"bizinikiwi_sub_txpool_resubmitted_retracted_txs_total",
"Total number of transactions resubmitted from retracted forks.",
)?,
registry,
)?,
submitted_from_mempool_txs: register(
Counter::new(
"bizinikiwi_sub_txpool_submitted_from_mempool_txs_total",
"Total number of transactions submitted from mempool to views.",
)?,
registry,
)?,
mempool_revalidation_invalid_txs: register(
Counter::new(
"bizinikiwi_sub_txpool_mempool_revalidation_invalid_txs_total",
"Total number of transactions found as invalid during mempool revalidation.",
)?,
registry,
)?,
view_revalidation_invalid_txs: register(
Counter::new(
"bizinikiwi_sub_txpool_view_revalidation_invalid_txs_total",
"Total number of transactions found as invalid during view revalidation.",
)?,
registry,
)?,
view_revalidation_resubmitted_txs: register(
Counter::new(
"bizinikiwi_sub_txpool_view_revalidation_resubmitted_txs_total",
"Total number of valid transactions processed during view revalidation.",
)?,
registry,
)?,
view_revalidation_duration: register(
Histogram::with_opts(histogram_opts!(
"bizinikiwi_sub_txpool_view_revalidation_duration_seconds",
"Histogram of view revalidation durations.",
linear_buckets(0.0, 0.25, 13).unwrap()
))?,
registry,
)?,
non_cloned_views: register(
Counter::new(
"bizinikiwi_sub_txpool_non_cloned_views_total",
"Total number of the views created w/o cloning existing view.",
)?,
registry,
)?,
events_histograms: EventsHistograms::register(registry)?,
}))
}
}
/// Messages used to report and compute event metrics.
enum EventMetricsMessage<Hash, BlockHash> {
/// Message indicating a transaction has been submitted, including the timestamp
/// and its hash.
Submitted(Instant, Hash),
/// Message indicating the new status of a transaction, including the timestamp and transaction
/// hash.
Status(Instant, Hash, TransactionStatus<Hash, BlockHash>),
}
/// Collects metrics related to transaction events.
pub struct EventsMetricsCollector<ChainApi: graph::ChainApi> {
/// Optional channel for sending event metrics messages.
///
/// If `None` no event metrics are collected (e.g. in tests).
metrics_message_sink: Option<MessageSink<ExtrinsicHash<ChainApi>, BlockHash<ChainApi>>>,
}
impl<ChainApi: graph::ChainApi> Default for EventsMetricsCollector<ChainApi> {
fn default() -> Self {
Self { metrics_message_sink: None }
}
}
impl<ChainApi: graph::ChainApi> Clone for EventsMetricsCollector<ChainApi> {
fn clone(&self) -> Self {
Self { metrics_message_sink: self.metrics_message_sink.clone() }
}
}
impl<ChainApi: graph::ChainApi> EventsMetricsCollector<ChainApi> {
/// Reports the status of a transaction.
///
/// Takes a transaction hash and status, and attempts to send a status
/// message to the metrics messages processing task.
pub fn report_status(
&self,
tx_hash: ExtrinsicHash<ChainApi>,
status: TransactionStatus<BlockHash<ChainApi>, ExtrinsicHash<ChainApi>>,
) {
self.metrics_message_sink.as_ref().map(|sink| {
if let Err(error) =
sink.unbounded_send(EventMetricsMessage::Status(Instant::now(), tx_hash, status))
{
trace!(target: LOG_TARGET, %error, "tx status metrics message send failed")
}
});
}
/// Reports that a transaction has been submitted.
///
/// Takes a transaction hash and its submission timestamp, and attempts to
/// send a submission message to the metrics messages processing task.
pub fn report_submitted(&self, insertion_info: &InsertionInfo<ExtrinsicHash<ChainApi>>) {
self.metrics_message_sink.as_ref().map(|sink| {
if let Err(error) = sink.unbounded_send(EventMetricsMessage::Submitted(
insertion_info
.source
.timestamp
.expect("timestamp is set in fork-aware pool. qed"),
insertion_info.hash,
)) {
trace!(target: LOG_TARGET, %error, "tx status metrics message send failed")
}
});
}
}
/// A type alias for a asynchronous task that collects metrics related to events.
pub type EventsMetricsCollectorTask = Pin<Box<dyn Future<Output = ()> + Send>>;
/// Sink type for sending event metrics messages.
type MessageSink<Hash, BlockHash> =
mpsc::TracingUnboundedSender<EventMetricsMessage<Hash, BlockHash>>;
/// Receiver type for receiving event metrics messages.
type MessageReceiver<Hash, BlockHash> =
mpsc::TracingUnboundedReceiver<EventMetricsMessage<Hash, BlockHash>>;
/// Holds data relevant to transaction event metrics, allowing de-duplication
/// of certain transaction statuses, and compute the timings of events.
struct TransactionEventMetricsData {
/// Flag indicating if the transaction was seen as `Ready`.
ready_seen: bool,
/// Flag indicating if the transaction was seen as `Broadcast`.
broadcast_seen: bool,
/// Flag indicating if the transaction was seen as `Future`.
future_seen: bool,
/// Flag indicating if the transaction was seen as `InBlock`.
in_block_seen: bool,
/// Flag indicating if the transaction was seen as `Retracted`.
retracted_seen: bool,
/// Timestamp when the transaction was submitted.
///
/// Used to compute a time elapsed until events are reported.
submit_timestamp: Instant,
}
impl TransactionEventMetricsData {
/// Creates a new `TransactionEventMetricsData` with the given timestamp.
fn new(submit_timestamp: Instant) -> Self {
Self {
submit_timestamp,
future_seen: false,
ready_seen: false,
broadcast_seen: false,
in_block_seen: false,
retracted_seen: false,
}
}
/// Sets flag to true once.
///
/// Return true if flag was toggled.
fn set_true_once(flag: &mut bool) -> bool {
if *flag {
false
} else {
*flag = true;
true
}
}
/// Updates the status flags based on the given transaction status.
///
/// Returns the submit timestamp if given status was not seen yet, `None` otherwise.
fn update<Hash, BlockHash>(
&mut self,
status: &TransactionStatus<Hash, BlockHash>,
) -> Option<Instant> {
let flag = match *status {
TransactionStatus::Ready => &mut self.ready_seen,
TransactionStatus::Future => &mut self.future_seen,
TransactionStatus::Broadcast(..) => &mut self.broadcast_seen,
TransactionStatus::InBlock(..) => &mut self.in_block_seen,
TransactionStatus::Retracted(..) => &mut self.retracted_seen,
_ => return Some(self.submit_timestamp),
};
Self::set_true_once(flag).then_some(self.submit_timestamp)
}
}
impl<ChainApi> EventsMetricsCollector<ChainApi>
where
ChainApi: graph::ChainApi + 'static,
{
/// Handles the status event.
///
/// Updates the metrics by observing the time taken for a transaction's status update
/// from its submission time.
fn handle_status(
hash: ExtrinsicHash<ChainApi>,
status: TransactionStatus<ExtrinsicHash<ChainApi>, BlockHash<ChainApi>>,
timestamp: Instant,
submitted_timestamp_map: &mut HashMap<ExtrinsicHash<ChainApi>, TransactionEventMetricsData>,
metrics: &MetricsLink,
) {
let Entry::Occupied(mut entry) = submitted_timestamp_map.entry(hash) else { return };
let remove = status.is_final();
if let Some(submit_timestamp) = entry.get_mut().update(&status) {
metrics.report(|metrics| {
metrics
.events_histograms
.observe(status, timestamp.duration_since(submit_timestamp))
});
}
remove.then(|| entry.remove());
}
/// Asynchronous task to process received messages and compute relevant event metrics.
///
/// Runs indefinitely, handling arriving messages and updating metrics
/// based on the recorded submission times and timestamps of current event statuses.
async fn task(
mut rx: MessageReceiver<ExtrinsicHash<ChainApi>, BlockHash<ChainApi>>,
metrics: MetricsLink,
) {
let mut submitted_timestamp_map =
HashMap::<ExtrinsicHash<ChainApi>, TransactionEventMetricsData>::default();
loop {
match rx.next().await {
Some(EventMetricsMessage::Submitted(timestamp, hash)) => {
submitted_timestamp_map
.insert(hash, TransactionEventMetricsData::new(timestamp));
},
Some(EventMetricsMessage::Status(timestamp, hash, status)) => {
Self::handle_status(
hash,
status,
timestamp,
&mut submitted_timestamp_map,
&metrics,
);
},
None => {
return; /* ? */
},
};
}
}
/// Constructs a new `EventsMetricsCollector` and its associated worker task.
///
/// Returns the collector alongside an asynchronous task. The task shall be polled by caller.
pub fn new_with_worker(metrics: MetricsLink) -> (Self, EventsMetricsCollectorTask) {
const QUEUE_WARN_SIZE: usize = 100_000;
let (metrics_message_sink, rx) =
mpsc::tracing_unbounded("txpool-event-metrics-collector", QUEUE_WARN_SIZE);
let task = Self::task(rx, metrics);
(Self { metrics_message_sink: Some(metrics_message_sink) }, task.boxed())
}
}
@@ -0,0 +1,400 @@
// 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 fork aware transaction pool implementation.
//!
//! # Top level overview.
//! This documentation provides high level overview of the main structures and the main flows within
//! the fork-aware transaction pool.
//!
//! ## Structures.
//! ### View.
//! #### Purpose.
//! The main responsibility of the [`View`] is to provide the valid set of ready transactions at
//! the given block. [`ForkAwareTxPool`] keeps the number of recent views for all the blocks
//! notified since recently finalized block.
//!
//! The views associated with blocks at the tips of the forks are actively updated with all newly
//! incoming transactions, while intermediate views are not updated (they still provide transactions
//! ready to be included at that block) due to performance reasons, since every transaction
//! submitted to the view needs to be [validated][runtime_api::validate].
//! Building upon the older blocks happens relatively rare so this does not affect blocks filling.
//!
//! The view is wrapper around [`Pool`] and exposes its functionality, including the ability
//! of [tracking][`Watcher`] the progress of every transaction.
//!
//! #### Views: active, inactive.
//! All the views are stored in [`ViewStore`] structure. In this documentation the views at the tips
//! of the forks are referred as [`active_views`], while the intermediate views as
//! [`inactive_views`].
//!
//!
//! #### The life cycle of the [`View`].
//! Views are created when the new [`ChainEvent`] is notified to the pool. The view that is
//! [closest][find_best_view] to the newly notified block is chosen to clone from. Once built and
//! updated the newly created view is placed in [`active_views`]. Detailed description of view
//! creation is described in [the material to follow](#handling-the-new-best-block). When the view
//! is no longer at the tip of the forks, it is moved to the [`inactive_views`]. When the block
//! number of the view is lower then the finalized block, the view is permanently removed.
//!
//!
//! *Example*:
//! The following chain:
//! ```text
//! C2 - C3 - C4
//! /
//! B1
//! \
//! B2 - B3 - B4
//! ```
//! and the following set of events:
//! ```text
//! New best block: B1, C3, C4, B4
//! ```
//! will result in the following set of views within the [`ViewStore`]:
//! ```text
//! active: C4, B4
//! inactive: B1, C3
//! ```
//! Please note that views are only created for the notified blocks.
//!
//!
//! ### View store.
//! [`ViewStore`] is the helper structure that provides means to perform some actions like
//! [`submit`] or [`submit_and_watch`] on every view. It keeps track of both active and inactive
//! views.
//!
//! It also keeps tracks of the `most_recent_view` which is used to implement some methods of
//! [TransactionPool API], see [API considerations](#api-considerations) section.
//!
//! ### Multi-view listeners
//! There is a number of event streams that are provided by individual views:
//! - aggregated stream of [transactions statuses][`AggregatedStream`] for all the transactions
//! within the view in the form of `(transaction-hash, status)` tuple,
//! - [ready notification][`vp::import_notification_stream`] (see [networking
//! section](#networking)),
//! - [dropped notification][`create_dropped_by_limits_stream`].
//!
//! These streams need to be merged into a single stream exposed by transaction pool (or used
//! internally). Those aggregators are often referred as multi-view listeners and they implement
//! stream-specific or event-specific logic.
//!
//! The most important is [`MultiViewListener`] which is owned by view store. Some internal details
//! on events' flow is provided in [transaction status](#monitoring-the-status-of-a-transaction)
//! section.
//!
//! ### Intermediate transactions buffer: [`TxMemPool`]
//! The main purpose of an internal [`TxMemPool`] (referred to as *mempool*) is to prevent a
//! transaction from being lost, e.g. due to race condition when the new transaction submission
//! occurs just before the new view is created. This could also happen when a transaction is invalid
//! on one fork and could be valid on another which is not yet fully processed by the maintain
//! procedure. Additionally, it allows the pool to accept transactions when no blocks have been
//! reported yet.
//!
//! The *mempool* keeps a track on how the transaction was submitted - keeping number of watched and
//! non-watched transactions is useful for testing and metrics. The [transaction
//! source][`TransactionSource`] used to submit transactions also needs to be kept in the *mempool*.
//! The *mempool* transaction is a simple [wrapper][`TxInMemPool`] around the [`Arc`] reference to
//! the actual extrinsic body.
//!
//! Once the view is created, all transactions from *mempool* are submitted to and validated at this
//! view.
//!
//! The *mempool* removes its transactions when they get finalized. The transactions in *mempool*
//! are also periodically verified at every finalized block and removed from the *mempool* if no
//! longer valid. This is process is called [*mempool* revalidation](#mempool-pruningrevalidation).
//!
//! ## Flows
//!
//! The transaction pool internally is executing numerous tasks. This includes handling submitted
//! transactions and tracking their progress, listening to [`ChainEvent`]s and executing the
//! maintain process, which aims to provide the set of ready transactions. On the other side
//! transaction pool provides a [`ready_at`] future that resolves to the iterator of ready
//! transactions. On top of that pool performs background revalidation jobs.
//!
//! This section provides a top level overview of all flows within the fork aware transaction pool.
//!
//! ### Transaction route: [`submit`][`api_submit`]
//! This flow is simple. Transaction is added to the mempool and if it is not rejected by it (due to
//! size limits), it is also [submitted][`submit`] into every view in [`active_views`].
//!
//! When the newly created view does not contain this transaction yet, it is
//! [re-submitted][ForkAwareTxPool::update_view_with_mempool] from [`TxMemPool`] into this view.
//!
//! ### Transaction route: [`submit_and_watch`][`api_submit_and_watch`]
//!
//! The [`submit_and_watch`] function allows to submit the transaction and track its
//! [status][`TransactionStatus`] within the pool.
//!
//! When a watched transaction is submitted to the pool it is added to the *mempool* with the
//! watched flag. The external stream for the transaction is created in a [`MultiViewListener`].
//! Then a transaction is submitted to every active [`View`] (using
//! [`submit_many`][`View::submit_many`]). The view's [aggregated
//! stream][`create_aggregated_stream`] was already connected to the [`MultiViewListener`] when new
//! view was created, so no additional action is required upon the submission. The view will provide
//! the required updates for all the transactions over this single stream.
//!
//!
//! #### Monitoring the status of a transaction
//!
//! Transaction status monitoring and triggering events to [external
//! listener][`TransactionStatusStreamFor`] (e.g. to RPC client) is responsibility of the
//! [`MultiViewListener`].
//!
//! Every view is providing an independent aggreagated [stream][`create_aggregated_stream`] of
//! events for all transactions in this view, which needs to be merged into the single stream
//! exposed to the [external listener][`TransactionStatusStreamFor`] (e.g. to RPC client). For
//! majority of events simple forwarding would not work (e.g. we could get multiple [`Ready`]
//! events, or [`Ready`] / [`Future`] mix). Some additional stateful logic (implemented by
//! [`MultiViewListener`]) is required to filter and process the views' events.
//!
//! It is not possible to trigger some external events (e.g., [`Dropped`], [`Finalized`],
//! [`Invalid`], and [`Broadcast`]) using only the view-aggregated streams. These events require a
//! pool-wide understanding of the transaction state. For example, dropping a transaction from a
//! single view does not mean it was dropped from other views. Broadcast and finalized notifications
//! are sent to the transaction pool API, not at the view level. These events are simply ignored
//! when they originate in the view. The pool uses a dedicated side channel exposed by
//! [`MultiViewListener`] to trigger the beforementioned events.
//!
//! ### Maintain
//! The transaction pool exposes the [task][`notification_future`] that listens to the
//! finalized and best block streams and executes the [`maintain`] procedure.
//!
//! The [`maintain`] is the main procedure of the transaction pool. It handles incoming
//! [`ChainEvent`]s, as described in the following two sub-sections.
//!
//! #### Handling the new (best) block
//! If the new block actually needs to be handled, the following steps are
//! executed:
//! - [find][find_best_view] the best view and clone it to [create a new
//! view][crate::ForkAwareTxPool::build_new_view],
//! - [update the view][ForkAwareTxPool::update_view_with_mempool] with the transactions from the
//! *mempool*
//! - all transactions from the *mempool* (with some obvious filtering applied) are submitted to
//! the view,
//! - the new [aggregated stream][`create_aggregated_stream`] of all transactions statuses is
//! created for the new view and it is connected to the multi-view-listener,
//! - [update the view][ForkAwareTxPool::update_view_with_fork] with the transactions from the [tree
//! route][`TreeRoute`] (which is computed from the recent best block to newly notified one by
//! [enactment state][`EnactmentState`] helper):
//! - resubmit the transactions from the retracted blocks,
//! - prune extrinsic from the enacted blocks, and trigger [`InBlock`] events,
//! - insert the newly created and updated view into the view store.
//!
//!
//! #### Handling the finalized block
//! The following actions are taken on every finalized block:
//! - send [`Finalized`] events for every transactions on the finalized [tree route][`TreeRoute`],
//! - remove all the views (both active and inactive) that are lower then finalized block from the
//! view store,
//! - removal of finalized transaction from the *mempool*,
//! - trigger [*mempool* background revalidation](#mempool-pruningrevalidation).
//! - clean up of multi-view listeners which is required to avoid ever-growing structures,
//!
//! ### Light maintain
//! The [maintain](#maintain) procedure can sometimes be quite heavy, and it may not be accomplished
//! within the time window expected by the block builder. On top of that block builder may want to
//! build few blocks in the row, not giving the pool enough time to accomplish possible ongoing
//! maintain process.
//!
//! To address this, there is a [light version][`ready_at_light`] of the maintain procedure. It
//! [finds the first descendent view][`find_view_descendent_up_to_number`] up to the recent
//! finalized block, clones it and prunes all the transactions that were included in enacted part of
//! the traversed route, from the base view to the block at which a ready iterator was requested. No
//! new [transaction validations][runtime_api::validate] are required to accomplish it. If no view
//! is found, it will return the ready transactions of the most recent view processed by the
//! transaction pool.
//!
//! ### Providing ready transactions: `ready_at`
//! The asynchronous [`ready_at`] function resolves to the [ready transactions
//! iterator][`ReadyTransactions`]. The block builder shall wait either for the future to be
//! resolved or for timeout to be hit. To avoid building empty blocks in case of timeout, the
//! waiting for timeout functionality was moved into the transaction pool, and new API function was
//! added: [`ready_at_with_timeout`]. This function also provides a fall back ready iterator which
//! is result of [light maintain](#light-maintain).
//!
//! New function internally waits either for [maintain](#maintain) process triggered for requested
//! block to be accomplished or for the timeout. If timeout hits then the result of [light
//! maintain](#light-maintain) is returned. Light maintain is always executed at the beginning of
//! [`ready_at_with_timeout`] to make sure that it is available w/ o additional delay.
//!
//! If the maintain process for the requested block was accomplished before the `ready_at` functions
//! are called both of them immediately provide the ready transactions iterator (which is simply
//! requested on the appropriate instance of the [`View`]).
//!
//! The little [`ReadyPoll`] helper contained within [`ForkAwareTxPool`] as ([`ready_poll`])
//! implements the futures management.
//!
//! ### Background tasks
//! The [maintain](#maintain) procedure shall be as quick as possible, so heavy revalidation job is
//! delegated to the background worker. These includes view and *mempool* revalidation which are
//! both handled by the [`RevalidationQueue`] which simply sends revalidation requests to the
//! background thread.
//!
//! #### View revalidation
//! View revalidation is performed in the background thread. Revalidation is executed for every
//! view. All the transaction from the view are [revalidated][`view::revalidate`].
//!
//! The fork-aware pool utilizes two threads to execute maintain and revalidation process
//! exclusively, ensuring maintain performance without overlapping with revalidation.
//!
//! The view revalidation process is [triggered][`start_background_revalidation`] at the very end of
//! the [maintain][`maintain`] process, and [stopped][`finish_background_revalidations`] at the
//! very beginning of the next maintenance execution (upon the next [`ChainEvent`] reception). The
//! results from the revalidation are immediately applied once the revalidation is
//! [terminated][crate::fork_aware_txpool::view::View::finish_revalidation].
//! ```text
//! time: ---------------------->
//! maintenance thread: M----M------M--M-M---
//! revalidation thread: -RRRR-RR-----RR-R-RRR
//! ```
//!
//! #### Mempool pruning/revalidation
//! Transactions within *mempool* are constantly revalidated in the background. The
//! [revalidation][`mp::revalidate`] is performed in [batches][`batch_size`], and transactions that
//! were validated as latest, are revalidated first in the next iteration. The revalidation is
//! triggered on every finalized block. If a transaction is found to be invalid, the [`Invalid`]
//! event is sent and transaction is removed from the *mempool*.
//!
//! NOTE: There is one exception: if transaction is referenced by any view as ready, then it is
//! removed from the *mempool*, but not removed from the view. The [`Invalid`] event is not sent.
//! This case is not likely to happen, however it may need some extra attention.
//!
//! ### Networking
//! The pool is exposing [`ImportNotificationStream`][`import_notification_stream`], the dedicated
//! channel over which all ready transactions are notified. Internally this channel needs to merge
//! all ready events from every view. This functionality is implemented by
//! [`MultiViewImportNotificationSink`].
//!
//! The networking module is utilizing this channel to receive info about new ready transactions
//! which later will be propagated over the network. On the other side, when a transaction is
//! received networking submits transaction to the pool using [`submit`][`api_submit`].
//!
//! ### Handling invalid transactions
//! Refer to *mempool* revalidation [section](#mempool-pruningrevalidation).
//!
//! ## Pool limits
//! Every [`View`] has the [limits][`Options`] for the number or size of transactions it can hold.
//! Obviously the number of transactions in every view is not distributed equally, so some views
//! might be fully filled while others not.
//!
//! On the other hand the size of internal *mempool* shall also be capped, but transactions that are
//! still referenced by views should not be removed.
//!
//! When the [`View`] is at its limits, it can either reject the transaction during
//! submission process, or it can accept the transaction and drop different transaction which is
//! already in the pool during the [`enforce_limits`][`vp::enforce_limits`] process.
//!
//! The [`StreamOfDropped`] stream aggregating [per-view][`create_dropped_by_limits_stream`] streams
//! allows to monitor the transactions that were dropped by all the views (or dropped by some views
//! while not referenced by the others), what means that transaction can also be
//! [removed][`dropped_monitor_task`] from the *mempool*.
//!
//!
//! ## API Considerations
//! Refer to github issue: <https://github.com/pezkuwichain/pezkuwi-sdk/issues/141>
//!
//! [`View`]: crate::fork_aware_txpool::view::View
//! [`view::revalidate`]: crate::fork_aware_txpool::view::View::revalidate
//! [`start_background_revalidation`]: crate::fork_aware_txpool::view::View::start_background_revalidation
//! [`View::submit_many`]: crate::fork_aware_txpool::view::View::submit_many
//! [`ViewStore`]: crate::fork_aware_txpool::view_store::ViewStore
//! [`finish_background_revalidations`]: crate::fork_aware_txpool::view_store::ViewStore::finish_background_revalidations
//! [find_best_view]: crate::fork_aware_txpool::view_store::ViewStore::find_best_view
//! [`find_view_descendent_up_to_number`]: crate::fork_aware_txpool::view_store::ViewStore::find_view_descendent_up_to_number
//! [`active_views`]: crate::fork_aware_txpool::view_store::ViewStore::active_views
//! [`inactive_views`]: crate::fork_aware_txpool::view_store::ViewStore::inactive_views
//! [`TxMemPool`]: crate::fork_aware_txpool::tx_mem_pool::TxMemPool
//! [`mp::revalidate`]: crate::fork_aware_txpool::tx_mem_pool::TxMemPool::revalidate
//! [`batch_size`]: crate::fork_aware_txpool::tx_mem_pool::TXMEMPOOL_MAX_REVALIDATION_BATCH_SIZE
//! [`TxInMemPool`]: crate::fork_aware_txpool::tx_mem_pool::TxInMemPool
//! [`MultiViewListener`]: crate::fork_aware_txpool::multi_view_listener::MultiViewListener
//! [`Pool`]: crate::graph::Pool
//! [`Watcher`]: crate::graph::watcher::Watcher
//! [`AggregatedStream`]: crate::fork_aware_txpool::view::AggregatedStream
//! [`Options`]: crate::graph::Options
//! [`vp::import_notification_stream`]: ../graph/validated_pool/struct.ValidatedPool.html#method.import_notification_stream
//! [`vp::enforce_limits`]: ../graph/validated_pool/struct.ValidatedPool.html#method.enforce_limits
//! [`create_dropped_by_limits_stream`]: ../graph/validated_pool/struct.ValidatedPool.html#method.create_dropped_by_limits_stream
//! [`create_aggregated_stream`]: ../graph/validated_pool/struct.ValidatedPool.html#method.create_aggregated_stream
//! [`ChainEvent`]: pezsc_transaction_pool_api::ChainEvent
//! [`TransactionStatusStreamFor`]: pezsc_transaction_pool_api::TransactionStatusStreamFor
//! [`api_submit`]: pezsc_transaction_pool_api::TransactionPool::submit_at
//! [`api_submit_and_watch`]: pezsc_transaction_pool_api::TransactionPool::submit_and_watch
//! [`ready_at_with_timeout`]: pezsc_transaction_pool_api::TransactionPool::ready_at_with_timeout
//! [`TransactionSource`]: pezsc_transaction_pool_api::TransactionSource
//! [TransactionPool API]: pezsc_transaction_pool_api::TransactionPool
//! [`TransactionStatus`]:pezsc_transaction_pool_api::TransactionStatus
//! [`Ready`]:pezsc_transaction_pool_api::TransactionStatus::Ready
//! [`Future`]:pezsc_transaction_pool_api::TransactionStatus::Future
//! [`Broadcast`]:pezsc_transaction_pool_api::TransactionStatus::Broadcast
//! [`Invalid`]:pezsc_transaction_pool_api::TransactionStatus::Invalid
//! [`InBlock`]:pezsc_transaction_pool_api::TransactionStatus::InBlock
//! [`Finalized`]:pezsc_transaction_pool_api::TransactionStatus::Finalized
//! [`Dropped`]:pezsc_transaction_pool_api::TransactionStatus::Dropped
//! [`ReadyTransactions`]:pezsc_transaction_pool_api::ReadyTransactions
//! [`dropped_monitor_task`]: ForkAwareTxPool::dropped_monitor_task
//! [`ready_poll`]: ForkAwareTxPool::ready_poll
//! [`ready_at_light`]: ForkAwareTxPool::ready_at_light
//! [`ready_at`]: ../struct.ForkAwareTxPool.html#method.ready_at
//! [`import_notification_stream`]: ../struct.ForkAwareTxPool.html#method.import_notification_stream
//! [`maintain`]: ../struct.ForkAwareTxPool.html#method.maintain
//! [`submit`]: ../struct.ForkAwareTxPool.html#method.submit_at
//! [`submit_and_watch`]: ../struct.ForkAwareTxPool.html#method.submit_and_watch
//! [`ReadyPoll`]: ../fork_aware_txpool/fork_aware_txpool/struct.ReadyPoll.html
//! [`TreeRoute`]: pezsp_blockchain::TreeRoute
//! [runtime_api::validate]: pezsp_transaction_pool::runtime_api::TaggedTransactionQueue::validate_transaction
//! [`notification_future`]: crate::common::notification_future
//! [`EnactmentState`]: crate::common::enactment_state::EnactmentState
//! [`MultiViewImportNotificationSink`]: crate::fork_aware_txpool::import_notification_sink::MultiViewImportNotificationSink
//! [`RevalidationQueue`]: crate::fork_aware_txpool::revalidation_worker::RevalidationQueue
//! [`StreamOfDropped`]: crate::fork_aware_txpool::dropped_watcher::StreamOfDropped
//! [`Arc`]: std::sync::Arc
mod dropped_watcher;
pub(crate) mod fork_aware_txpool;
mod import_notification_sink;
mod metrics;
mod multi_view_listener;
mod revalidation_worker;
mod tx_mem_pool;
mod view;
mod view_store;
pub use fork_aware_txpool::{ForkAwareTxPool, ForkAwareTxPoolTask};
mod stream_map_util {
use futures::Stream;
use std::marker::Unpin;
use tokio_stream::StreamMap;
pub async fn next_event<K, V>(
stream_map: &mut StreamMap<K, V>,
) -> Option<(K, <V as Stream>::Item)>
where
K: Clone + Unpin,
V: Stream + Unpin,
{
if stream_map.is_empty() {
// yield pending to prevent busy-loop on an empty map
futures::pending!()
}
futures::StreamExt::next(stream_map).await
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,252 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! The background worker for the [`View`] and [`TxMemPool`] revalidation.
//!
//! The [*Background tasks*](../index.html#background-tasks) section provides some extra details on
//! revalidation process.
use std::{marker::PhantomData, pin::Pin, sync::Arc};
use crate::{graph::ChainApi, LOG_TARGET};
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
use pezsp_blockchain::HashAndNumber;
use pezsp_runtime::traits::Block as BlockT;
use super::{tx_mem_pool::TxMemPool, view_store::ViewStore};
use futures::prelude::*;
use tracing::{debug, warn};
use super::view::{FinishRevalidationWorkerChannels, View};
/// Revalidation request payload sent from the queue to the worker.
enum WorkerPayload<Api, Block>
where
Block: BlockT,
Api: ChainApi<Block = Block> + 'static,
{
/// Request to revalidated the given instance of the [`View`]
///
/// Communication channels with maintain thread are also provided.
RevalidateView(Arc<View<Api>>, FinishRevalidationWorkerChannels<Api>),
/// Request to revalidated the given instance of the [`TxMemPool`] at provided block hash.
RevalidateMempool(Arc<TxMemPool<Api, Block>>, Arc<ViewStore<Api, Block>>, HashAndNumber<Block>),
}
/// The background revalidation worker.
struct RevalidationWorker<Block: BlockT> {
_phantom: PhantomData<Block>,
}
impl<Block> RevalidationWorker<Block>
where
Block: BlockT,
<Block as BlockT>::Hash: Unpin,
{
/// Create a new instance of the background worker.
fn new() -> Self {
Self { _phantom: Default::default() }
}
/// A background worker main loop.
///
/// Waits for and dispatches the [`WorkerPayload`] messages sent from the
/// [`RevalidationQueue`].
pub async fn run<Api: ChainApi<Block = Block> + 'static>(
self,
from_queue: TracingUnboundedReceiver<WorkerPayload<Api, Block>>,
) {
let mut from_queue = from_queue.fuse();
loop {
let Some(payload) = from_queue.next().await else {
// R.I.P. worker!
break;
};
match payload {
WorkerPayload::RevalidateView(view, worker_channels) =>
view.revalidate(worker_channels).await,
WorkerPayload::RevalidateMempool(
mempool,
view_store,
finalized_hash_and_number,
) => mempool.revalidate(view_store, finalized_hash_and_number).await,
};
}
}
}
/// A Revalidation queue.
///
/// Allows to send the revalidation requests to the [`RevalidationWorker`].
pub struct RevalidationQueue<Api, Block>
where
Api: ChainApi<Block = Block> + 'static,
Block: BlockT,
{
background: Option<TracingUnboundedSender<WorkerPayload<Api, Block>>>,
}
impl<Api, Block> RevalidationQueue<Api, Block>
where
Api: ChainApi<Block = Block> + 'static,
Block: BlockT,
<Block as BlockT>::Hash: Unpin,
{
/// New revalidation queue without background worker.
///
/// All validation requests will be blocking.
pub fn new() -> Self {
Self { background: None }
}
/// New revalidation queue with background worker.
///
/// All validation requests will be executed in the background.
pub fn new_with_worker() -> (Self, Pin<Box<dyn Future<Output = ()> + Send>>) {
let (to_worker, from_queue) = tracing_unbounded("mpsc_revalidation_queue", 100_000);
(Self { background: Some(to_worker) }, RevalidationWorker::new().run(from_queue).boxed())
}
/// Queue the view for later revalidation.
///
/// If the queue is configured with background worker, this will return immediately.
/// If the queue is configured without background worker, this will resolve after
/// revalidation is actually done.
///
/// Schedules execution of the [`View::revalidate`].
pub async fn revalidate_view(
&self,
view: Arc<View<Api>>,
finish_revalidation_worker_channels: FinishRevalidationWorkerChannels<Api>,
) {
debug!(
target: LOG_TARGET,
view_at_hash = ?view.at.hash,
"revalidation_queue::revalidate_view: Sending view to revalidation queue"
);
if let Some(ref to_worker) = self.background {
if let Err(error) = to_worker.unbounded_send(WorkerPayload::RevalidateView(
view,
finish_revalidation_worker_channels,
)) {
warn!(
target: LOG_TARGET,
?error,
"revalidation_queue::revalidate_view: Failed to update background worker"
);
}
} else {
view.revalidate(finish_revalidation_worker_channels).await
}
}
/// Revalidates the given mempool instance.
///
/// If queue configured with background worker, this will return immediately.
/// If queue configured without background worker, this will resolve after
/// revalidation is actually done.
///
/// Schedules execution of the [`TxMemPool::revalidate`].
pub async fn revalidate_mempool(
&self,
mempool: Arc<TxMemPool<Api, Block>>,
view_store: Arc<ViewStore<Api, Block>>,
finalized_hash: HashAndNumber<Block>,
) {
debug!(
target: LOG_TARGET,
?finalized_hash,
"Sent mempool to revalidation queue"
);
if let Some(ref to_worker) = self.background {
if let Err(error) = to_worker.unbounded_send(WorkerPayload::RevalidateMempool(
mempool,
view_store,
finalized_hash,
)) {
warn!(
target: LOG_TARGET,
?error,
"Failed to update background worker"
);
}
} else {
mempool.revalidate(view_store, finalized_hash).await
}
}
}
#[cfg(test)]
//todo: add more tests [#5480]
mod tests {
use super::*;
use crate::{
common::tests::{uxt, TestApi},
fork_aware_txpool::view::FinishRevalidationLocalChannels,
TimedTransactionSource, ValidateTransactionPriority,
};
use futures::executor::block_on;
use bizinikiwi_test_runtime::{AccountId, Transfer, H256};
use bizinikiwi_test_runtime_client::Sr25519Keyring::Alice;
#[test]
fn revalidation_queue_works() {
let api = Arc::new(TestApi::default());
let block0 = api.expect_hash_and_number(0);
let view = Arc::new(
View::new(api.clone(), block0, Default::default(), Default::default(), false.into()).0,
);
let queue = Arc::new(RevalidationQueue::new());
let uxt = uxt(Transfer {
from: Alice.into(),
to: AccountId::from_h256(H256::from_low_u64_be(2)),
amount: 5,
nonce: 0,
});
let _ = block_on(view.submit_many(
std::iter::once((TimedTransactionSource::new_external(false), uxt.clone().into())),
ValidateTransactionPriority::Submitted,
));
assert_eq!(api.validation_requests().len(), 1);
let (finish_revalidation_request_tx, finish_revalidation_request_rx) =
tokio::sync::mpsc::channel(1);
let (revalidation_result_tx, revalidation_result_rx) = tokio::sync::mpsc::channel(1);
let finish_revalidation_worker_channels = FinishRevalidationWorkerChannels::new(
finish_revalidation_request_rx,
revalidation_result_tx,
);
let _finish_revalidation_local_channels = FinishRevalidationLocalChannels::new(
finish_revalidation_request_tx,
revalidation_result_rx,
);
block_on(queue.revalidate_view(view.clone(), finish_revalidation_worker_channels));
assert_eq!(api.validation_requests().len(), 2);
// number of ready
assert_eq!(view.status().ready, 1);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,764 @@
// 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/>.
//! Provides structures for storing and managing transactions in a transaction memory pool.
//!
//! The module includes `SizeTrackedStore`, a map designed for concurrent access, and
//! `IndexedStorage`, which manages transaction entries by key and priority. Transactions are stored
//! efficiently with operations to insert items based on priority and manage space utilization. This
//! module provides core functionality for maintaining the `TxMemPool` state.
use std::{
collections::{BTreeMap, HashMap},
sync::{
atomic::{AtomicIsize, Ordering as AtomicOrdering},
Arc,
},
};
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
/// Something that can report its size.
pub(super) trait Size {
fn size(&self) -> usize;
}
/// Trait for items with a priority and timestamp ordering.
///
/// `PriorityAndTimestamp` defines methods to access the priority and timestamp,
/// facilitating sorting in structures like `SizeTrackedStore`.
pub(super) trait PriorityAndTimestamp {
type Priority: Ord;
type Timestamp: Ord;
fn priority(&self) -> Self::Priority;
fn timestamp(&self) -> Self::Timestamp;
}
/// A dual-key struct for ordering by priority and timestamp.
///
/// `PriorityKey<U, V>` allows sorting where the primary criteria is `Priority`
/// and ties are broken using `Timestamp`.
#[derive(Eq, PartialEq, Ord, PartialOrd, Debug)]
pub struct PriorityKey<U, V>(U, V);
/// Composite key for sorting collections by priority keys (embedded in `PriorityKey`) and item key
/// (typically hash).
#[derive(Eq, PartialEq, Ord, PartialOrd, Debug)]
struct SortKey<P, K>(P, K);
impl<K, A, B> SortKey<PriorityKey<A, B>, K>
where
K: Ord + Copy,
A: Ord,
B: Ord,
{
/// Creates a new `SortKey` for a given item and its key, based on the item's priority and
/// timestamp.
fn new<V: PriorityAndTimestamp<Priority = A, Timestamp = B>>(key: &K, item: &V) -> Self {
Self(PriorityKey(item.priority(), item.timestamp()), *key)
}
}
/// Internal storage for managing TxMemPool entries.
///
/// `IndexedStorage` uses `HashMap` for fast access by key and `BTreeMap` for
/// efficient priority ordering.
#[derive(Debug)]
struct IndexedStorage<K, S, V>
where
K: Ord,
S: Ord,
{
/// HashMap storing transactions by unique key for quick access.
items_by_hashes: HashMap<K, V>,
/// BTreeMap ordering transactions for prioritized access based on sort key.
items_by_priority: BTreeMap<SortKey<S, K>, V>,
}
/// Core structure for storing and managing transactions in TxMemPool.
///
/// `SizeTrackedStore` is a thread-safe map designed to track the size and priority
/// of transactions, optimized for use in a transaction memory pool. The map
/// preserves sorting order based on priority and timestamp.
///
/// Size reported might be slightly off and only approximately true.
#[derive(Debug)]
pub struct SizeTrackedStore<K, S, V>
where
K: Ord,
S: Ord,
{
/// Internal storage maintaining transaction entries.
index: Arc<RwLock<IndexedStorage<K, S, V>>>,
/// Atomic counter tracking the total size, in bytes, of all transactions.
bytes: AtomicIsize,
/// Atomic counter maintaining the count of transactions.
length: AtomicIsize,
}
impl<K, S, V> Default for IndexedStorage<K, S, V>
where
K: Ord,
S: Ord,
{
fn default() -> Self {
Self { items_by_hashes: Default::default(), items_by_priority: Default::default() }
}
}
impl<K, S, V> IndexedStorage<K, S, V>
where
K: Ord + std::hash::Hash,
S: Ord,
{
/// Retrieves a reference to the value corresponding to the key, if present.
pub fn get(&self, key: &K) -> Option<&V> {
self.items_by_hashes.get(key)
}
/// Checks if the map contains the specified key.
pub fn contains_key(&self, key: &K) -> bool {
self.items_by_hashes.contains_key(key)
}
/// Returns an iterator over the values in the map.
pub fn values(&self) -> std::collections::hash_map::Values<'_, K, V> {
self.items_by_hashes.values()
}
/// Returns the number of elements in the map.
pub fn len(&self) -> usize {
debug_assert_eq!(self.items_by_hashes.len(), self.items_by_priority.len());
self.items_by_hashes.len()
}
/// Removes and returns the first entry in the priority map, if it exists (testing only).
#[cfg(test)]
pub fn pop_first(&mut self) -> Option<V> {
self.items_by_priority.pop_first().map(|(_, v)| v)
}
pub fn with_items<F, R>(&self, f: F) -> R
where
F: Fn(std::collections::hash_map::Iter<K, V>) -> R,
{
f(self.items_by_hashes.iter())
}
}
impl<K, A, B, V> IndexedStorage<K, PriorityKey<A, B>, V>
where
K: Ord + std::hash::Hash + Copy,
A: Ord,
B: Ord,
V: Clone + PriorityAndTimestamp<Priority = A, Timestamp = B>,
V: std::cmp::PartialEq + std::fmt::Debug,
{
/// Inserts a key-value pair into the map, ordering by priority.
pub fn insert(&mut self, key: K, val: V) -> Option<V> {
let r = self.items_by_hashes.insert(key, val.clone());
if let Some(ref removed) = r {
let a = self.items_by_priority.remove(&SortKey::new(&key, removed));
debug_assert_eq!(r, a);
}
let a = self.items_by_priority.insert(SortKey::new(&key, &val), val);
debug_assert!(a.is_none());
r
}
/// Removes a key-value pair from the map based on the key.
pub fn remove(&mut self, key: &K) -> Option<V> {
let r = self.items_by_hashes.remove(key);
let _ = r.as_ref().map(|r| {
let k = SortKey::new(key, r);
let a = self.items_by_priority.remove(&k);
debug_assert_eq!(r.clone(), a.expect("item should be in both maps. qed."));
});
r
}
/// Allows to mutate item for given key with a closure, if key exists.
///
/// Intended to mutate priority and timestamp. Changing size is not possible.
pub fn update_item<F>(&mut self, key: &K, f: F) -> Option<()>
where
F: FnOnce(&mut V),
{
let item = self.items_by_hashes.get_mut(key)?;
let old_key = SortKey::new(key, item);
f(item);
let new_key = SortKey::new(key, item);
if old_key != new_key {
self.items_by_priority.remove(&old_key);
self.items_by_priority.insert(new_key, item.clone());
}
Some(())
}
}
impl<K, A, B, V> IndexedStorage<K, PriorityKey<A, B>, V>
where
K: Ord + std::hash::Hash + Copy + std::fmt::Debug,
A: Ord + std::fmt::Debug,
B: Ord + std::fmt::Debug,
V: Clone + PriorityAndTimestamp<Priority = A, Timestamp = B> + Size,
V: std::cmp::PartialEq + std::fmt::Debug,
{
/// Attempts to insert an item with replacement based on free space and priority.
/// Returns the total size in bytes of removed items, and their keys.
///
/// Insertion always results with other item's removal, the len bound is kept elsewhere
///
/// If nothing was inserted `(None,0)` is returned.
pub fn try_insert_with_replacement(
&mut self,
free_bytes: usize,
key: K,
item: V,
) -> (Option<Vec<K>>, usize) {
let mut total_size_removed = 0usize;
let mut to_be_removed = vec![];
if item.size() == 0 {
return (None, 0);
}
if self.contains_key(&key) {
return (None, 0);
}
for (SortKey(PriorityKey(worst_priority, worst_timestamp), worst_key), worst_item) in
&self.items_by_priority
{
if *worst_priority > item.priority() {
return (None, 0);
}
if *worst_priority == item.priority() && *worst_timestamp < item.timestamp() {
return (None, 0);
}
total_size_removed += worst_item.size();
to_be_removed.push(*worst_key);
if free_bytes + total_size_removed >= item.size() {
break;
}
}
if item.size() > free_bytes + total_size_removed {
return (None, 0);
}
self.insert(key, item);
for worst_key in &to_be_removed {
self.remove(worst_key);
}
(Some(to_be_removed), total_size_removed)
}
}
impl<K, S, V> Default for SizeTrackedStore<K, S, V>
where
K: Ord,
S: Ord,
{
fn default() -> Self {
Self {
index: Arc::new(IndexedStorage::default().into()),
bytes: 0.into(),
length: 0.into(),
}
}
}
//
impl<K, S, V> SizeTrackedStore<K, S, V>
where
K: Ord,
S: Ord,
{
/// Current tracked length of the content.
pub fn len(&self) -> usize {
std::cmp::max(self.length.load(AtomicOrdering::Relaxed), 0) as usize
}
/// Current sum of content length.
pub fn bytes(&self) -> usize {
std::cmp::max(self.bytes.load(AtomicOrdering::Relaxed), 0) as usize
}
/// Lock map for read.
pub async fn read(&self) -> SizeTrackedStoreReadAccess<'_, K, S, V> {
SizeTrackedStoreReadAccess { inner_guard: self.index.read().await }
}
/// Lock map for write.
pub async fn write(&self) -> SizeTrackedStoreWriteAccess<'_, K, S, V> {
SizeTrackedStoreWriteAccess {
inner_guard: self.index.write().await,
bytes: &self.bytes,
length: &self.length,
}
}
}
pub struct SizeTrackedStoreReadAccess<'a, K, S, V>
where
K: Ord,
S: Ord,
{
inner_guard: RwLockReadGuard<'a, IndexedStorage<K, S, V>>,
}
impl<K, S, V> SizeTrackedStoreReadAccess<'_, K, S, V>
where
K: Ord + std::hash::Hash,
S: Ord,
{
/// Returns true if the map contains given key.
pub fn contains_key(&self, key: &K) -> bool {
self.inner_guard.contains_key(key)
}
/// Returns the reference to the contained value by key, if exists.
pub fn get(&self, key: &K) -> Option<&V> {
self.inner_guard.get(key)
}
/// Returns an iterator over all values.
pub fn values(&self) -> std::collections::hash_map::Values<'_, K, V> {
self.inner_guard.values()
}
/// Returns the number of elements in the map.
pub fn len(&self) -> usize {
self.inner_guard.len()
}
pub fn with_items<F, R>(&self, f: F) -> R
where
F: Fn(std::collections::hash_map::Iter<K, V>) -> R,
{
self.inner_guard.with_items(f)
}
}
pub struct SizeTrackedStoreWriteAccess<'a, K, S, V>
where
K: Ord,
S: Ord,
{
bytes: &'a AtomicIsize,
length: &'a AtomicIsize,
inner_guard: RwLockWriteGuard<'a, IndexedStorage<K, S, V>>,
}
impl<K, A, B, V> SizeTrackedStoreWriteAccess<'_, K, PriorityKey<A, B>, V>
where
K: Ord + std::hash::Hash + Copy + std::fmt::Debug,
A: Ord + std::fmt::Debug,
B: Ord + std::fmt::Debug,
V: Clone + PriorityAndTimestamp<Priority = A, Timestamp = B> + Size,
V: std::cmp::PartialEq + std::fmt::Debug,
{
/// Insert value and return previous (if any).
pub fn insert(&mut self, key: K, val: V) -> Option<V> {
let new_bytes = val.size();
self.bytes.fetch_add(new_bytes as isize, AtomicOrdering::Relaxed);
self.length.fetch_add(1, AtomicOrdering::Relaxed);
self.inner_guard.insert(key, val).inspect(|old_val| {
self.bytes.fetch_sub(old_val.size() as isize, AtomicOrdering::Relaxed);
self.length.fetch_sub(1, AtomicOrdering::Relaxed);
})
}
/// Remove value by key.
pub fn remove(&mut self, key: &K) -> Option<V> {
let val = self.inner_guard.remove(key);
if let Some(size) = val.as_ref().map(Size::size) {
self.bytes.fetch_sub(size as isize, AtomicOrdering::Relaxed);
self.length.fetch_sub(1, AtomicOrdering::Relaxed);
}
val
}
/// Refer to [`IndexedStorage::try_insert_with_replacement`]
pub fn try_insert_with_replacement(
&mut self,
max_total_bytes: usize,
key: K,
item: V,
) -> Option<Vec<K>> {
let item_size = item.size();
let current_bytes = std::cmp::max(self.bytes.load(AtomicOrdering::Relaxed), 0) as usize;
let free_bytes = max_total_bytes - current_bytes;
let (removed_keys, removed_bytes) =
self.inner_guard.try_insert_with_replacement(free_bytes, key, item);
if let Some(ref removed_keys) = removed_keys {
let delta = item_size as isize - removed_bytes as isize;
self.bytes.fetch_add(delta, AtomicOrdering::Relaxed);
self.length.fetch_sub(removed_keys.len() as isize, AtomicOrdering::Relaxed);
self.length.fetch_add(1, AtomicOrdering::Relaxed);
}
removed_keys
}
/// Allows to mutate item for given key, if exists.
///
/// Intended to mutate priority and timestamp. Changing size is not possible.
pub fn update_item<F>(&mut self, hash: &K, f: F) -> Option<()>
where
F: FnOnce(&mut V),
{
self.inner_guard.update_item(hash, f)
}
}
impl<K, S, V> SizeTrackedStoreWriteAccess<'_, K, S, V>
where
K: Ord + std::hash::Hash,
S: Ord,
{
/// Returns `true` if the inner map contains a value for the specified key.
pub fn contains_key(&self, key: &K) -> bool {
self.inner_guard.contains_key(key)
}
/// Returns the number of elements in the map.
pub fn len(&mut self) -> usize {
self.inner_guard.len()
}
#[cfg(test)]
pub fn pop_first(&mut self) -> Option<V> {
self.inner_guard.pop_first()
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[derive(Clone, Debug, PartialEq)]
struct TestItem {
size: usize,
prio: u32,
ts: u32,
}
impl PriorityAndTimestamp for TestItem {
type Priority = u32;
type Timestamp = u32;
fn priority(&self) -> Self::Priority {
self.prio
}
fn timestamp(&self) -> Self::Timestamp {
self.ts
}
}
impl Size for TestItem {
fn size(&self) -> usize {
self.size
}
}
impl TestItem {
fn new(prio: u32, ts: u32, size: usize) -> Self {
Self { prio, ts, size }
}
}
#[tokio::test]
async fn basic() {
let map = SizeTrackedStore::default();
let i0 = TestItem::new(1, 0, 10);
let i1 = TestItem::new(2, 0, 11);
let i2 = TestItem::new(3, 0, 20);
map.write().await.insert(0xa, i0);
map.write().await.insert(0xb, i1);
assert_eq!(map.bytes(), 21);
assert_eq!(map.len(), 2);
map.write().await.insert(0xc, i2);
assert_eq!(map.bytes(), 41);
assert_eq!(map.len(), 3);
map.write().await.remove(&0xa);
assert_eq!(map.bytes(), 31);
assert_eq!(map.len(), 2);
assert_eq!(map.read().await.len(), 2);
}
#[tokio::test]
async fn insert_same_hash() {
let map = SizeTrackedStore::default();
let i0 = TestItem::new(1, 0, 10);
let i1 = TestItem::new(2, 0, 11);
let i2 = TestItem::new(3, 0, 20);
let i3 = TestItem::new(4, 0, 40);
let i4 = TestItem::new(5, 0, 50);
let i5 = TestItem::new(6, 0, 1);
map.write().await.insert(0xa, i0);
map.write().await.insert(0xb, i1.clone());
map.write().await.insert(0xc, i2);
assert_eq!(map.bytes(), 41);
assert_eq!(map.len(), 3);
assert_eq!(map.read().await.len(), 3);
map.write().await.insert(0xc, i3.clone());
assert_eq!(map.bytes(), 61);
assert_eq!(map.len(), 3);
assert_eq!(map.read().await.len(), 3);
map.write().await.insert(0xa, i4);
assert_eq!(map.bytes(), 101);
assert_eq!(map.len(), 3);
assert_eq!(map.read().await.len(), 3);
map.write().await.insert(0xa, i5.clone());
assert_eq!(map.bytes(), 52);
assert_eq!(map.len(), 3);
assert_eq!(map.read().await.len(), 3);
let items = map.read().await.values().cloned().collect::<Vec<_>>();
let expected = [i1, i3, i5];
assert!(expected.iter().all(|e| items.contains(e)),);
assert_eq!(items.len(), expected.len());
}
#[tokio::test]
async fn remove_non_existent() {
let map = SizeTrackedStore::<_, _, TestItem>::default();
map.write().await.remove(&0xa);
assert_eq!(map.bytes(), 0);
assert_eq!(map.len(), 0);
assert_eq!(map.read().await.len(), 0);
}
#[rstest]
#[case(20, 30, 50, 50, 100, 3, 100, 2, 100)]
#[case(2, 46, 50, 3, 100, 3, 98, 3, 99)]
#[case(2, 46, 50, 2, 100, 3, 98, 3, 98)]
#[case(2, 47, 50, 4, 100, 3, 99, 2, 54)]
#[case(1, 47, 50, 99, 100, 3, 98, 1, 99)]
#[case(1, 1, 1, 2, 100, 3, 3, 3, 4)] //always remove
#[case(20, 30, 40, 150, 100, 3, 90, 3, 90)]
#[case(10, 20, 30, 80, 100, 3, 60, 1, 80)]
#[tokio::test]
#[allow(clippy::too_many_arguments)]
async fn try_insert_with_replacement_works_param2(
#[case] i0_bytes: usize,
#[case] i1_bytes: usize,
#[case] i2_bytes: usize,
#[case] i3_bytes: usize,
#[case] max_bytes: usize,
#[case] expected_len_before: usize,
#[case] expected_bytes_before: usize,
#[case] expected_len_after: usize,
#[case] expected_bytes_after: usize,
) {
let map = SizeTrackedStore::default();
let i0 = TestItem::new(1, 0, i0_bytes);
let i1 = TestItem::new(2, 0, i1_bytes);
let i2 = TestItem::new(3, 0, i2_bytes);
map.write().await.insert(0xa, i0);
map.write().await.insert(0xb, i1);
map.write().await.insert(0xc, i2.clone());
assert_eq!(map.bytes(), expected_bytes_before);
assert_eq!(map.len(), expected_len_before);
let i3 = TestItem::new(4, 0, i3_bytes);
map.write().await.try_insert_with_replacement(max_bytes, 0xd, i3.clone());
assert_eq!(map.len(), expected_len_after);
assert_eq!(map.read().await.len(), expected_len_after);
assert_eq!(map.bytes(), expected_bytes_after);
}
#[tokio::test]
async fn try_insert_with_replacement_items() {
let map = SizeTrackedStore::default();
let i0 = TestItem::new(1, 0, 20);
let i1 = TestItem::new(2, 0, 30);
let i2 = TestItem::new(3, 0, 50);
map.write().await.insert(0xa, i0);
map.write().await.insert(0xb, i1);
map.write().await.insert(0xc, i2.clone());
assert_eq!(map.bytes(), 100);
assert_eq!(map.len(), 3);
let i3 = TestItem::new(4, 0, 50);
let _ = map.write().await.try_insert_with_replacement(100, 0xd, i3.clone());
assert_eq!(map.bytes(), 100);
assert_eq!(map.len(), 2);
assert_eq!(map.read().await.len(), 2);
let items = map.read().await.values().cloned().collect::<Vec<_>>();
let expected = [&i2, &i3];
assert!(expected.iter().all(|e| items.contains(e)),);
assert_eq!(items.len(), expected.len());
assert_eq!(map.write().await.pop_first().unwrap(), i2);
assert_eq!(map.write().await.pop_first().unwrap(), i3);
}
#[tokio::test]
async fn try_insert_with_replacement_works_known_key_reject() {
let map = SizeTrackedStore::default();
let i0 = TestItem::new(1, 0, 20);
let i1 = TestItem::new(2, 0, 30);
let i2 = TestItem::new(3, 0, 50);
map.write().await.insert(0xa, i0.clone());
map.write().await.insert(0xb, i1.clone());
map.write().await.insert(0xc, i2.clone());
assert_eq!(map.bytes(), 100);
assert_eq!(map.len(), 3);
let i3 = TestItem::new(4, 0, 50);
let r = map.write().await.try_insert_with_replacement(100, 0xb, i3);
assert!(r.is_none());
assert_eq!(map.bytes(), 100);
assert_eq!(map.len(), 3);
assert_eq!(map.read().await.len(), 3);
let items = map.read().await.values().cloned().collect::<Vec<_>>();
let expected = [&i0, &i1, &i2];
assert!(expected.iter().all(|e| items.contains(e)),);
assert_eq!(items.len(), expected.len());
assert_eq!(map.write().await.pop_first().unwrap(), i0);
assert_eq!(map.write().await.pop_first().unwrap(), i1);
assert_eq!(map.write().await.pop_first().unwrap(), i2);
}
#[tokio::test]
async fn try_insert_with_replacement_zero_size_reject() {
let map = SizeTrackedStore::default();
let i0 = TestItem::new(1, 0, 20);
let i1 = TestItem::new(2, 0, 30);
let i2 = TestItem::new(3, 0, 50);
map.write().await.insert(0xa, i0.clone());
map.write().await.insert(0xb, i1.clone());
map.write().await.insert(0xc, i2.clone());
assert_eq!(map.bytes(), 100);
assert_eq!(map.len(), 3);
let i3 = TestItem::new(4, 0, 0);
let r = map.write().await.try_insert_with_replacement(100, 0xb, i3);
assert!(r.is_none());
assert_eq!(map.bytes(), 100);
assert_eq!(map.len(), 3);
assert_eq!(map.read().await.len(), 3);
let items = map.read().await.values().cloned().collect::<Vec<_>>();
let expected = [i0, i1, i2];
assert!(expected.iter().all(|e| items.contains(e)),);
assert_eq!(items.len(), expected.len());
}
#[tokio::test]
async fn sorting_works() {
let map = SizeTrackedStore::default();
let i0 = TestItem::new(1, 0, 10);
let i1 = TestItem::new(1, 1, 10);
let i2 = TestItem::new(2, 0, 20);
let i3 = TestItem::new(3, 0, 30);
let i4 = TestItem::new(4, 0, 40);
map.write().await.insert(0xc, i2.clone());
map.write().await.insert(0xb, i1.clone());
map.write().await.insert(0xa, i0.clone());
map.write().await.insert(0xe, i4.clone());
map.write().await.insert(0xd, i3.clone());
assert_eq!(map.bytes(), 110);
assert_eq!(map.len(), 5);
assert_eq!(map.write().await.pop_first().unwrap(), i0);
assert_eq!(map.write().await.pop_first().unwrap(), i1);
assert_eq!(map.write().await.pop_first().unwrap(), i2);
assert_eq!(map.write().await.pop_first().unwrap(), i3);
assert_eq!(map.write().await.pop_first().unwrap(), i4);
}
#[tokio::test]
async fn update_item() {
let map = SizeTrackedStore::default();
let i0 = TestItem::new(1, 0, 20);
let i1 = TestItem::new(2, 0, 30);
let iu = TestItem::new(0, 0, 30);
let i2 = TestItem::new(3, 0, 50);
map.write().await.insert(0xa, i0.clone());
map.write().await.insert(0xb, i1.clone());
map.write().await.insert(0xc, i2.clone());
assert_eq!(map.bytes(), 100);
assert_eq!(map.len(), 3);
assert_eq!(map.read().await.len(), 3);
map.write().await.update_item(&0xb, |item| item.prio = iu.prio).unwrap();
assert_eq!(map.bytes(), 100);
assert_eq!(map.len(), 3);
assert_eq!(map.read().await.len(), 3);
let items = map.read().await.values().cloned().collect::<Vec<_>>();
let expected = [&i0, &iu, &i2];
assert!(expected.iter().all(|e| items.contains(e)),);
assert_eq!(items.len(), expected.len());
assert_eq!(map.write().await.pop_first().unwrap(), iu);
assert_eq!(map.write().await.pop_first().unwrap(), i0);
assert_eq!(map.write().await.pop_first().unwrap(), i2);
}
}
@@ -0,0 +1,682 @@
// 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 view.
//!
//! The View represents the state of the transaction pool at given block. The view is created when
//! new block is notified to transaction pool. Views are removed on finalization.
//!
//! Refer to [*View*](../index.html#view) section for more details.
use super::metrics::MetricsLink as PrometheusMetrics;
use crate::{
common::tracing_log_xt::log_xt_trace,
graph::{
self, base_pool::TimedTransactionSource, BlockHash, ExtrinsicFor, ExtrinsicHash,
IsValidator, TransactionFor, ValidateTransactionPriority, ValidatedPoolSubmitOutcome,
ValidatedTransaction, ValidatedTransactionFor,
},
LOG_TARGET,
};
use indexmap::IndexMap;
use parking_lot::Mutex;
use pezsc_transaction_pool_api::{error::Error as TxPoolError, PoolStatus, TransactionStatus};
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
use pezsp_blockchain::HashAndNumber;
use pezsp_runtime::{
generic::BlockId, traits::Block as BlockT, transaction_validity::TransactionValidityError,
SaturatedConversion,
};
use std::{sync::Arc, time::Instant};
use tracing::{debug, instrument, trace, Level};
pub(super) struct RevalidationResult<ChainApi: graph::ChainApi> {
revalidated: IndexMap<ExtrinsicHash<ChainApi>, ValidatedTransactionFor<ChainApi>>,
invalid_hashes: Vec<ExtrinsicHash<ChainApi>>,
}
/// Used to obtain result from RevalidationWorker on View side.
pub(super) type RevalidationResultReceiver<ChainApi> =
tokio::sync::mpsc::Receiver<RevalidationResult<ChainApi>>;
/// Used to send revalidation result from RevalidationWorker to View.
pub(super) type RevalidationResultSender<ChainApi> =
tokio::sync::mpsc::Sender<RevalidationResult<ChainApi>>;
/// Used to receive finish-revalidation-request from View on RevalidationWorker side.
pub(super) type FinishRevalidationRequestReceiver = tokio::sync::mpsc::Receiver<()>;
/// Used to send finish-revalidation-request from View to RevalidationWorker.
pub(super) type FinishRevalidationRequestSender = tokio::sync::mpsc::Sender<()>;
/// Endpoints of channels used on View side (maintain thread)
pub(super) struct FinishRevalidationLocalChannels<ChainApi: graph::ChainApi> {
/// Used to send finish revalidation request.
finish_revalidation_request_tx: Option<FinishRevalidationRequestSender>,
/// Used to receive revalidation results.
revalidation_result_rx: RevalidationResultReceiver<ChainApi>,
}
impl<ChainApi: graph::ChainApi> FinishRevalidationLocalChannels<ChainApi> {
/// Creates a new instance of endpoints for channels used on View side
pub fn new(
finish_revalidation_request_tx: FinishRevalidationRequestSender,
revalidation_result_rx: RevalidationResultReceiver<ChainApi>,
) -> Self {
Self {
finish_revalidation_request_tx: Some(finish_revalidation_request_tx),
revalidation_result_rx,
}
}
/// Removes a finish revalidation sender
///
/// Should be called when revalidation was already terminated and finish revalidation message is
/// no longer expected.
fn remove_sender(&mut self) {
self.finish_revalidation_request_tx = None;
}
}
/// Endpoints of channels used on `RevalidationWorker` side (background thread)
pub(super) struct FinishRevalidationWorkerChannels<ChainApi: graph::ChainApi> {
/// Used to receive finish revalidation request.
finish_revalidation_request_rx: FinishRevalidationRequestReceiver,
/// Used to send revalidation results.
revalidation_result_tx: RevalidationResultSender<ChainApi>,
}
impl<ChainApi: graph::ChainApi> FinishRevalidationWorkerChannels<ChainApi> {
/// Creates a new instance of endpoints for channels used on `RevalidationWorker` side
pub fn new(
finish_revalidation_request_rx: FinishRevalidationRequestReceiver,
revalidation_result_tx: RevalidationResultSender<ChainApi>,
) -> Self {
Self { finish_revalidation_request_rx, revalidation_result_tx }
}
}
/// Single event used in aggregated stream. Tuple containing hash of transactions and its status.
pub(super) type TransactionStatusEvent<H, BH> = (H, TransactionStatus<H, BH>);
/// Warning threshold for (unbounded) channel used in aggregated view's streams.
const VIEW_STREAM_WARN_THRESHOLD: usize = 100_000;
/// Stream of events providing statuses of all the transactions within the pool.
pub(super) type AggregatedStream<H, BH> = TracingUnboundedReceiver<TransactionStatusEvent<H, BH>>;
/// Type alias for a stream of events intended to track dropped transactions.
type DroppedMonitoringStream<H, BH> = TracingUnboundedReceiver<TransactionStatusEvent<H, BH>>;
/// Notification handler for transactions updates triggered in `ValidatedPool`.
///
/// `ViewPoolObserver` handles transaction status changes notifications coming from an instance of
/// validated pool associated with the `View` and forwards them through specified channels
/// into the View's streams.
pub(super) struct ViewPoolObserver<ChainApi: graph::ChainApi> {
/// The sink used to notify dropped by enforcing limits or by being usurped, or invalid
/// transactions.
///
/// Note: Ready and future statuses are alse communicated through this channel, enabling the
/// stream consumer to track views that reference the transaction.
dropped_stream_sink: TracingUnboundedSender<
TransactionStatusEvent<ExtrinsicHash<ChainApi>, BlockHash<ChainApi>>,
>,
/// The sink of the single, merged stream providing updates for all the transactions in the
/// associated pool.
///
/// Note: some of the events which are currently ignored on the other side of this channel
/// (external watcher) are not relayed.
aggregated_stream_sink: TracingUnboundedSender<
TransactionStatusEvent<ExtrinsicHash<ChainApi>, BlockHash<ChainApi>>,
>,
}
impl<C: graph::ChainApi> graph::EventHandler<C> for ViewPoolObserver<C> {
// note: skipped, notified by ForkAwareTxPool directly to multi view listener.
fn broadcasted(&self, _: ExtrinsicHash<C>, _: Vec<String>) {}
fn dropped(&self, _: ExtrinsicHash<C>) {}
fn finalized(&self, _: ExtrinsicHash<C>, _: BlockHash<C>, _: usize) {}
fn retracted(&self, _: ExtrinsicHash<C>, _: BlockHash<C>) {
// note: [#5479], we do not send to aggregated stream.
}
fn ready(&self, tx: ExtrinsicHash<C>) {
let status = TransactionStatus::Ready;
self.send_to_dropped_stream_sink(tx, status.clone());
self.send_to_aggregated_stream_sink(tx, status);
}
fn future(&self, tx: ExtrinsicHash<C>) {
let status = TransactionStatus::Future;
self.send_to_dropped_stream_sink(tx, status.clone());
self.send_to_aggregated_stream_sink(tx, status);
}
fn limits_enforced(&self, tx: ExtrinsicHash<C>) {
self.send_to_dropped_stream_sink(tx, TransactionStatus::Dropped);
}
fn usurped(&self, tx: ExtrinsicHash<C>, by: ExtrinsicHash<C>) {
self.send_to_dropped_stream_sink(tx, TransactionStatus::Usurped(by));
}
fn invalid(&self, tx: ExtrinsicHash<C>) {
self.send_to_dropped_stream_sink(tx, TransactionStatus::Invalid);
}
fn pruned(&self, tx: ExtrinsicHash<C>, block_hash: BlockHash<C>, tx_index: usize) {
self.send_to_aggregated_stream_sink(tx, TransactionStatus::InBlock((block_hash, tx_index)));
}
fn finality_timeout(&self, tx: ExtrinsicHash<C>, hash: BlockHash<C>) {
//todo: do we need this? [related issue: #5482]
self.send_to_aggregated_stream_sink(tx, TransactionStatus::FinalityTimeout(hash));
}
}
impl<ChainApi: graph::ChainApi> ViewPoolObserver<ChainApi> {
/// Creates an instance of `ViewPoolObserver` together with associated view's streams.
///
/// This methods creates an event handler that shall be registered in the `ValidatedPool`
/// instance associated with the view. It also creates new view's streams:
/// - a single stream intended to watch dropped transactions only. The stream can be used to
/// subscribe to events related to dropping of all extrinsics in the pool.
/// - a single merged stream for all extrinsics in the associated pool. The stream can be used
/// to subscribe to life-cycle events of all extrinsics in the pool. For fork-aware
/// pool implementation this approach seems to be more efficient than using individual
/// streams for every transaction.
fn new() -> (
Self,
DroppedMonitoringStream<ExtrinsicHash<ChainApi>, BlockHash<ChainApi>>,
AggregatedStream<ExtrinsicHash<ChainApi>, BlockHash<ChainApi>>,
) {
let (dropped_stream_sink, dropped_stream) =
tracing_unbounded("mpsc_txpool_watcher", VIEW_STREAM_WARN_THRESHOLD);
let (aggregated_stream_sink, aggregated_stream) =
tracing_unbounded("mpsc_txpool_aggregated_stream", VIEW_STREAM_WARN_THRESHOLD);
(Self { dropped_stream_sink, aggregated_stream_sink }, dropped_stream, aggregated_stream)
}
/// Sends given event to the `dropped_stream_sink`.
fn send_to_dropped_stream_sink(
&self,
tx: ExtrinsicHash<ChainApi>,
status: TransactionStatus<ExtrinsicHash<ChainApi>, BlockHash<ChainApi>>,
) {
if let Err(e) = self.dropped_stream_sink.unbounded_send((tx, status.clone())) {
trace!(target: LOG_TARGET, "[{:?}] dropped_sink: {:?} send message failed: {:?}", tx, status, e);
}
}
/// Sends given event to the `aggregated_stream_sink`.
fn send_to_aggregated_stream_sink(
&self,
tx: ExtrinsicHash<ChainApi>,
status: TransactionStatus<ExtrinsicHash<ChainApi>, BlockHash<ChainApi>>,
) {
if let Err(e) = self.aggregated_stream_sink.unbounded_send((tx, status.clone())) {
trace!(target: LOG_TARGET, "[{:?}] aggregated_stream {:?} send message failed: {:?}", tx, status, e);
}
}
}
/// Represents the state of transaction pool for given block.
///
/// Refer to [*View*](../index.html#view) section for more details on the purpose and life cycle of
/// the `View`.
pub(super) struct View<ChainApi: graph::ChainApi> {
/// The internal pool keeping the set of ready and future transaction at the given block.
pub(super) pool: graph::Pool<ChainApi, ViewPoolObserver<ChainApi>>,
/// The hash and number of the block with which this view is associated.
pub(super) at: HashAndNumber<ChainApi::Block>,
/// Endpoints of communication channel with background worker.
revalidation_worker_channels: Mutex<Option<FinishRevalidationLocalChannels<ChainApi>>>,
/// Prometheus's metrics endpoint.
metrics: PrometheusMetrics,
}
impl<ChainApi> View<ChainApi>
where
ChainApi: graph::ChainApi,
<ChainApi::Block as BlockT>::Hash: Unpin,
{
/// Creates a new empty view.
pub(super) fn new(
api: Arc<ChainApi>,
at: HashAndNumber<ChainApi::Block>,
options: graph::Options,
metrics: PrometheusMetrics,
is_validator: IsValidator,
) -> (
Self,
DroppedMonitoringStream<ExtrinsicHash<ChainApi>, BlockHash<ChainApi>>,
AggregatedStream<ExtrinsicHash<ChainApi>, BlockHash<ChainApi>>,
) {
metrics.report(|metrics| metrics.non_cloned_views.inc());
let (event_handler, dropped_stream, aggregated_stream) = ViewPoolObserver::new();
(
Self {
pool: graph::Pool::new_with_event_handler(
options,
is_validator,
api,
event_handler,
),
at,
revalidation_worker_channels: Mutex::from(None),
metrics,
},
dropped_stream,
aggregated_stream,
)
}
/// Creates a copy of the other view.
pub(super) fn new_from_other(
&self,
at: &HashAndNumber<ChainApi::Block>,
) -> (
Self,
DroppedMonitoringStream<ExtrinsicHash<ChainApi>, BlockHash<ChainApi>>,
AggregatedStream<ExtrinsicHash<ChainApi>, BlockHash<ChainApi>>,
) {
let (event_handler, dropped_stream, aggregated_stream) = ViewPoolObserver::new();
(
View {
at: at.clone(),
pool: self.pool.deep_clone_with_event_handler(event_handler),
revalidation_worker_channels: Mutex::from(None),
metrics: self.metrics.clone(),
},
dropped_stream,
aggregated_stream,
)
}
/// Imports single unvalidated extrinsic into the view.
#[instrument(level = Level::TRACE, skip_all, target = "txpool", name = "view::submit_one")]
pub(super) async fn submit_one(
&self,
source: TimedTransactionSource,
xt: ExtrinsicFor<ChainApi>,
validation_priority: ValidateTransactionPriority,
) -> Result<ValidatedPoolSubmitOutcome<ChainApi>, ChainApi::Error> {
self.submit_many(std::iter::once((source, xt)), validation_priority)
.await
.pop()
.expect("There is exactly one result, qed.")
}
/// Imports many unvalidated extrinsics into the view.
#[instrument(level = Level::TRACE, skip_all, target = "txpool", name = "view::submit_many")]
pub(super) async fn submit_many(
&self,
xts: impl IntoIterator<Item = (TimedTransactionSource, ExtrinsicFor<ChainApi>)>,
validation_priority: ValidateTransactionPriority,
) -> Vec<Result<ValidatedPoolSubmitOutcome<ChainApi>, ChainApi::Error>> {
if tracing::enabled!(target: LOG_TARGET, tracing::Level::TRACE) {
let xts = xts.into_iter().collect::<Vec<_>>();
log_xt_trace!(
target: LOG_TARGET,
xts.iter().map(|(_,xt)| self.pool.validated_pool().api().hash_and_length(xt).0),
"view::submit_many at:{}",
self.at.hash
);
self.pool.submit_at(&self.at, xts, validation_priority).await
} else {
self.pool.submit_at(&self.at, xts, validation_priority).await
}
}
/// Synchronously imports single unvalidated extrinsics into the view.
pub(super) fn submit_local(
&self,
xt: ExtrinsicFor<ChainApi>,
) -> Result<ValidatedPoolSubmitOutcome<ChainApi>, ChainApi::Error> {
let (tx_hash, length) = self.pool.validated_pool().api().hash_and_length(&xt);
trace!(
target: LOG_TARGET,
?tx_hash,
view_at_hash = ?self.at.hash,
"view::submit_local"
);
let validity = self
.pool
.validated_pool()
.api()
.validate_transaction_blocking(
self.at.hash,
pezsc_transaction_pool_api::TransactionSource::Local,
Arc::from(xt.clone()),
)?
.map_err(|e| {
match e {
TransactionValidityError::Invalid(i) => TxPoolError::InvalidTransaction(i),
TransactionValidityError::Unknown(u) => TxPoolError::UnknownTransaction(u),
}
.into()
})?;
let block_number = self
.pool
.validated_pool()
.api()
.block_id_to_number(&BlockId::hash(self.at.hash))?
.ok_or_else(|| TxPoolError::InvalidBlockId(format!("{:?}", self.at.hash)))?;
let validated = ValidatedTransaction::valid_at(
block_number.saturated_into::<u64>(),
tx_hash,
TimedTransactionSource::new_local(true),
Arc::from(xt),
length,
validity,
);
self.pool.validated_pool().submit(vec![validated]).remove(0)
}
/// Status of the pool associated with the view.
pub(super) fn status(&self) -> PoolStatus {
self.pool.validated_pool().status()
}
/// Revalidates some part of transaction from the internal pool.
///
/// Intended to be called from the revalidation worker. The revalidation process can be
/// terminated by sending a message to the `rx` channel provided within
/// `finish_revalidation_worker_channels`. Revalidation results are sent back over the `tx`
/// channels and shall be applied in maintain thread.
///
/// View revalidation currently is not throttled, and until not terminated it will revalidate
/// all the transactions. Note: this can be improved if CPU usage due to revalidation becomes a
/// problem.
pub(super) async fn revalidate(
&self,
finish_revalidation_worker_channels: FinishRevalidationWorkerChannels<ChainApi>,
) {
let FinishRevalidationWorkerChannels {
mut finish_revalidation_request_rx,
revalidation_result_tx,
} = finish_revalidation_worker_channels;
debug!(
target: LOG_TARGET,
at_hash = ?self.at.hash,
"view::revalidate: at starting"
);
let start = Instant::now();
let validated_pool = self.pool.validated_pool();
let api = validated_pool.api();
let batch: Vec<_> = validated_pool.ready().collect();
let batch_len = batch.len();
//todo: sort batch by revalidation timestamp | maybe not needed at all? xts will be getting
//out of the view...
//todo: revalidate future, remove if invalid [#5496]
let mut invalid_hashes = Vec::new();
let mut revalidated = IndexMap::new();
let mut validation_results = vec![];
let mut batch_iter = batch.into_iter();
loop {
let mut should_break = false;
tokio::select! {
_ = finish_revalidation_request_rx.recv() => {
debug!(
target: LOG_TARGET,
at_hash = ?self.at.hash,
"view::revalidate: finish revalidation request received"
);
break
}
_ = async {
if let Some(tx) = batch_iter.next() {
let validation_result = (
api.validate_transaction(self.at.hash,
tx.source.clone().into(), tx.data.clone(),
ValidateTransactionPriority::Maintained).await,
tx.hash,
tx
);
validation_results.push(validation_result);
} else {
self.revalidation_worker_channels.lock().as_mut().map(|ch| ch.remove_sender());
should_break = true;
}
} => {}
}
if should_break {
break;
}
}
let revalidation_duration = start.elapsed();
self.metrics.report(|metrics| {
metrics.view_revalidation_duration.observe(revalidation_duration.as_secs_f64());
});
debug!(
target: LOG_TARGET,
at_hash = ?self.at.hash,
count = validation_results.len(),
batch_len,
duration = ?revalidation_duration,
"view::revalidate"
);
log_xt_trace!(
data:tuple,
target:LOG_TARGET,
validation_results.iter().map(|x| (x.1, &x.0)),
"view::revalidate result: {:?}"
);
for (validation_result, tx_hash, tx) in validation_results {
match validation_result {
Ok(Err(TransactionValidityError::Invalid(_))) => {
invalid_hashes.push(tx_hash);
},
Ok(Ok(validity)) => {
revalidated.insert(
tx_hash,
ValidatedTransaction::valid_at(
self.at.number.saturated_into::<u64>(),
tx_hash,
tx.source.clone(),
tx.data.clone(),
api.hash_and_length(&tx.data).1,
validity,
),
);
},
Ok(Err(TransactionValidityError::Unknown(error))) => {
trace!(
target: LOG_TARGET,
?tx_hash,
?error,
"Removing. Cannot determine transaction validity"
);
invalid_hashes.push(tx_hash);
},
Err(error) => {
trace!(
target: LOG_TARGET,
?tx_hash,
%error,
"Removing due to error during revalidation"
);
invalid_hashes.push(tx_hash);
},
}
}
debug!(
target: LOG_TARGET,
at_hash = ?self.at.hash,
"view::revalidate: sending revalidation result"
);
if let Err(error) = revalidation_result_tx
.send(RevalidationResult { invalid_hashes, revalidated })
.await
{
trace!(
target: LOG_TARGET,
at_hash = ?self.at.hash,
?error,
"view::revalidate: sending revalidation_result failed"
);
}
}
/// Sends revalidation request to the background worker.
///
/// Creates communication channels required to stop revalidation request and receive the
/// revalidation results and sends the revalidation request to the background worker.
///
/// Intended to be called from maintain thread, at the very end of the maintain process.
///
/// Refer to [*View revalidation*](../index.html#view-revalidation) for more details.
pub(super) async fn start_background_revalidation(
view: Arc<Self>,
revalidation_queue: Arc<
super::revalidation_worker::RevalidationQueue<ChainApi, ChainApi::Block>,
>,
) {
debug!(
target: LOG_TARGET,
at_hash = ?view.at.hash,
"view::start_background_revalidation"
);
let (finish_revalidation_request_tx, finish_revalidation_request_rx) =
tokio::sync::mpsc::channel(1);
let (revalidation_result_tx, revalidation_result_rx) = tokio::sync::mpsc::channel(1);
let finish_revalidation_worker_channels = FinishRevalidationWorkerChannels::new(
finish_revalidation_request_rx,
revalidation_result_tx,
);
let finish_revalidation_local_channels = FinishRevalidationLocalChannels::new(
finish_revalidation_request_tx,
revalidation_result_rx,
);
*view.revalidation_worker_channels.lock() = Some(finish_revalidation_local_channels);
revalidation_queue
.revalidate_view(view.clone(), finish_revalidation_worker_channels)
.await;
}
/// Terminates a background view revalidation.
///
/// Receives the results from the background worker and applies them to the internal pool.
/// Intended to be called from the maintain thread, at the very beginning of the maintain
/// process, before the new view is cloned and updated. Applying results before cloning ensures
/// that view contains up-to-date set of revalidated transactions.
///
/// Refer to [*View revalidation*](../index.html#view-revalidation) for more details.
pub(super) async fn finish_revalidation(&self) {
trace!(
target: LOG_TARGET,
at_hash = ?self.at.hash,
"view::finish_revalidation"
);
let Some(revalidation_worker_channels) = self.revalidation_worker_channels.lock().take()
else {
trace!(target:LOG_TARGET, "view::finish_revalidation: no finish_revalidation_request_tx");
return;
};
let FinishRevalidationLocalChannels {
finish_revalidation_request_tx,
mut revalidation_result_rx,
} = revalidation_worker_channels;
if let Some(finish_revalidation_request_tx) = finish_revalidation_request_tx {
if let Err(error) = finish_revalidation_request_tx.send(()).await {
trace!(
target: LOG_TARGET,
at_hash = ?self.at.hash,
%error,
"view::finish_revalidation: sending cancellation request failed"
);
}
}
if let Some(revalidation_result) = revalidation_result_rx.recv().await {
let start = Instant::now();
let revalidated_len = revalidation_result.revalidated.len();
let validated_pool = self.pool.validated_pool();
validated_pool.remove_invalid(&revalidation_result.invalid_hashes);
if revalidated_len > 0 {
self.pool.resubmit(revalidation_result.revalidated);
}
self.metrics.report(|metrics| {
let _ = (
revalidation_result
.invalid_hashes
.len()
.try_into()
.map(|v| metrics.view_revalidation_invalid_txs.inc_by(v)),
revalidated_len
.try_into()
.map(|v| metrics.view_revalidation_resubmitted_txs.inc_by(v)),
);
});
debug!(
target: LOG_TARGET,
invalid = revalidation_result.invalid_hashes.len(),
revalidated = revalidated_len,
at_hash = ?self.at.hash,
duration = ?start.elapsed(),
"view::finish_revalidation: applying revalidation result"
);
}
}
/// Returns true if the transaction with given hash is already imported into the view.
pub(super) fn is_imported(&self, tx_hash: &ExtrinsicHash<ChainApi>) -> bool {
const IGNORE_BANNED: bool = false;
self.pool.validated_pool().check_is_known(tx_hash, IGNORE_BANNED).is_err()
}
/// Removes the whole transaction subtree from the inner pool.
///
/// Refer to [`crate::graph::ValidatedPool::remove_subtree`] for more details.
pub fn remove_subtree<F>(
&self,
hashes: &[ExtrinsicHash<ChainApi>],
ban_transactions: bool,
listener_action: F,
) -> Vec<TransactionFor<ChainApi>>
where
F: Fn(
&mut crate::graph::EventDispatcher<ChainApi, ViewPoolObserver<ChainApi>>,
ExtrinsicHash<ChainApi>,
),
{
self.pool
.validated_pool()
.remove_subtree(hashes, ban_transactions, listener_action)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,276 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::{
collections::{HashMap, HashSet},
fmt, hash,
sync::Arc,
};
use pezsp_core::hexdisplay::HexDisplay;
use pezsp_runtime::transaction_validity::TransactionTag as Tag;
use std::time::Instant;
use super::base_pool::Transaction;
use crate::{common::tracing_log_xt::log_xt_trace, LOG_TARGET};
/// Transaction with partially satisfied dependencies.
pub struct WaitingTransaction<Hash, Ex> {
/// Transaction details.
pub transaction: Arc<Transaction<Hash, Ex>>,
/// Tags that are required and have not been satisfied yet by other transactions in the pool.
pub missing_tags: HashSet<Tag>,
/// Time of import to the Future Queue.
pub imported_at: Instant,
}
impl<Hash: fmt::Debug, Ex: fmt::Debug> fmt::Debug for WaitingTransaction<Hash, Ex> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "WaitingTransaction {{ ")?;
write!(fmt, "imported_at: {:?}, ", self.imported_at)?;
write!(fmt, "transaction: {:?}, ", self.transaction)?;
write!(
fmt,
"missing_tags: {{{}}}",
self.missing_tags
.iter()
.map(|tag| HexDisplay::from(tag).to_string())
.collect::<Vec<_>>()
.join(", "),
)?;
write!(fmt, "}}")
}
}
impl<Hash, Ex> Clone for WaitingTransaction<Hash, Ex> {
fn clone(&self) -> Self {
Self {
transaction: self.transaction.clone(),
missing_tags: self.missing_tags.clone(),
imported_at: self.imported_at,
}
}
}
impl<Hash, Ex> WaitingTransaction<Hash, Ex> {
/// Creates a new `WaitingTransaction`.
///
/// Computes the set of missing tags based on the requirements and tags that
/// are provided by all transactions in the ready queue.
pub fn new(
transaction: Transaction<Hash, Ex>,
provided: &HashMap<Tag, Hash>,
recently_pruned: &[HashSet<Tag>],
) -> Self {
let missing_tags = transaction
.requires
.iter()
.filter(|tag| {
// is true if the tag is already satisfied either via transaction in the pool
// or one that was recently included.
let is_provided = provided.contains_key(&**tag) ||
recently_pruned.iter().any(|x| x.contains(&**tag));
!is_provided
})
.cloned()
.collect();
Self { transaction: Arc::new(transaction), missing_tags, imported_at: Instant::now() }
}
/// Marks the tag as satisfied.
pub fn satisfy_tag(&mut self, tag: &Tag) {
self.missing_tags.remove(tag);
}
/// Returns true if transaction has all requirements satisfied.
pub fn is_ready(&self) -> bool {
self.missing_tags.is_empty()
}
}
/// A pool of transactions that are not yet ready to be included in the block.
///
/// Contains transactions that are still awaiting some other transactions that
/// could provide a tag that they require.
#[derive(Clone, Debug)]
pub struct FutureTransactions<Hash: hash::Hash + Eq, Ex> {
/// tags that are not yet provided by any transaction, and we await for them
wanted_tags: HashMap<Tag, HashSet<Hash>>,
/// Transactions waiting for a particular other transaction
waiting: HashMap<Hash, WaitingTransaction<Hash, Ex>>,
}
impl<Hash: hash::Hash + Eq, Ex> Default for FutureTransactions<Hash, Ex> {
fn default() -> Self {
Self { wanted_tags: Default::default(), waiting: Default::default() }
}
}
const WAITING_PROOF: &str = r"#
In import we always insert to `waiting` if we push to `wanted_tags`;
when removing from `waiting` we always clear `wanted_tags`;
every hash from `wanted_tags` is always present in `waiting`;
qed
#";
impl<Hash: hash::Hash + Eq + Clone + std::fmt::Debug, Ex: std::fmt::Debug>
FutureTransactions<Hash, Ex>
{
/// Import transaction to Future queue.
///
/// Only transactions that don't have all their tags satisfied should occupy
/// the Future queue.
/// As soon as required tags are provided by some other transactions that are ready
/// we should remove the transactions from here and move them to the Ready queue.
pub fn import(&mut self, tx: WaitingTransaction<Hash, Ex>) {
assert!(!tx.is_ready(), "Transaction is ready.");
assert!(
!self.waiting.contains_key(&tx.transaction.hash),
"Transaction is already imported."
);
// Add all tags that are missing
for tag in &tx.missing_tags {
let entry = self.wanted_tags.entry(tag.clone()).or_insert_with(HashSet::new);
entry.insert(tx.transaction.hash.clone());
}
// Add the transaction to a by-hash waiting map
self.waiting.insert(tx.transaction.hash.clone(), tx);
}
/// Returns true if given hash is part of the queue.
pub fn contains(&self, hash: &Hash) -> bool {
self.waiting.contains_key(hash)
}
/// Returns a list of known transactions
pub fn by_hashes(&self, hashes: &[Hash]) -> Vec<Option<Arc<Transaction<Hash, Ex>>>> {
hashes
.iter()
.map(|h| self.waiting.get(h).map(|x| x.transaction.clone()))
.collect()
}
/// Removes transactions that provide any of tags in the given list.
///
/// Returns list of removed transactions.
pub fn prune_tags(&mut self, tags: &Vec<Tag>) -> Vec<Arc<Transaction<Hash, Ex>>> {
let pruned = self
.waiting
.values()
.filter_map(|tx| {
tx.transaction
.provides
.iter()
.any(|provided_tag| tags.contains(provided_tag))
.then(|| tx.transaction.hash.clone())
})
.collect::<Vec<_>>();
log_xt_trace!(target: LOG_TARGET, &pruned, "FutureTransactions: removed while pruning tags.");
self.remove(&pruned)
}
/// Satisfies provided tags in transactions that are waiting for them.
///
/// Returns (and removes) transactions that became ready after their last tag got
/// satisfied, and now we can remove them from Future and move to Ready queue.
pub fn satisfy_tags<T: AsRef<Tag>>(
&mut self,
tags: impl IntoIterator<Item = T>,
) -> Vec<WaitingTransaction<Hash, Ex>> {
let mut became_ready = vec![];
for tag in tags {
if let Some(hashes) = self.wanted_tags.remove(tag.as_ref()) {
for hash in hashes {
let is_ready = {
let tx = self.waiting.get_mut(&hash).expect(WAITING_PROOF);
tx.satisfy_tag(tag.as_ref());
tx.is_ready()
};
if is_ready {
let tx = self.waiting.remove(&hash).expect(WAITING_PROOF);
became_ready.push(tx);
}
}
}
}
became_ready
}
/// Removes transactions for given list of hashes.
///
/// Returns a list of actually removed transactions.
pub fn remove(&mut self, hashes: &[Hash]) -> Vec<Arc<Transaction<Hash, Ex>>> {
let mut removed = vec![];
for hash in hashes {
if let Some(waiting_tx) = self.waiting.remove(hash) {
// remove from wanted_tags as well
for tag in waiting_tx.missing_tags {
let remove = if let Some(wanted) = self.wanted_tags.get_mut(&tag) {
wanted.remove(hash);
wanted.is_empty()
} else {
false
};
if remove {
self.wanted_tags.remove(&tag);
}
}
// add to result
removed.push(waiting_tx.transaction)
}
}
removed
}
/// Fold a list of future transactions to compute a single value.
pub fn fold<R, F: FnMut(Option<R>, &WaitingTransaction<Hash, Ex>) -> Option<R>>(
&mut self,
f: F,
) -> Option<R> {
self.waiting.values().fold(None, f)
}
/// Returns iterator over all future transactions
pub fn all(&self) -> impl Iterator<Item = &Transaction<Hash, Ex>> {
self.waiting.values().map(|waiting| &*waiting.transaction)
}
/// Removes and returns all future transactions.
pub fn clear(&mut self) -> Vec<Arc<Transaction<Hash, Ex>>> {
self.wanted_tags.clear();
self.waiting.drain().map(|(_, tx)| tx.transaction).collect()
}
/// Returns number of transactions in the Future queue.
pub fn len(&self) -> usize {
self.waiting.len()
}
/// Returns sum of encoding lengths of all transactions in this queue.
pub fn bytes(&self) -> usize {
self.waiting.values().fold(0, |acc, tx| acc + tx.transaction.bytes)
}
}
@@ -0,0 +1,276 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::{collections::HashMap, fmt::Debug, hash};
use linked_hash_map::LinkedHashMap;
use tracing::trace;
use super::{watcher, BlockHash, ChainApi, ExtrinsicHash};
static LOG_TARGET: &str = "txpool::watcher";
/// The `EventHandler` trait provides a mechanism for clients to respond to various
/// transaction-related events. It offers a set of callback methods that are invoked by the
/// transaction pool's event dispatcher to notify about changes in the status of transactions.
///
/// This trait can be implemented by any component that needs to respond to transaction lifecycle
/// events, enabling custom logic and handling of these events.
pub trait EventHandler<C: ChainApi> {
/// Called when a transaction is broadcasted.
fn broadcasted(&self, _hash: ExtrinsicHash<C>, _peers: Vec<String>) {}
/// Called when a transaction is ready for execution.
fn ready(&self, _tx: ExtrinsicHash<C>) {}
/// Called when a transaction is deemed to be executable in the future.
fn future(&self, _tx: ExtrinsicHash<C>) {}
/// Called when transaction pool limits result in a transaction being affected.
fn limits_enforced(&self, _tx: ExtrinsicHash<C>) {}
/// Called when a transaction is replaced by another.
fn usurped(&self, _tx: ExtrinsicHash<C>, _by: ExtrinsicHash<C>) {}
/// Called when a transaction is dropped from the pool.
fn dropped(&self, _tx: ExtrinsicHash<C>) {}
/// Called when a transaction is found to be invalid.
fn invalid(&self, _tx: ExtrinsicHash<C>) {}
/// Called when a transaction was pruned from the pool due to its presence in imported block.
fn pruned(&self, _tx: ExtrinsicHash<C>, _block_hash: BlockHash<C>, _tx_index: usize) {}
/// Called when a transaction is retracted from inclusion in a block.
fn retracted(&self, _tx: ExtrinsicHash<C>, _block_hash: BlockHash<C>) {}
/// Called when a transaction has not been finalized within a timeout period.
fn finality_timeout(&self, _tx: ExtrinsicHash<C>, _hash: BlockHash<C>) {}
/// Called when a transaction is finalized in a block.
fn finalized(&self, _tx: ExtrinsicHash<C>, _block_hash: BlockHash<C>, _tx_index: usize) {}
}
impl<C: ChainApi> EventHandler<C> for () {}
/// The `EventDispatcher` struct is responsible for dispatching transaction-related events from the
/// validated pool to interested observers and an optional event handler. It acts as the primary
/// liaison between the transaction pool and clients that are monitoring transaction statuses.
pub struct EventDispatcher<H: hash::Hash + Eq, C: ChainApi, L: EventHandler<C>> {
/// Map containing per-transaction sinks for emitting transaction status events.
watchers: HashMap<H, watcher::Sender<H, BlockHash<C>>>,
finality_watchers: LinkedHashMap<ExtrinsicHash<C>, Vec<H>>,
/// Optional event handler (listener) that will be notified about all transactions status
/// changes from the pool.
event_handler: Option<L>,
}
/// Maximum number of blocks awaiting finality at any time.
const MAX_FINALITY_WATCHERS: usize = 512;
impl<H: hash::Hash + Eq + Debug, C: ChainApi, L: EventHandler<C>> Default
for EventDispatcher<H, C, L>
{
fn default() -> Self {
Self {
watchers: Default::default(),
finality_watchers: Default::default(),
event_handler: None,
}
}
}
impl<C: ChainApi, L: EventHandler<C>> EventDispatcher<ExtrinsicHash<C>, C, L> {
/// Creates a new instance with provided event handler.
pub fn new_with_event_handler(event_handler: Option<L>) -> Self {
Self { event_handler, ..Default::default() }
}
fn fire<F>(&mut self, hash: &ExtrinsicHash<C>, fun: F)
where
F: FnOnce(&mut watcher::Sender<ExtrinsicHash<C>, ExtrinsicHash<C>>),
{
let clean = if let Some(h) = self.watchers.get_mut(hash) {
fun(h);
h.is_done()
} else {
false
};
if clean {
self.watchers.remove(hash);
}
}
/// Creates a new watcher for given verified extrinsic.
///
/// The watcher can be used to subscribe to life-cycle events of that extrinsic.
pub fn create_watcher(
&mut self,
hash: ExtrinsicHash<C>,
) -> watcher::Watcher<ExtrinsicHash<C>, ExtrinsicHash<C>> {
let sender = self.watchers.entry(hash).or_insert_with(watcher::Sender::default);
sender.new_watcher(hash)
}
/// Notify the listeners about the extrinsic broadcast.
pub fn broadcasted(&mut self, tx_hash: &ExtrinsicHash<C>, peers: Vec<String>) {
trace!(
target: LOG_TARGET,
?tx_hash,
"Broadcasted."
);
self.fire(tx_hash, |watcher| watcher.broadcast(peers.clone()));
self.event_handler.as_ref().map(|l| l.broadcasted(*tx_hash, peers));
}
/// New transaction was added to the ready pool or promoted from the future pool.
pub fn ready(&mut self, tx: &ExtrinsicHash<C>, old: Option<&ExtrinsicHash<C>>) {
trace!(
target: LOG_TARGET,
tx_hash = ?*tx,
replaced_with = ?old,
"Ready."
);
self.fire(tx, |watcher| watcher.ready());
if let Some(old) = old {
self.fire(old, |watcher| watcher.usurped(*tx));
}
self.event_handler.as_ref().map(|l| l.ready(*tx));
}
/// New transaction was added to the future pool.
pub fn future(&mut self, tx_hash: &ExtrinsicHash<C>) {
trace!(
target: LOG_TARGET,
?tx_hash,
"Future."
);
self.fire(tx_hash, |watcher| watcher.future());
self.event_handler.as_ref().map(|l| l.future(*tx_hash));
}
/// Transaction was dropped from the pool because of enforcing the limit.
pub fn limits_enforced(&mut self, tx_hash: &ExtrinsicHash<C>) {
trace!(
target: LOG_TARGET,
?tx_hash,
"Dropped (limits enforced)."
);
self.fire(tx_hash, |watcher| watcher.limit_enforced());
self.event_handler.as_ref().map(|l| l.limits_enforced(*tx_hash));
}
/// Transaction was replaced with other extrinsic.
pub fn usurped(&mut self, tx: &ExtrinsicHash<C>, by: &ExtrinsicHash<C>) {
trace!(
target: LOG_TARGET,
tx_hash = ?tx,
?by,
"Dropped (replaced)."
);
self.fire(tx, |watcher| watcher.usurped(*by));
self.event_handler.as_ref().map(|l| l.usurped(*tx, *by));
}
/// Transaction was dropped from the pool because of the failure during the resubmission of
/// revalidate transactions or failure during pruning tags.
pub fn dropped(&mut self, tx_hash: &ExtrinsicHash<C>) {
trace!(
target: LOG_TARGET,
?tx_hash,
"Dropped."
);
self.fire(tx_hash, |watcher| watcher.dropped());
self.event_handler.as_ref().map(|l| l.dropped(*tx_hash));
}
/// Transaction was removed as invalid.
pub fn invalid(&mut self, tx_hash: &ExtrinsicHash<C>) {
trace!(
target: LOG_TARGET,
?tx_hash,
"Extrinsic invalid."
);
self.fire(tx_hash, |watcher| watcher.invalid());
self.event_handler.as_ref().map(|l| l.invalid(*tx_hash));
}
/// Transaction was pruned from the pool.
pub fn pruned(&mut self, block_hash: BlockHash<C>, tx_hash: &ExtrinsicHash<C>) {
trace!(
target: LOG_TARGET,
?tx_hash,
?block_hash,
"Pruned at."
);
// Get the transactions included in the given block hash.
let txs = self.finality_watchers.entry(block_hash).or_insert(vec![]);
txs.push(*tx_hash);
// Current transaction is the last one included.
let tx_index = txs.len() - 1;
self.fire(tx_hash, |watcher| watcher.in_block(block_hash, tx_index));
self.event_handler.as_ref().map(|l| l.pruned(*tx_hash, block_hash, tx_index));
while self.finality_watchers.len() > MAX_FINALITY_WATCHERS {
if let Some((hash, txs)) = self.finality_watchers.pop_front() {
for tx in txs {
self.fire(&tx, |watcher| watcher.finality_timeout(hash));
self.event_handler.as_ref().map(|l| l.finality_timeout(tx, block_hash));
}
}
}
}
/// The block this transaction was included in has been retracted.
pub fn retracted(&mut self, block_hash: BlockHash<C>) {
if let Some(hashes) = self.finality_watchers.remove(&block_hash) {
for hash in hashes {
self.fire(&hash, |watcher| watcher.retracted(block_hash));
self.event_handler.as_ref().map(|l| l.retracted(hash, block_hash));
}
}
}
/// Notify all watchers that transactions have been finalized
pub fn finalized(&mut self, block_hash: BlockHash<C>) {
if let Some(hashes) = self.finality_watchers.remove(&block_hash) {
for (tx_index, tx_hash) in hashes.into_iter().enumerate() {
trace!(
target: LOG_TARGET,
?tx_hash,
?block_hash,
"Sent finalization event."
);
self.fire(&tx_hash, |watcher| watcher.finalized(block_hash, tx_index));
self.event_handler.as_ref().map(|l| l.finalized(tx_hash, block_hash, tx_index));
}
}
}
/// Provides hashes of all watched transactions.
pub fn watched_transactions(&self) -> impl Iterator<Item = &ExtrinsicHash<C>> {
self.watchers.keys()
}
}
@@ -0,0 +1,53 @@
// 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/>.
//! Generic Transaction Pool
//!
//! The pool is based on dependency graph between transactions
//! and their priority.
//! The pool is able to return an iterator that traverses transaction
//! graph in the correct order taking into account priorities and dependencies.
#![warn(missing_docs)]
#![warn(unused_extern_crates)]
mod future;
mod listener;
mod pool;
mod ready;
mod rotator;
pub(crate) mod tracked_map;
mod validated_pool;
pub mod base_pool;
pub mod watcher;
pub use self::pool::{
BlockHash, ChainApi, ExtrinsicFor, ExtrinsicHash, NumberFor, Options, Pool, RawExtrinsicFor,
TransactionFor, ValidateTransactionPriority, ValidatedTransactionFor,
};
pub use validated_pool::{
BaseSubmitOutcome, EventDispatcher, IsValidator, ValidatedPoolSubmitOutcome,
ValidatedTransaction,
};
pub(crate) use self::pool::CheckBannedBeforeVerify;
pub(crate) use listener::EventHandler;
#[cfg(doc)]
pub(crate) use validated_pool::ValidatedPool;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,827 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::{
cmp,
collections::{BTreeSet, HashMap, HashSet},
hash,
sync::Arc,
};
use crate::LOG_TARGET;
use pezsc_transaction_pool_api::error;
use serde::Serialize;
use pezsp_runtime::{traits::Member, transaction_validity::TransactionTag as Tag};
use tracing::trace;
use super::{
base_pool::Transaction,
future::WaitingTransaction,
tracked_map::{self, TrackedMap},
};
/// An in-pool transaction reference.
///
/// Should be cheap to clone.
#[derive(Debug)]
pub struct TransactionRef<Hash, Ex> {
/// The actual transaction data.
pub transaction: Arc<Transaction<Hash, Ex>>,
/// Unique id when transaction was inserted into the pool.
pub insertion_id: u64,
}
impl<Hash, Ex> Clone for TransactionRef<Hash, Ex> {
fn clone(&self) -> Self {
Self { transaction: self.transaction.clone(), insertion_id: self.insertion_id }
}
}
impl<Hash, Ex> Ord for TransactionRef<Hash, Ex> {
fn cmp(&self, other: &Self) -> cmp::Ordering {
self.transaction
.priority
.cmp(&other.transaction.priority)
.then_with(|| other.transaction.valid_till.cmp(&self.transaction.valid_till))
.then_with(|| other.insertion_id.cmp(&self.insertion_id))
}
}
impl<Hash, Ex> PartialOrd for TransactionRef<Hash, Ex> {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<Hash, Ex> PartialEq for TransactionRef<Hash, Ex> {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == cmp::Ordering::Equal
}
}
impl<Hash, Ex> Eq for TransactionRef<Hash, Ex> {}
#[derive(Debug)]
pub struct ReadyTx<Hash, Ex> {
/// A reference to a transaction
pub transaction: TransactionRef<Hash, Ex>,
/// A list of transactions that get unlocked by this one
pub unlocks: Vec<Hash>,
/// How many required tags are provided inherently
///
/// Some transactions might be already pruned from the queue,
/// so when we compute ready set we may consider these transactions ready earlier.
pub requires_offset: usize,
}
impl<Hash: Clone, Ex> Clone for ReadyTx<Hash, Ex> {
fn clone(&self) -> Self {
Self {
transaction: self.transaction.clone(),
unlocks: self.unlocks.clone(),
requires_offset: self.requires_offset,
}
}
}
const HASH_READY: &str = r#"
Every time transaction is imported its hash is placed in `ready` map and tags in `provided_tags`;
Every time transaction is removed from the queue we remove the hash from `ready` map and from `provided_tags`;
Hence every hash retrieved from `provided_tags` is always present in `ready`;
qed
"#;
/// Validated transactions that are block ready with all their dependencies met.
#[derive(Clone, Debug)]
pub struct ReadyTransactions<Hash: hash::Hash + Eq, Ex> {
/// Next free insertion id (used to indicate when a transaction was inserted into the pool).
insertion_id: u64,
/// tags that are provided by Ready transactions
/// (only a single transaction can provide a specific tag)
provided_tags: HashMap<Tag, Hash>,
/// Transactions that are ready (i.e. don't have any requirements external to the pool)
ready: TrackedMap<Hash, ReadyTx<Hash, Ex>>,
/// Best transactions that are ready to be included to the block without any other previous
/// transaction.
best: BTreeSet<TransactionRef<Hash, Ex>>,
}
impl<Hash, Ex> tracked_map::Size for ReadyTx<Hash, Ex> {
fn size(&self) -> usize {
self.transaction.transaction.bytes
}
}
impl<Hash: hash::Hash + Eq, Ex> Default for ReadyTransactions<Hash, Ex> {
fn default() -> Self {
Self {
insertion_id: Default::default(),
provided_tags: Default::default(),
ready: Default::default(),
best: Default::default(),
}
}
}
impl<Hash: hash::Hash + Member + Serialize, Ex> ReadyTransactions<Hash, Ex> {
/// Borrows a map of tags that are provided by transactions in this queue.
pub fn provided_tags(&self) -> &HashMap<Tag, Hash> {
&self.provided_tags
}
/// Returns an iterator of ready transactions.
///
/// Transactions are returned in order:
/// 1. First by the dependencies:
/// - never return transaction that requires a tag, which was not provided by one of the
/// previously
/// returned transactions
/// 2. Then by priority:
/// - If there are two transactions with all requirements satisfied the one with higher priority
/// goes first.
/// 3. Then by the ttl that's left
/// - transactions that are valid for a shorter time go first
/// 4. Lastly we sort by the time in the queue
/// - transactions that are longer in the queue go first
///
/// The iterator is providing a way to report transactions that the receiver considers invalid.
/// In such case the entire subgraph of transactions that depend on the reported one will be
/// skipped.
pub fn get(&self) -> BestIterator<Hash, Ex> {
BestIterator {
all: self.ready.clone_map(),
best: self.best.clone(),
awaiting: Default::default(),
invalid: Default::default(),
}
}
/// Imports transactions to the pool of ready transactions.
///
/// The transaction needs to have all tags satisfied (be ready) by transactions
/// that are in this queue.
/// Returns transactions that were replaced by the one imported.
pub fn import(
&mut self,
tx: WaitingTransaction<Hash, Ex>,
) -> error::Result<Vec<Arc<Transaction<Hash, Ex>>>> {
assert!(
tx.is_ready(),
"Only ready transactions can be imported. Missing: {:?}",
tx.missing_tags
);
assert!(
!self.ready.read().contains_key(&tx.transaction.hash),
"Transaction is already imported."
);
self.insertion_id += 1;
let insertion_id = self.insertion_id;
let hash = tx.transaction.hash.clone();
let transaction = tx.transaction;
let (replaced, unlocks) = self.replace_previous(&transaction)?;
let mut goes_to_best = true;
let mut ready = self.ready.write();
let mut requires_offset = 0;
// Add links to transactions that unlock the current one
for tag in &transaction.requires {
// Check if the transaction that satisfies the tag is still in the queue.
if let Some(other) = self.provided_tags.get(tag) {
let tx = ready.get_mut(other).expect(HASH_READY);
tx.unlocks.push(hash.clone());
// this transaction depends on some other, so it doesn't go to best directly.
goes_to_best = false;
} else {
requires_offset += 1;
}
}
// update provided_tags
// call to replace_previous guarantees that we will be overwriting
// only entries that have been removed.
for tag in &transaction.provides {
self.provided_tags.insert(tag.clone(), hash.clone());
}
let transaction = TransactionRef { insertion_id, transaction };
// insert to best if it doesn't require any other transaction to be included before it
if goes_to_best {
self.best.insert(transaction.clone());
}
// insert to Ready
ready.insert(hash, ReadyTx { transaction, unlocks, requires_offset });
Ok(replaced)
}
/// Fold a list of ready transactions to compute a single value using initial value of
/// accumulator.
pub fn fold<R, F: FnMut(R, &ReadyTx<Hash, Ex>) -> R>(&self, init: R, f: F) -> R {
self.ready.read().values().fold(init, f)
}
/// Returns true if given transaction is part of the queue.
pub fn contains(&self, hash: &Hash) -> bool {
self.ready.read().contains_key(hash)
}
/// Retrieve transaction by hash
pub fn by_hash(&self, hash: &Hash) -> Option<Arc<Transaction<Hash, Ex>>> {
self.by_hashes(&[hash.clone()]).into_iter().next().unwrap_or(None)
}
/// Retrieve transactions by hash
pub fn by_hashes(&self, hashes: &[Hash]) -> Vec<Option<Arc<Transaction<Hash, Ex>>>> {
let ready = self.ready.read();
hashes
.iter()
.map(|hash| ready.get(hash).map(|x| x.transaction.transaction.clone()))
.collect()
}
/// Removes a subtree of transactions from the ready pool.
///
/// NOTE removing a transaction will also cause a removal of all transactions that depend on
/// that one (i.e. the entire subgraph that this transaction is a start of will be removed).
/// All removed transactions are returned.
pub fn remove_subtree(&mut self, hashes: &[Hash]) -> Vec<Arc<Transaction<Hash, Ex>>> {
let to_remove = hashes.to_vec();
self.remove_subtree_with_tag_filter(to_remove, None)
}
/// Removes a subtrees of transactions trees starting from roots given in `to_remove`.
///
/// We proceed with a particular branch only if there is at least one provided tag
/// that is not part of `provides_tag_filter`. I.e. the filter contains tags
/// that will stay in the pool, so that we can early exit and avoid descending.
fn remove_subtree_with_tag_filter(
&mut self,
mut to_remove: Vec<Hash>,
provides_tag_filter: Option<HashSet<Tag>>,
) -> Vec<Arc<Transaction<Hash, Ex>>> {
let mut removed = vec![];
let mut ready = self.ready.write();
while let Some(tx_hash) = to_remove.pop() {
if let Some(mut tx) = ready.remove(&tx_hash) {
let invalidated = tx.transaction.transaction.provides.iter().filter(|tag| {
provides_tag_filter
.as_ref()
.map(|filter| !filter.contains(&**tag))
.unwrap_or(true)
});
let mut removed_some_tags = false;
// remove entries from provided_tags
for tag in invalidated {
removed_some_tags = true;
self.provided_tags.remove(tag);
}
// remove from unlocks
for tag in &tx.transaction.transaction.requires {
if let Some(hash) = self.provided_tags.get(tag) {
if let Some(tx_unlocking) = ready.get_mut(hash) {
remove_item(&mut tx_unlocking.unlocks, &tx_hash);
}
}
}
// remove from best
self.best.remove(&tx.transaction);
if removed_some_tags {
// remove all transactions that the current one unlocks
to_remove.append(&mut tx.unlocks);
}
// add to removed
trace!(target: LOG_TARGET, ?tx_hash, "Removed as part of the subtree.");
removed.push(tx.transaction.transaction);
}
}
removed
}
/// Removes transactions that provide given tag.
///
/// All transactions that lead to a transaction, which provides this tag
/// are going to be removed from the queue, but no other transactions are touched -
/// i.e. all other subgraphs starting from given tag are still considered valid & ready.
pub fn prune_tags(&mut self, tag: Tag) -> Vec<Arc<Transaction<Hash, Ex>>> {
let mut removed = vec![];
let mut to_remove = vec![tag];
while let Some(tag) = to_remove.pop() {
let res = self
.provided_tags
.remove(&tag)
.and_then(|hash| self.ready.write().remove(&hash));
if let Some(tx) = res {
let unlocks = tx.unlocks;
// Make sure we remove it from best txs
self.best.remove(&tx.transaction);
let tx = tx.transaction.transaction;
// prune previous transactions as well
{
let hash = &tx.hash;
let mut ready = self.ready.write();
let mut find_previous = |tag| -> Option<Vec<Tag>> {
let prev_hash = self.provided_tags.get(tag)?;
let tx2 = ready.get_mut(prev_hash)?;
remove_item(&mut tx2.unlocks, hash);
// We eagerly prune previous transactions as well.
// But it might not always be good.
// Possible edge case:
// - tx provides two tags
// - the second tag enables some subgraph we don't know of yet
// - we will prune the transaction
// - when we learn about the subgraph it will go to future
// - we will have to wait for re-propagation of that transaction
// Alternatively the caller may attempt to re-import these transactions.
if tx2.unlocks.is_empty() {
Some(tx2.transaction.transaction.provides.clone())
} else {
None
}
};
// find previous transactions
for tag in &tx.requires {
if let Some(mut tags_to_remove) = find_previous(tag) {
to_remove.append(&mut tags_to_remove);
}
}
}
// add the transactions that just got unlocked to `best`
for hash in unlocks {
if let Some(tx) = self.ready.write().get_mut(&hash) {
tx.requires_offset += 1;
// this transaction is ready
if tx.requires_offset == tx.transaction.transaction.requires.len() {
self.best.insert(tx.transaction.clone());
}
}
}
// we also need to remove all other tags that this transaction provides,
// but since all the hard work is done, we only clear the provided_tag -> hash
// mapping.
let current_tag = &tag;
for tag in &tx.provides {
let removed = self.provided_tags.remove(tag);
assert_eq!(
removed.as_ref(),
if current_tag == tag { None } else { Some(&tx.hash) },
"The pool contains exactly one transaction providing given tag; the removed transaction
claims to provide that tag, so it has to be mapped to it's hash; qed"
);
}
removed.push(tx);
}
}
removed
}
/// Checks if the transaction is providing the same tags as other transactions.
///
/// In case that's true it determines if the priority of transactions that
/// we are about to replace is lower than the priority of the replacement transaction.
/// We remove/replace old transactions in case they have lower priority.
///
/// In case replacement is successful returns a list of removed transactions
/// and a list of hashes that are still in pool and gets unlocked by the new transaction.
fn replace_previous(
&mut self,
tx: &Transaction<Hash, Ex>,
) -> error::Result<(Vec<Arc<Transaction<Hash, Ex>>>, Vec<Hash>)> {
let (to_remove, unlocks) = {
// check if we are replacing a transaction
let replace_hashes = tx
.provides
.iter()
.filter_map(|tag| self.provided_tags.get(tag))
.collect::<HashSet<_>>();
// early exit if we are not replacing anything.
if replace_hashes.is_empty() {
return Ok((vec![], vec![]));
}
// now check if collective priority is lower than the replacement transaction.
let old_priority = {
let ready = self.ready.read();
replace_hashes
.iter()
.filter_map(|hash| ready.get(hash))
.fold(0u64, |total, tx| {
total.saturating_add(tx.transaction.transaction.priority)
})
};
// bail - the transaction has too low priority to replace the old ones
if old_priority >= tx.priority {
return Err(error::Error::TooLowPriority { old: old_priority, new: tx.priority });
}
// construct a list of unlocked transactions
let unlocks = {
let ready = self.ready.read();
replace_hashes.iter().filter_map(|hash| ready.get(hash)).fold(
vec![],
|mut list, tx| {
list.extend(tx.unlocks.iter().cloned());
list
},
)
};
(replace_hashes.into_iter().cloned().collect::<Vec<_>>(), unlocks)
};
let new_provides = tx.provides.iter().cloned().collect::<HashSet<_>>();
let removed = self.remove_subtree_with_tag_filter(to_remove, Some(new_provides));
Ok((removed, unlocks))
}
/// Returns number of transactions in this queue.
pub fn len(&self) -> usize {
self.ready.len()
}
/// Returns sum of encoding lengths of all transactions in this queue.
pub fn bytes(&self) -> usize {
self.ready.bytes()
}
}
/// Iterator of ready transactions ordered by priority.
pub struct BestIterator<Hash, Ex> {
all: HashMap<Hash, ReadyTx<Hash, Ex>>,
awaiting: HashMap<Hash, (usize, TransactionRef<Hash, Ex>)>,
best: BTreeSet<TransactionRef<Hash, Ex>>,
invalid: HashSet<Hash>,
}
impl<Hash: hash::Hash + Member, Ex> BestIterator<Hash, Ex> {
/// Depending on number of satisfied requirements insert given ref
/// either to awaiting set or to best set.
fn best_or_awaiting(&mut self, satisfied: usize, tx_ref: TransactionRef<Hash, Ex>) {
if satisfied >= tx_ref.transaction.requires.len() {
// If we have satisfied all deps insert to best
self.best.insert(tx_ref);
} else {
// otherwise we're still awaiting for some deps
self.awaiting.insert(tx_ref.transaction.hash.clone(), (satisfied, tx_ref));
}
}
}
impl<Hash: hash::Hash + Member, Ex> pezsc_transaction_pool_api::ReadyTransactions
for BestIterator<Hash, Ex>
{
fn report_invalid(&mut self, tx: &Self::Item) {
BestIterator::report_invalid(self, tx)
}
}
impl<Hash: hash::Hash + Member, Ex> BestIterator<Hash, Ex> {
/// Report given transaction as invalid.
///
/// As a consequence, all values that depend on the invalid one will be skipped.
/// When given transaction is not in the pool it has no effect.
/// When invoked on a fully drained iterator it has no effect either.
pub fn report_invalid(&mut self, tx: &Arc<Transaction<Hash, Ex>>) {
if let Some(to_report) = self.all.get(&tx.hash) {
trace!(
target: LOG_TARGET,
tx_hash = ?to_report.transaction.transaction.hash,
"best-iterator: Reported as invalid. Will skip sub-chains while iterating."
);
for hash in &to_report.unlocks {
self.invalid.insert(hash.clone());
}
}
}
}
impl<Hash: hash::Hash + Member, Ex> Iterator for BestIterator<Hash, Ex> {
type Item = Arc<Transaction<Hash, Ex>>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let best = self.best.iter().next_back()?.clone();
let best = self.best.take(&best)?;
let tx_hash = &best.transaction.hash;
// Check if the transaction was marked invalid.
if self.invalid.contains(tx_hash) {
trace!(
target: LOG_TARGET,
?tx_hash,
"Skipping invalid child transaction while iterating."
);
continue;
}
let ready = match self.all.get(tx_hash).cloned() {
Some(ready) => ready,
// The transaction is not in all, maybe it was removed in the meantime?
None => continue,
};
// Insert transactions that just got unlocked.
for hash in &ready.unlocks {
// first check local awaiting transactions
let res = if let Some((mut satisfied, tx_ref)) = self.awaiting.remove(hash) {
satisfied += 1;
Some((satisfied, tx_ref))
// then get from the pool
} else {
self.all
.get(hash)
.map(|next| (next.requires_offset + 1, next.transaction.clone()))
};
if let Some((satisfied, tx_ref)) = res {
self.best_or_awaiting(satisfied, tx_ref)
}
}
return Some(best.transaction);
}
}
}
// See: https://github.com/rust-lang/rust/issues/40062
fn remove_item<T: PartialEq>(vec: &mut Vec<T>, item: &T) {
if let Some(idx) = vec.iter().position(|i| i == item) {
vec.swap_remove(idx);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tx(id: u8) -> Transaction<u64, Vec<u8>> {
Transaction {
data: vec![id],
bytes: 1,
hash: id as u64,
priority: 1,
valid_till: 2,
requires: vec![vec![1], vec![2]],
provides: vec![vec![3], vec![4]],
propagate: true,
source: crate::TimedTransactionSource::new_external(false),
}
}
fn import<H: hash::Hash + Eq + Member + Serialize, Ex>(
ready: &mut ReadyTransactions<H, Ex>,
tx: Transaction<H, Ex>,
) -> error::Result<Vec<Arc<Transaction<H, Ex>>>> {
let x = WaitingTransaction::new(tx, ready.provided_tags(), &[]);
ready.import(x)
}
#[test]
fn should_replace_transaction_that_provides_the_same_tag() {
// given
let mut ready = ReadyTransactions::default();
let mut tx1 = tx(1);
tx1.requires.clear();
let mut tx2 = tx(2);
tx2.requires.clear();
tx2.provides = vec![vec![3]];
let mut tx3 = tx(3);
tx3.requires.clear();
tx3.provides = vec![vec![4]];
// when
import(&mut ready, tx2).unwrap();
import(&mut ready, tx3).unwrap();
assert_eq!(ready.get().count(), 2);
// too low priority
import(&mut ready, tx1.clone()).unwrap_err();
tx1.priority = 10;
import(&mut ready, tx1).unwrap();
// then
assert_eq!(ready.get().count(), 1);
}
#[test]
fn should_replace_multiple_transactions_correctly() {
// given
let mut ready = ReadyTransactions::default();
let mut tx0 = tx(0);
tx0.requires = vec![];
tx0.provides = vec![vec![0]];
let mut tx1 = tx(1);
tx1.requires = vec![];
tx1.provides = vec![vec![1]];
let mut tx2 = tx(2);
tx2.requires = vec![vec![0], vec![1]];
tx2.provides = vec![vec![2], vec![3]];
let mut tx3 = tx(3);
tx3.requires = vec![vec![2]];
tx3.provides = vec![vec![4]];
let mut tx4 = tx(4);
tx4.requires = vec![vec![3]];
tx4.provides = vec![vec![5]];
// replacement
let mut tx2_2 = tx(5);
tx2_2.requires = vec![vec![0], vec![1]];
tx2_2.provides = vec![vec![2]];
tx2_2.priority = 10;
for tx in vec![tx0, tx1, tx2, tx3, tx4] {
import(&mut ready, tx).unwrap();
}
assert_eq!(ready.get().count(), 5);
// when
import(&mut ready, tx2_2).unwrap();
// then
assert_eq!(ready.get().count(), 3);
}
/// Populate the pool, with a graph that looks like so:
///
/// tx1 -> tx2 \
/// -> -> tx3
/// -> tx4 -> tx5 -> tx6
/// -> tx7
fn populate_pool(ready: &mut ReadyTransactions<u64, Vec<u8>>) {
let mut tx1 = tx(1);
tx1.requires.clear();
let mut tx2 = tx(2);
tx2.requires = tx1.provides.clone();
tx2.provides = vec![vec![106]];
let mut tx3 = tx(3);
tx3.requires = vec![tx1.provides[0].clone(), vec![106]];
tx3.provides = vec![];
let mut tx4 = tx(4);
tx4.requires = vec![tx1.provides[0].clone()];
tx4.provides = vec![vec![107]];
let mut tx5 = tx(5);
tx5.requires = vec![tx4.provides[0].clone()];
tx5.provides = vec![vec![108]];
let mut tx6 = tx(6);
tx6.requires = vec![tx5.provides[0].clone()];
tx6.provides = vec![];
let tx7 = Transaction {
data: vec![7].into(),
bytes: 1,
hash: 7,
priority: 1,
valid_till: u64::MAX, // use the max here for testing.
requires: vec![tx1.provides[0].clone()],
provides: vec![],
propagate: true,
source: crate::TimedTransactionSource::new_external(false),
};
// when
for tx in vec![tx1, tx2, tx3, tx7, tx4, tx5, tx6] {
import(ready, tx).unwrap();
}
assert_eq!(ready.best.len(), 1);
}
#[test]
fn should_return_best_transactions_in_correct_order() {
// given
let mut ready = ReadyTransactions::default();
populate_pool(&mut ready);
// when
let mut it = ready.get().map(|tx| tx.data[0]);
// then
assert_eq!(it.next(), Some(1));
assert_eq!(it.next(), Some(2));
assert_eq!(it.next(), Some(3));
assert_eq!(it.next(), Some(4));
assert_eq!(it.next(), Some(5));
assert_eq!(it.next(), Some(6));
assert_eq!(it.next(), Some(7));
assert_eq!(it.next(), None);
}
#[test]
fn should_order_refs() {
let mut id = 1;
let mut with_priority = |priority, longevity| {
id += 1;
let mut tx = tx(id);
tx.priority = priority;
tx.valid_till = longevity;
tx
};
// higher priority = better
assert!(
TransactionRef { transaction: Arc::new(with_priority(3, 3)), insertion_id: 1 } >
TransactionRef { transaction: Arc::new(with_priority(2, 3)), insertion_id: 2 }
);
// lower validity = better
assert!(
TransactionRef { transaction: Arc::new(with_priority(3, 2)), insertion_id: 1 } >
TransactionRef { transaction: Arc::new(with_priority(3, 3)), insertion_id: 2 }
);
// lower insertion_id = better
assert!(
TransactionRef { transaction: Arc::new(with_priority(3, 3)), insertion_id: 1 } >
TransactionRef { transaction: Arc::new(with_priority(3, 3)), insertion_id: 2 }
);
}
#[test]
fn should_skip_invalid_transactions_while_iterating() {
// given
let mut ready = ReadyTransactions::default();
populate_pool(&mut ready);
// when
let mut it = ready.get();
let data = |tx: &Arc<Transaction<u64, Vec<u8>>>| tx.data[0];
// then
assert_eq!(it.next().as_ref().map(data), Some(1));
assert_eq!(it.next().as_ref().map(data), Some(2));
assert_eq!(it.next().as_ref().map(data), Some(3));
let tx4 = it.next();
assert_eq!(tx4.as_ref().map(data), Some(4));
// report 4 as invalid, which should skip 5 & 6.
it.report_invalid(&tx4.unwrap());
assert_eq!(it.next().as_ref().map(data), Some(7));
assert_eq!(it.next().as_ref().map(data), None);
}
#[test]
fn should_remove_tx_from_unlocks_set_of_its_parent() {
// given
let mut ready = ReadyTransactions::default();
populate_pool(&mut ready);
// when
let mut it = ready.get();
let tx1 = it.next().unwrap();
let tx2 = it.next().unwrap();
let tx3 = it.next().unwrap();
let tx4 = it.next().unwrap();
let lock = ready.ready.read();
let tx1_unlocks = &lock.get(&tx1.hash).unwrap().unlocks;
// There are two tags provided by tx1 and required by tx2.
assert_eq!(tx1_unlocks[0], tx2.hash);
assert_eq!(tx1_unlocks[1], tx2.hash);
assert_eq!(tx1_unlocks[2], tx3.hash);
assert_eq!(tx1_unlocks[4], tx4.hash);
drop(lock);
// then consider tx2 invalid, and hence, remove it.
let removed = ready.remove_subtree(&[tx2.hash]);
assert_eq!(removed.len(), 2);
assert_eq!(removed[0].hash, tx2.hash);
// tx3 is removed too, since it requires tx2 provides tags.
assert_eq!(removed[1].hash, tx3.hash);
let lock = ready.ready.read();
let tx1_unlocks = &lock.get(&tx1.hash).unwrap().unlocks;
assert!(!tx1_unlocks.contains(&tx2.hash));
assert!(!tx1_unlocks.contains(&tx3.hash));
assert!(tx1_unlocks.contains(&tx4.hash));
}
}
@@ -0,0 +1,240 @@
// 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/>.
//! Rotate extrinsic inside the pool.
//!
//! Keeps only recent extrinsic and discard the ones kept for a significant amount of time.
//! Discarded extrinsics are banned so that they don't get re-imported again.
use parking_lot::RwLock;
use std::{
collections::HashMap,
hash, iter,
time::{Duration, Instant},
};
use super::base_pool::Transaction;
/// Expected size of the banned extrinsics cache.
const DEFAULT_EXPECTED_SIZE: usize = 2048;
/// The default duration, in seconds, for which an extrinsic is banned.
const DEFAULT_BAN_TIME_SECS: u64 = 30 * 60;
/// Pool rotator is responsible to only keep fresh extrinsics in the pool.
///
/// Extrinsics that occupy the pool for too long are culled and temporarily banned from entering
/// the pool again.
pub struct PoolRotator<Hash> {
/// How long the extrinsic is banned for.
ban_time: Duration,
/// Currently banned extrinsics.
banned_until: RwLock<HashMap<Hash, Instant>>,
/// Expected size of the banned extrinsics cache.
expected_size: usize,
}
impl<Hash: Clone> Clone for PoolRotator<Hash> {
fn clone(&self) -> Self {
Self {
ban_time: self.ban_time,
banned_until: RwLock::new(self.banned_until.read().clone()),
expected_size: self.expected_size,
}
}
}
impl<Hash: hash::Hash + Eq> Default for PoolRotator<Hash> {
fn default() -> Self {
Self {
ban_time: Duration::from_secs(DEFAULT_BAN_TIME_SECS),
banned_until: Default::default(),
expected_size: DEFAULT_EXPECTED_SIZE,
}
}
}
impl<Hash: hash::Hash + Eq + Clone> PoolRotator<Hash> {
/// New rotator instance with specified ban time.
pub fn new(ban_time: Duration) -> Self {
Self { ban_time, ..Self::default() }
}
/// New rotator instance with specified ban time and expected cache size.
pub fn new_with_expected_size(ban_time: Duration, expected_size: usize) -> Self {
Self { expected_size, ..Self::new(ban_time) }
}
/// Returns `true` if extrinsic hash is currently banned.
pub fn is_banned(&self, hash: &Hash) -> bool {
self.banned_until.read().contains_key(hash)
}
/// Bans given set of hashes.
pub fn ban(&self, now: &Instant, hashes: impl IntoIterator<Item = Hash>) {
let mut banned = self.banned_until.write();
for hash in hashes {
banned.insert(hash, *now + self.ban_time);
}
if banned.len() > 2 * self.expected_size {
while banned.len() > self.expected_size {
if let Some(key) = banned.keys().next().cloned() {
banned.remove(&key);
}
}
}
}
/// Bans extrinsic if it's stale.
///
/// Returns `true` if extrinsic is stale and got banned.
pub fn ban_if_stale<Ex>(
&self,
now: &Instant,
current_block: u64,
xt: &Transaction<Hash, Ex>,
) -> bool {
if xt.valid_till > current_block {
return false;
}
self.ban(now, iter::once(xt.hash.clone()));
true
}
/// Removes timed bans.
pub fn clear_timeouts(&self, now: &Instant) {
let mut banned = self.banned_until.write();
banned.retain(|_, &mut v| v >= *now);
}
}
#[cfg(test)]
mod tests {
use super::*;
type Hash = u64;
type Ex = ();
fn rotator() -> PoolRotator<Hash> {
PoolRotator { ban_time: Duration::from_millis(10), ..Default::default() }
}
fn tx() -> (Hash, Transaction<Hash, Ex>) {
let hash = 5u64;
let tx = Transaction {
data: (),
bytes: 1,
hash,
priority: 5,
valid_till: 1,
requires: vec![],
provides: vec![],
propagate: true,
source: crate::TimedTransactionSource::new_external(false),
};
(hash, tx)
}
#[test]
fn should_not_ban_if_not_stale() {
// given
let (hash, tx) = tx();
let rotator = rotator();
assert!(!rotator.is_banned(&hash));
let now = Instant::now();
let past_block = 0;
// when
assert!(!rotator.ban_if_stale(&now, past_block, &tx));
// then
assert!(!rotator.is_banned(&hash));
}
#[test]
fn should_ban_stale_extrinsic() {
// given
let (hash, tx) = tx();
let rotator = rotator();
assert!(!rotator.is_banned(&hash));
// when
assert!(rotator.ban_if_stale(&Instant::now(), 1, &tx));
// then
assert!(rotator.is_banned(&hash));
}
#[test]
fn should_clear_banned() {
// given
let (hash, tx) = tx();
let rotator = rotator();
assert!(rotator.ban_if_stale(&Instant::now(), 1, &tx));
assert!(rotator.is_banned(&hash));
// when
let future = Instant::now() + rotator.ban_time + rotator.ban_time;
rotator.clear_timeouts(&future);
// then
assert!(!rotator.is_banned(&hash));
}
#[test]
fn should_garbage_collect() {
// given
fn tx_with(i: u64, valid_till: u64) -> Transaction<Hash, Ex> {
let hash = i;
Transaction {
data: (),
bytes: 2,
hash,
priority: 5,
valid_till,
requires: vec![],
provides: vec![],
propagate: true,
source: crate::TimedTransactionSource::new_external(false),
}
}
let rotator = rotator();
let now = Instant::now();
let past_block = 0;
// when
for i in 0..2 * DEFAULT_EXPECTED_SIZE {
let tx = tx_with(i as u64, past_block);
assert!(rotator.ban_if_stale(&now, past_block, &tx));
}
assert_eq!(rotator.banned_until.read().len(), 2 * DEFAULT_EXPECTED_SIZE);
// then
let tx = tx_with(2 * DEFAULT_EXPECTED_SIZE as u64, past_block);
// trigger a garbage collection
assert!(rotator.ban_if_stale(&now, past_block, &tx));
assert_eq!(rotator.banned_until.read().len(), DEFAULT_EXPECTED_SIZE);
}
}
@@ -0,0 +1,187 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::{
collections::HashMap,
sync::{
atomic::{AtomicIsize, Ordering as AtomicOrdering},
Arc,
},
};
/// Something that can report its size.
pub trait Size {
fn size(&self) -> usize;
}
/// Map with size tracking.
///
/// Size reported might be slightly off and only approximately true.
#[derive(Debug)]
pub struct TrackedMap<K, V> {
index: Arc<RwLock<HashMap<K, V>>>,
bytes: AtomicIsize,
length: AtomicIsize,
}
impl<K, V> Default for TrackedMap<K, V> {
fn default() -> Self {
Self { index: Arc::new(HashMap::default().into()), bytes: 0.into(), length: 0.into() }
}
}
impl<K, V> Clone for TrackedMap<K, V>
where
K: Clone,
V: Clone,
{
fn clone(&self) -> Self {
Self {
index: Arc::from(RwLock::from(self.index.read().clone())),
bytes: self.bytes.load(AtomicOrdering::Relaxed).into(),
length: self.length.load(AtomicOrdering::Relaxed).into(),
}
}
}
impl<K, V> TrackedMap<K, V> {
/// Current tracked length of the content.
pub fn len(&self) -> usize {
std::cmp::max(self.length.load(AtomicOrdering::Relaxed), 0) as usize
}
/// Current sum of content length.
pub fn bytes(&self) -> usize {
std::cmp::max(self.bytes.load(AtomicOrdering::Relaxed), 0) as usize
}
/// Lock map for read.
pub fn read(&self) -> TrackedMapReadAccess<'_, K, V> {
TrackedMapReadAccess { inner_guard: self.index.read() }
}
/// Lock map for write.
pub fn write(&self) -> TrackedMapWriteAccess<'_, K, V> {
TrackedMapWriteAccess {
inner_guard: self.index.write(),
bytes: &self.bytes,
length: &self.length,
}
}
}
impl<K: Clone, V: Clone> TrackedMap<K, V> {
/// Clone the inner map.
pub fn clone_map(&self) -> HashMap<K, V> {
self.index.read().clone()
}
}
pub struct TrackedMapReadAccess<'a, K, V> {
inner_guard: RwLockReadGuard<'a, HashMap<K, V>>,
}
impl<'a, K, V> TrackedMapReadAccess<'a, K, V>
where
K: Eq + std::hash::Hash,
{
/// Returns true if the map contains given key.
pub fn contains_key(&self, key: &K) -> bool {
self.inner_guard.contains_key(key)
}
/// Returns the reference to the contained value by key, if exists.
pub fn get(&self, key: &K) -> Option<&V> {
self.inner_guard.get(key)
}
/// Returns an iterator over all values.
pub fn values(&self) -> std::collections::hash_map::Values<'_, K, V> {
self.inner_guard.values()
}
}
pub struct TrackedMapWriteAccess<'a, K, V> {
bytes: &'a AtomicIsize,
length: &'a AtomicIsize,
inner_guard: RwLockWriteGuard<'a, HashMap<K, V>>,
}
impl<'a, K, V> TrackedMapWriteAccess<'a, K, V>
where
K: Eq + std::hash::Hash,
V: Size,
{
/// Insert value and return previous (if any).
pub fn insert(&mut self, key: K, val: V) -> Option<V> {
let new_bytes = val.size();
self.bytes.fetch_add(new_bytes as isize, AtomicOrdering::Relaxed);
self.length.fetch_add(1, AtomicOrdering::Relaxed);
self.inner_guard.insert(key, val).inspect(|old_val| {
self.bytes.fetch_sub(old_val.size() as isize, AtomicOrdering::Relaxed);
self.length.fetch_sub(1, AtomicOrdering::Relaxed);
})
}
/// Remove value by key.
pub fn remove(&mut self, key: &K) -> Option<V> {
let val = self.inner_guard.remove(key);
if let Some(size) = val.as_ref().map(Size::size) {
self.bytes.fetch_sub(size as isize, AtomicOrdering::Relaxed);
self.length.fetch_sub(1, AtomicOrdering::Relaxed);
}
val
}
/// Returns mutable reference to the contained value by key, if exists.
pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
self.inner_guard.get_mut(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
impl Size for i32 {
fn size(&self) -> usize {
*self as usize / 10
}
}
#[test]
fn basic() {
let map = TrackedMap::default();
map.write().insert(5, 10);
map.write().insert(6, 20);
assert_eq!(map.bytes(), 3);
assert_eq!(map.len(), 2);
map.write().insert(6, 30);
assert_eq!(map.bytes(), 4);
assert_eq!(map.len(), 2);
map.write().remove(&6);
assert_eq!(map.bytes(), 1);
assert_eq!(map.len(), 1);
}
}
@@ -0,0 +1,898 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::{
common::{
sliding_stat::SyncDurationSlidingStats, tracing_log_xt::log_xt_trace, STAT_SLIDING_WINDOW,
},
insert_and_log_throttled_sync, LOG_TARGET,
};
use futures::channel::mpsc::{channel, Sender};
use indexmap::IndexMap;
use parking_lot::{Mutex, RwLock};
use pezsc_transaction_pool_api::{error, PoolStatus, ReadyTransactions, TransactionPriority};
use pezsp_blockchain::HashAndNumber;
use pezsp_runtime::{
traits::SaturatedConversion,
transaction_validity::{TransactionTag as Tag, ValidTransaction},
};
use std::{
collections::{HashMap, HashSet},
sync::Arc,
time::{Duration, Instant},
};
use tracing::{debug, trace, warn, Level};
use super::{
base_pool::{self as base, PruneStatus},
listener::EventHandler,
pool::{
BlockHash, ChainApi, EventStream, ExtrinsicFor, ExtrinsicHash, Options, TransactionFor,
},
rotator::PoolRotator,
watcher::Watcher,
};
/// Pre-validated transaction. Validated pool only accepts transactions wrapped in this enum.
#[derive(Debug)]
pub enum ValidatedTransaction<Hash, Ex, Error> {
/// Transaction that has been validated successfully.
Valid(base::Transaction<Hash, Ex>),
/// Transaction that is invalid.
Invalid(Hash, Error),
/// Transaction which validity can't be determined.
///
/// We're notifying watchers about failure, if 'unknown' transaction is submitted.
Unknown(Hash, Error),
}
impl<Hash, Ex, Error> ValidatedTransaction<Hash, Ex, Error> {
/// Consume validity result, transaction data and produce ValidTransaction.
pub fn valid_at(
at: u64,
hash: Hash,
source: base::TimedTransactionSource,
data: Ex,
bytes: usize,
validity: ValidTransaction,
) -> Self {
Self::Valid(base::Transaction {
data,
bytes,
hash,
source,
priority: validity.priority,
requires: validity.requires,
provides: validity.provides,
propagate: validity.propagate,
valid_till: at.saturated_into::<u64>().saturating_add(validity.longevity),
})
}
/// Returns priority for valid transaction, None if transaction is not valid.
pub fn priority(&self) -> Option<TransactionPriority> {
match self {
ValidatedTransaction::Valid(base::Transaction { priority, .. }) => Some(*priority),
_ => None,
}
}
}
/// A type of validated transaction stored in the validated pool.
pub type ValidatedTransactionFor<B> =
ValidatedTransaction<ExtrinsicHash<B>, ExtrinsicFor<B>, <B as ChainApi>::Error>;
/// A type alias representing ValidatedPool event dispatcher for given ChainApi type.
pub type EventDispatcher<B, L> = super::listener::EventDispatcher<ExtrinsicHash<B>, B, L>;
/// A closure that returns true if the local node is a validator that can author blocks.
#[derive(Clone)]
pub struct IsValidator(Arc<Box<dyn Fn() -> bool + Send + Sync>>);
impl From<bool> for IsValidator {
fn from(is_validator: bool) -> Self {
Self(Arc::new(Box::new(move || is_validator)))
}
}
impl From<Box<dyn Fn() -> bool + Send + Sync>> for IsValidator {
fn from(is_validator: Box<dyn Fn() -> bool + Send + Sync>) -> Self {
Self(Arc::new(is_validator))
}
}
/// Represents the result of `submit` or `submit_and_watch` operations.
pub struct BaseSubmitOutcome<B: ChainApi, W> {
/// The hash of the submitted transaction.
hash: ExtrinsicHash<B>,
/// A transaction watcher. This is `Some` for `submit_and_watch` and `None` for `submit`.
watcher: Option<W>,
/// The priority of the transaction. Defaults to None if unknown.
priority: Option<TransactionPriority>,
}
/// Type alias to outcome of submission to `ValidatedPool`.
pub type ValidatedPoolSubmitOutcome<B> =
BaseSubmitOutcome<B, Watcher<ExtrinsicHash<B>, ExtrinsicHash<B>>>;
impl<B: ChainApi, W> BaseSubmitOutcome<B, W> {
/// Creates a new instance with given hash and priority.
pub fn new(hash: ExtrinsicHash<B>, priority: Option<TransactionPriority>) -> Self {
Self { hash, priority, watcher: None }
}
/// Sets the transaction watcher.
pub fn with_watcher(mut self, watcher: W) -> Self {
self.watcher = Some(watcher);
self
}
/// Provides priority of submitted transaction.
pub fn priority(&self) -> Option<TransactionPriority> {
self.priority
}
/// Provides hash of submitted transaction.
pub fn hash(&self) -> ExtrinsicHash<B> {
self.hash
}
/// Provides a watcher. Should only be called on outcomes of `submit_and_watch`. Otherwise will
/// panic (that would mean logical error in program).
pub fn expect_watcher(&mut self) -> W {
self.watcher.take().expect("watcher was set in submit_and_watch. qed")
}
}
/// Pool that deals with validated transactions.
pub struct ValidatedPool<B: ChainApi, L: EventHandler<B>> {
api: Arc<B>,
is_validator: IsValidator,
options: Options,
event_dispatcher: RwLock<EventDispatcher<B, L>>,
pub(crate) pool: RwLock<base::BasePool<ExtrinsicHash<B>, ExtrinsicFor<B>>>,
import_notification_sinks: Mutex<Vec<Sender<ExtrinsicHash<B>>>>,
rotator: PoolRotator<ExtrinsicHash<B>>,
enforce_limits_stats: SyncDurationSlidingStats,
}
impl<B: ChainApi, L: EventHandler<B>> Clone for ValidatedPool<B, L> {
fn clone(&self) -> Self {
Self {
api: self.api.clone(),
is_validator: self.is_validator.clone(),
options: self.options.clone(),
event_dispatcher: Default::default(),
pool: RwLock::from(self.pool.read().clone()),
import_notification_sinks: Default::default(),
rotator: self.rotator.clone(),
enforce_limits_stats: self.enforce_limits_stats.clone(),
}
}
}
impl<B: ChainApi, L: EventHandler<B>> ValidatedPool<B, L> {
pub fn deep_clone_with_event_handler(&self, event_handler: L) -> Self {
Self {
event_dispatcher: RwLock::new(EventDispatcher::new_with_event_handler(Some(
event_handler,
))),
..self.clone()
}
}
/// Create a new transaction pool with statically sized rotator.
pub fn new_with_staticly_sized_rotator(
options: Options,
is_validator: IsValidator,
api: Arc<B>,
) -> Self {
let ban_time = options.ban_time;
Self::new_with_rotator(options, is_validator, api, PoolRotator::new(ban_time), None)
}
/// Create a new transaction pool.
pub fn new(options: Options, is_validator: IsValidator, api: Arc<B>) -> Self {
let ban_time = options.ban_time;
let total_count = options.total_count();
Self::new_with_rotator(
options,
is_validator,
api,
PoolRotator::new_with_expected_size(ban_time, total_count),
None,
)
}
/// Create a new transaction pool with given event handler.
pub fn new_with_event_handler(
options: Options,
is_validator: IsValidator,
api: Arc<B>,
event_handler: L,
) -> Self {
let ban_time = options.ban_time;
let total_count = options.total_count();
Self::new_with_rotator(
options,
is_validator,
api,
PoolRotator::new_with_expected_size(ban_time, total_count),
Some(event_handler),
)
}
fn new_with_rotator(
options: Options,
is_validator: IsValidator,
api: Arc<B>,
rotator: PoolRotator<ExtrinsicHash<B>>,
event_handler: Option<L>,
) -> Self {
let base_pool = base::BasePool::new(options.reject_future_transactions);
Self {
is_validator,
options,
event_dispatcher: RwLock::new(EventDispatcher::new_with_event_handler(event_handler)),
api,
pool: RwLock::new(base_pool),
import_notification_sinks: Default::default(),
rotator,
enforce_limits_stats: SyncDurationSlidingStats::new(Duration::from_secs(
STAT_SLIDING_WINDOW,
)),
}
}
/// Bans given set of hashes.
pub fn ban(&self, now: &Instant, hashes: impl IntoIterator<Item = ExtrinsicHash<B>>) {
self.rotator.ban(now, hashes)
}
/// Returns true if transaction with given hash is currently banned from the pool.
pub fn is_banned(&self, hash: &ExtrinsicHash<B>) -> bool {
self.rotator.is_banned(hash)
}
/// A fast check before doing any further processing of a transaction, like validation.
///
/// If `ignore_banned` is `true`, it will not check if the transaction is banned.
///
/// It checks if the transaction is already imported or banned. If so, it returns an error.
pub fn check_is_known(
&self,
tx_hash: &ExtrinsicHash<B>,
ignore_banned: bool,
) -> Result<(), B::Error> {
if !ignore_banned && self.is_banned(tx_hash) {
Err(error::Error::TemporarilyBanned.into())
} else if self.pool.read().is_imported(tx_hash) {
Err(error::Error::AlreadyImported(Box::new(*tx_hash)).into())
} else {
Ok(())
}
}
/// Imports a bunch of pre-validated transactions to the pool.
pub fn submit(
&self,
txs: impl IntoIterator<Item = ValidatedTransactionFor<B>>,
) -> Vec<Result<ValidatedPoolSubmitOutcome<B>, B::Error>> {
let results = txs
.into_iter()
.map(|validated_tx| self.submit_one(validated_tx))
.collect::<Vec<_>>();
// only enforce limits if there is at least one imported transaction
let removed = if results.iter().any(|res| res.is_ok()) {
let start = Instant::now();
let removed = self.enforce_limits();
insert_and_log_throttled_sync!(
Level::DEBUG,
target:"txpool",
prefix:"enforce_limits_stats",
self.enforce_limits_stats,
start.elapsed().into()
);
removed
} else {
Default::default()
};
results
.into_iter()
.map(|res| match res {
Ok(outcome) if removed.contains(&outcome.hash) =>
Err(error::Error::ImmediatelyDropped.into()),
other => other,
})
.collect()
}
/// Submit single pre-validated transaction to the pool.
fn submit_one(
&self,
tx: ValidatedTransactionFor<B>,
) -> Result<ValidatedPoolSubmitOutcome<B>, B::Error> {
match tx {
ValidatedTransaction::Valid(tx) => {
let priority = tx.priority;
trace!(
target: LOG_TARGET,
tx_hash = ?tx.hash,
"ValidatedPool::submit_one"
);
if !tx.propagate && !(self.is_validator.0)() {
return Err(error::Error::Unactionable.into());
}
let imported = self.pool.write().import(tx)?;
if let base::Imported::Ready { ref hash, .. } = imported {
let sinks = &mut self.import_notification_sinks.lock();
sinks.retain_mut(|sink| match sink.try_send(*hash) {
Ok(()) => true,
Err(e) =>
if e.is_full() {
warn!(
target: LOG_TARGET,
tx_hash = ?hash,
"Trying to notify an import but the channel is full"
);
true
} else {
false
},
});
}
let mut event_dispatcher = self.event_dispatcher.write();
fire_events(&mut *event_dispatcher, &imported);
Ok(ValidatedPoolSubmitOutcome::new(*imported.hash(), Some(priority)))
},
ValidatedTransaction::Invalid(tx_hash, error) => {
trace!(
target: LOG_TARGET,
?tx_hash,
?error,
"ValidatedPool::submit_one invalid"
);
self.rotator.ban(&Instant::now(), std::iter::once(tx_hash));
Err(error)
},
ValidatedTransaction::Unknown(tx_hash, error) => {
trace!(
target: LOG_TARGET,
?tx_hash,
?error,
"ValidatedPool::submit_one unknown"
);
self.event_dispatcher.write().invalid(&tx_hash);
Err(error)
},
}
}
fn enforce_limits(&self) -> HashSet<ExtrinsicHash<B>> {
let status = self.pool.read().status();
let ready_limit = &self.options.ready;
let future_limit = &self.options.future;
if ready_limit.is_exceeded(status.ready, status.ready_bytes) ||
future_limit.is_exceeded(status.future, status.future_bytes)
{
trace!(
target: LOG_TARGET,
ready_count = ready_limit.count,
ready_kb = ready_limit.total_bytes / 1024,
future_count = future_limit.count,
future_kb = future_limit.total_bytes / 1024,
"Enforcing limits"
);
// clean up the pool
let removed = {
let mut pool = self.pool.write();
let removed = pool
.enforce_limits(ready_limit, future_limit)
.into_iter()
.map(|x| x.hash)
.collect::<HashSet<_>>();
// ban all removed transactions
self.rotator.ban(&Instant::now(), removed.iter().copied());
removed
};
if !removed.is_empty() {
trace!(
target: LOG_TARGET,
dropped_count = removed.len(),
"Enforcing limits"
);
}
// run notifications
let mut event_dispatcher = self.event_dispatcher.write();
for h in &removed {
event_dispatcher.limits_enforced(h);
}
removed
} else {
Default::default()
}
}
/// Import a single extrinsic and starts to watch their progress in the pool.
pub fn submit_and_watch(
&self,
tx: ValidatedTransactionFor<B>,
) -> Result<ValidatedPoolSubmitOutcome<B>, B::Error> {
match tx {
ValidatedTransaction::Valid(tx) => {
let hash = self.api.hash_and_length(&tx.data).0;
let watcher = self.create_watcher(hash);
self.submit(std::iter::once(ValidatedTransaction::Valid(tx)))
.pop()
.expect("One extrinsic passed; one result returned; qed")
.map(|outcome| outcome.with_watcher(watcher))
},
ValidatedTransaction::Invalid(hash, err) => {
self.rotator.ban(&Instant::now(), std::iter::once(hash));
Err(err)
},
ValidatedTransaction::Unknown(_, err) => Err(err),
}
}
/// Creates a new watcher for given extrinsic.
pub fn create_watcher(
&self,
tx_hash: ExtrinsicHash<B>,
) -> Watcher<ExtrinsicHash<B>, ExtrinsicHash<B>> {
self.event_dispatcher.write().create_watcher(tx_hash)
}
/// Provides a list of hashes for all watched transactions in the pool.
pub fn watched_transactions(&self) -> Vec<ExtrinsicHash<B>> {
self.event_dispatcher.read().watched_transactions().map(Clone::clone).collect()
}
/// Resubmits revalidated transactions back to the pool.
///
/// Removes and then submits passed transactions and all dependent transactions.
/// Transactions that are missing from the pool are not submitted.
pub fn resubmit(
&self,
mut updated_transactions: IndexMap<ExtrinsicHash<B>, ValidatedTransactionFor<B>>,
) {
#[derive(Debug, Clone, Copy, PartialEq)]
enum Status {
Future,
Ready,
Failed,
Dropped,
}
let (mut initial_statuses, final_statuses) = {
let mut pool = self.pool.write();
// remove all passed transactions from the ready/future queues
// (this may remove additional transactions as well)
//
// for every transaction that has an entry in the `updated_transactions`,
// we store updated validation result in txs_to_resubmit
// for every transaction that has no entry in the `updated_transactions`,
// we store last validation result (i.e. the pool entry) in txs_to_resubmit
let mut initial_statuses = HashMap::new();
let mut txs_to_resubmit = Vec::with_capacity(updated_transactions.len());
while !updated_transactions.is_empty() {
let hash = updated_transactions
.keys()
.next()
.cloned()
.expect("transactions is not empty; qed");
// note we are not considering tx with hash invalid here - we just want
// to remove it along with dependent transactions and `remove_subtree()`
// does exactly what we need
let removed = pool.remove_subtree(&[hash]);
for removed_tx in removed {
let removed_hash = removed_tx.hash;
let updated_transaction = updated_transactions.shift_remove(&removed_hash);
let tx_to_resubmit = if let Some(updated_tx) = updated_transaction {
updated_tx
} else {
// in most cases we'll end up in successful `try_unwrap`, but if not
// we still need to reinsert transaction back to the pool => duplicate call
let transaction = match Arc::try_unwrap(removed_tx) {
Ok(transaction) => transaction,
Err(transaction) => transaction.duplicate(),
};
ValidatedTransaction::Valid(transaction)
};
initial_statuses.insert(removed_hash, Status::Ready);
txs_to_resubmit.push((removed_hash, tx_to_resubmit));
}
// make sure to remove the hash even if it's not present in the pool anymore.
updated_transactions.shift_remove(&hash);
}
// if we're rejecting future transactions, then insertion order matters here:
// if tx1 depends on tx2, then if tx1 is inserted before tx2, then it goes
// to the future queue and gets rejected immediately
// => let's temporary stop rejection and clear future queue before return
pool.with_futures_enabled(|pool, reject_future_transactions| {
// now resubmit all removed transactions back to the pool
let mut final_statuses = HashMap::new();
for (tx_hash, tx_to_resubmit) in txs_to_resubmit {
match tx_to_resubmit {
ValidatedTransaction::Valid(tx) => match pool.import(tx) {
Ok(imported) => match imported {
base::Imported::Ready { promoted, failed, removed, .. } => {
final_statuses.insert(tx_hash, Status::Ready);
for hash in promoted {
final_statuses.insert(hash, Status::Ready);
}
for hash in failed {
final_statuses.insert(hash, Status::Failed);
}
for tx in removed {
final_statuses.insert(tx.hash, Status::Dropped);
}
},
base::Imported::Future { .. } => {
final_statuses.insert(tx_hash, Status::Future);
},
},
Err(error) => {
// we do not want to fail if single transaction import has failed
// nor we do want to propagate this error, because it could tx
// unknown to caller => let's just notify listeners (and issue debug
// message)
warn!(
target: LOG_TARGET,
?tx_hash,
%error,
"Removing invalid transaction from update"
);
final_statuses.insert(tx_hash, Status::Failed);
},
},
ValidatedTransaction::Invalid(_, _) |
ValidatedTransaction::Unknown(_, _) => {
final_statuses.insert(tx_hash, Status::Failed);
},
}
}
// if the pool is configured to reject future transactions, let's clear the future
// queue, updating final statuses as required
if reject_future_transactions {
for future_tx in pool.clear_future() {
final_statuses.insert(future_tx.hash, Status::Dropped);
}
}
(initial_statuses, final_statuses)
})
};
// and now let's notify listeners about status changes
let mut event_dispatcher = self.event_dispatcher.write();
for (hash, final_status) in final_statuses {
let initial_status = initial_statuses.remove(&hash);
if initial_status.is_none() || Some(final_status) != initial_status {
match final_status {
Status::Future => event_dispatcher.future(&hash),
Status::Ready => event_dispatcher.ready(&hash, None),
Status::Dropped => event_dispatcher.dropped(&hash),
Status::Failed => event_dispatcher.invalid(&hash),
}
}
}
}
/// For each extrinsic, returns tags that it provides (if known), or None (if it is unknown).
pub fn extrinsics_tags(&self, hashes: &[ExtrinsicHash<B>]) -> Vec<Option<Vec<Tag>>> {
self.pool
.read()
.by_hashes(hashes)
.into_iter()
.map(|existing_in_pool| {
existing_in_pool.map(|transaction| transaction.provides.to_vec())
})
.collect()
}
/// Get ready transaction by hash
pub fn ready_by_hash(&self, hash: &ExtrinsicHash<B>) -> Option<TransactionFor<B>> {
self.pool.read().ready_by_hash(hash)
}
/// Prunes ready transactions that provide given list of tags.
pub fn prune_tags(
&self,
tags: impl IntoIterator<Item = Tag>,
) -> PruneStatus<ExtrinsicHash<B>, ExtrinsicFor<B>> {
// Perform tag-based pruning in the base pool
let status = self.pool.write().prune_tags(tags);
// Notify event listeners of all transactions
// that were promoted to `Ready` or were dropped.
{
let mut event_dispatcher = self.event_dispatcher.write();
for promoted in &status.promoted {
fire_events(&mut *event_dispatcher, promoted);
}
for f in &status.failed {
event_dispatcher.dropped(f);
}
}
status
}
/// Resubmit transactions that have been revalidated after prune_tags call.
pub fn resubmit_pruned(
&self,
at: &HashAndNumber<B::Block>,
known_imported_hashes: impl IntoIterator<Item = ExtrinsicHash<B>> + Clone,
pruned_hashes: Vec<ExtrinsicHash<B>>,
pruned_xts: Vec<ValidatedTransactionFor<B>>,
) {
debug_assert_eq!(pruned_hashes.len(), pruned_xts.len());
// Resubmit pruned transactions
let results = self.submit(pruned_xts);
// Collect the hashes of transactions that now became invalid (meaning that they are
// successfully pruned).
let hashes = results.into_iter().enumerate().filter_map(|(idx, r)| {
match r.map_err(error::IntoPoolError::into_pool_error) {
Err(Ok(error::Error::InvalidTransaction(_))) => Some(pruned_hashes[idx]),
_ => None,
}
});
// Fire `pruned` notifications for collected hashes and make sure to include
// `known_imported_hashes` since they were just imported as part of the block.
let hashes = hashes.chain(known_imported_hashes.into_iter());
self.fire_pruned(at, hashes);
// perform regular cleanup of old transactions in the pool
// and update temporary bans.
self.clear_stale(at);
}
/// Fire notifications for pruned transactions.
pub fn fire_pruned(
&self,
at: &HashAndNumber<B::Block>,
hashes: impl Iterator<Item = ExtrinsicHash<B>>,
) {
let mut event_dispatcher = self.event_dispatcher.write();
let mut set = HashSet::with_capacity(hashes.size_hint().0);
for h in hashes {
// `hashes` has possibly duplicate hashes.
// we'd like to send out the `InBlock` notification only once.
if !set.contains(&h) {
event_dispatcher.pruned(at.hash, &h);
set.insert(h);
}
}
}
/// Removes stale transactions from the pool.
///
/// Stale transactions are transaction beyond their longevity period.
/// Note this function does not remove transactions that are already included in the chain.
/// See `prune_tags` if you want this.
pub fn clear_stale(&self, at: &HashAndNumber<B::Block>) {
let HashAndNumber { number, .. } = *at;
let number = number.saturated_into::<u64>();
let now = Instant::now();
let to_remove = {
self.ready()
.filter(|tx| self.rotator.ban_if_stale(&now, number, tx))
.map(|tx| tx.hash)
.collect::<Vec<_>>()
};
let futures_to_remove: Vec<ExtrinsicHash<B>> = {
let p = self.pool.read();
let mut hashes = Vec::new();
for tx in p.futures() {
if self.rotator.ban_if_stale(&now, number, tx) {
hashes.push(tx.hash);
}
}
hashes
};
debug!(
target:LOG_TARGET,
to_remove_len=to_remove.len(),
futures_to_remove_len=futures_to_remove.len(),
"clear_stale"
);
// removing old transactions
self.remove_invalid(&to_remove);
self.remove_invalid(&futures_to_remove);
// clear banned transactions timeouts
self.rotator.clear_timeouts(&now);
}
/// Get api reference.
pub fn api(&self) -> &B {
&self.api
}
/// Return an event stream of notifications for when transactions are imported to the pool.
///
/// Consumers of this stream should use the `ready` method to actually get the
/// pending transactions in the right order.
pub fn import_notification_stream(&self) -> EventStream<ExtrinsicHash<B>> {
const CHANNEL_BUFFER_SIZE: usize = 1024;
let (sink, stream) = channel(CHANNEL_BUFFER_SIZE);
self.import_notification_sinks.lock().push(sink);
stream
}
/// Invoked when extrinsics are broadcasted.
pub fn on_broadcasted(&self, propagated: HashMap<ExtrinsicHash<B>, Vec<String>>) {
let mut event_dispatcher = self.event_dispatcher.write();
for (hash, peers) in propagated.into_iter() {
event_dispatcher.broadcasted(&hash, peers);
}
}
/// Remove a subtree of transactions from the pool and mark them invalid.
///
/// The transactions passed as an argument will be additionally banned
/// to prevent them from entering the pool right away.
/// Note this is not the case for the dependent transactions - those may
/// still be valid so we want to be able to re-import them.
///
/// For every removed transaction an Invalid event is triggered.
///
/// Returns the list of actually removed transactions, which may include transactions dependent
/// on provided set.
pub fn remove_invalid(&self, hashes: &[ExtrinsicHash<B>]) -> Vec<TransactionFor<B>> {
// early exit in case there is no invalid transactions.
if hashes.is_empty() {
return vec![];
}
let invalid = self.remove_subtree(hashes, true, |listener, removed_tx_hash| {
listener.invalid(&removed_tx_hash);
});
trace!(
target: LOG_TARGET,
removed_count = hashes.len(),
invalid_count = invalid.len(),
"Removed invalid transactions"
);
log_xt_trace!(target: LOG_TARGET, invalid.iter().map(|t| t.hash), "Removed invalid transaction");
invalid
}
/// Get an iterator for ready transactions ordered by priority
pub fn ready(&self) -> impl ReadyTransactions<Item = TransactionFor<B>> + Send {
self.pool.read().ready()
}
/// Returns a Vec of hashes and extrinsics in the future pool.
pub fn futures(&self) -> Vec<(ExtrinsicHash<B>, ExtrinsicFor<B>)> {
self.pool.read().futures().map(|tx| (tx.hash, tx.data.clone())).collect()
}
/// Returns pool status.
pub fn status(&self) -> PoolStatus {
self.pool.read().status()
}
/// Notify all watchers that transactions in the block with hash have been finalized
pub async fn on_block_finalized(&self, block_hash: BlockHash<B>) -> Result<(), B::Error> {
trace!(
target: LOG_TARGET,
?block_hash,
"Attempting to notify watchers of finalization"
);
self.event_dispatcher.write().finalized(block_hash);
Ok(())
}
/// Notify the event_dispatcher of retracted blocks
pub fn on_block_retracted(&self, block_hash: BlockHash<B>) {
self.event_dispatcher.write().retracted(block_hash)
}
/// Resends ready and future events for all the ready and future transactions that are already
/// in the pool.
///
/// Intended to be called after cloning the instance of `ValidatedPool`.
pub fn retrigger_notifications(&self) {
let pool = self.pool.read();
let mut event_dispatcher = self.event_dispatcher.write();
pool.ready().for_each(|r| {
event_dispatcher.ready(&r.hash, None);
});
pool.futures().for_each(|f| {
event_dispatcher.future(&f.hash);
});
}
/// Removes a transaction subtree from the pool, starting from the given transaction hash.
///
/// This function traverses the dependency graph of transactions and removes the specified
/// transaction along with all its descendant transactions from the pool.
///
/// The root transactions will be banned from re-entrering the pool if `ban_transactions` is
/// true. Descendant transactions may be re-submitted to the pool if required.
///
/// A `event_disaptcher_action` callback function is invoked for every transaction that is
/// removed, providing a reference to the pool's event dispatcher and the hash of the removed
/// transaction. This allows to trigger the required events.
///
/// Returns a vector containing the hashes of all removed transactions, including the root
/// transaction specified by `tx_hash`.
pub fn remove_subtree<F>(
&self,
hashes: &[ExtrinsicHash<B>],
ban_transactions: bool,
event_dispatcher_action: F,
) -> Vec<TransactionFor<B>>
where
F: Fn(&mut EventDispatcher<B, L>, ExtrinsicHash<B>),
{
// temporarily ban removed transactions if requested
if ban_transactions {
self.rotator.ban(&Instant::now(), hashes.iter().cloned());
};
let removed = self.pool.write().remove_subtree(hashes);
removed
.into_iter()
.map(|tx| {
let removed_tx_hash = tx.hash;
let mut event_dispatcher = self.event_dispatcher.write();
event_dispatcher_action(&mut *event_dispatcher, removed_tx_hash);
tx.clone()
})
.collect::<Vec<_>>()
}
}
fn fire_events<B, L, Ex>(
event_dispatcher: &mut EventDispatcher<B, L>,
imported: &base::Imported<ExtrinsicHash<B>, Ex>,
) where
B: ChainApi,
L: EventHandler<B>,
{
match *imported {
base::Imported::Ready { ref promoted, ref failed, ref removed, ref hash } => {
event_dispatcher.ready(hash, None);
failed.iter().for_each(|f| event_dispatcher.invalid(f));
removed.iter().for_each(|r| event_dispatcher.usurped(&r.hash, hash));
promoted.iter().for_each(|p| event_dispatcher.ready(p, None));
},
base::Imported::Future { ref hash } => event_dispatcher.future(hash),
}
}
@@ -0,0 +1,140 @@
// 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/>.
//! Extrinsics status updates.
use futures::Stream;
use pezsc_transaction_pool_api::TransactionStatus;
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
/// Extrinsic watcher.
///
/// Represents a stream of status updates for a particular extrinsic.
#[derive(Debug)]
pub struct Watcher<H, BH> {
receiver: TracingUnboundedReceiver<TransactionStatus<H, BH>>,
/// transaction hash of watched extrinsic
hash: H,
}
impl<H, BH> Watcher<H, BH> {
/// Returns the transaction hash.
pub fn hash(&self) -> &H {
&self.hash
}
/// Pipe the notifications to given sink.
///
/// Make sure to drive the future to completion.
pub fn into_stream(self) -> impl Stream<Item = TransactionStatus<H, BH>> {
self.receiver
}
}
/// Sender part of the watcher. Exposed only for testing purposes.
#[derive(Debug)]
pub struct Sender<H, BH> {
receivers: Vec<TracingUnboundedSender<TransactionStatus<H, BH>>>,
is_finalized: bool,
}
impl<H, BH> Default for Sender<H, BH> {
fn default() -> Self {
Sender { receivers: Default::default(), is_finalized: false }
}
}
impl<H: Clone, BH: Clone> Sender<H, BH> {
/// Add a new watcher to this sender object.
pub fn new_watcher(&mut self, hash: H) -> Watcher<H, BH> {
let (tx, receiver) = tracing_unbounded("mpsc_txpool_watcher", 100_000);
self.receivers.push(tx);
Watcher { receiver, hash }
}
/// Transaction became ready.
pub fn ready(&mut self) {
self.send(TransactionStatus::Ready)
}
/// Transaction was moved to future.
pub fn future(&mut self) {
self.send(TransactionStatus::Future)
}
/// Some state change (perhaps another extrinsic was included) rendered this extrinsic invalid.
pub fn usurped(&mut self, hash: H) {
self.send(TransactionStatus::Usurped(hash));
self.is_finalized = true;
}
/// Extrinsic has been included in block with given hash.
pub fn in_block(&mut self, hash: BH, index: usize) {
self.send(TransactionStatus::InBlock((hash, index)));
}
/// Extrinsic has been finalized by a finality gadget.
pub fn finalized(&mut self, hash: BH, index: usize) {
self.send(TransactionStatus::Finalized((hash, index)));
self.is_finalized = true;
}
/// The block this extrinsic was included in has been retracted
pub fn finality_timeout(&mut self, hash: BH) {
self.send(TransactionStatus::FinalityTimeout(hash));
self.is_finalized = true;
}
/// The block this extrinsic was included in has been retracted
pub fn retracted(&mut self, hash: BH) {
self.send(TransactionStatus::Retracted(hash));
}
/// Extrinsic has been marked as invalid by the block builder.
pub fn invalid(&mut self) {
self.send(TransactionStatus::Invalid);
// we mark as finalized as there are no more notifications
self.is_finalized = true;
}
/// Transaction has been dropped from the pool because of the limit.
pub fn limit_enforced(&mut self) {
self.send(TransactionStatus::Dropped);
self.is_finalized = true;
}
/// Transaction has been dropped from the pool.
pub fn dropped(&mut self) {
self.send(TransactionStatus::Dropped);
self.is_finalized = true;
}
/// The extrinsic has been broadcast to the given peers.
pub fn broadcast(&mut self, peers: Vec<String>) {
self.send(TransactionStatus::Broadcast(peers))
}
/// Returns true if there are no more listeners for this extrinsic, or it was finalized.
pub fn is_done(&self) -> bool {
self.is_finalized || self.receivers.is_empty()
}
fn send(&mut self, status: TransactionStatus<H, BH>) {
self.receivers.retain(|sender| sender.unbounded_send(status.clone()).is_ok())
}
}
@@ -0,0 +1,61 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! Bizinikiwi transaction pool implementation.
#![recursion_limit = "256"]
#![warn(missing_docs)]
#![warn(unused_extern_crates)]
mod builder;
mod common;
mod fork_aware_txpool;
mod graph;
mod single_state_txpool;
mod transaction_pool_wrapper;
use common::{api, enactment_state};
use std::sync::Arc;
pub use api::FullChainApi;
pub use builder::{Builder, TransactionPoolHandle, TransactionPoolOptions, TransactionPoolType};
pub use common::notification_future;
pub use fork_aware_txpool::{ForkAwareTxPool, ForkAwareTxPoolTask};
pub use graph::{
base_pool::{Limit as PoolLimit, TimedTransactionSource},
ChainApi, Options, Pool, ValidateTransactionPriority,
};
use single_state_txpool::prune_known_txs_for_block;
pub use single_state_txpool::{BasicPool, RevalidationType};
pub use transaction_pool_wrapper::TransactionPoolWrapper;
type BoxedReadyIterator<Hash, Data> = Box<
dyn pezsc_transaction_pool_api::ReadyTransactions<
Item = Arc<graph::base_pool::Transaction<Hash, Data>>,
> + Send,
>;
type ReadyIteratorFor<PoolApi> =
BoxedReadyIterator<graph::ExtrinsicHash<PoolApi>, graph::ExtrinsicFor<PoolApi>>;
/// Log target for transaction pool.
///
/// It can be used by other components for logging functionality strictly related to txpool (e.g.
/// importing transaction).
pub const LOG_TARGET: &str = "txpool";
const LOG_TARGET_STAT: &str = "txpoolstats";
@@ -0,0 +1,67 @@
// 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 single-state transaction pool.
use crate::common::metrics::{GenericMetricsLink, MetricsRegistrant};
use prometheus_endpoint::{register, Counter, PrometheusError, Registry, U64};
pub type MetricsLink = GenericMetricsLink<Metrics>;
/// Transaction pool Prometheus metrics.
pub struct Metrics {
pub submitted_transactions: Counter<U64>,
pub validations_invalid: Counter<U64>,
pub block_transactions_pruned: Counter<U64>,
pub block_transactions_resubmitted: Counter<U64>,
}
impl MetricsRegistrant for Metrics {
fn register(registry: &Registry) -> Result<Box<Self>, PrometheusError> {
Ok(Box::from(Self {
submitted_transactions: register(
Counter::new(
"bizinikiwi_sub_txpool_submitted_transactions",
"Total number of transactions submitted",
)?,
registry,
)?,
validations_invalid: register(
Counter::new(
"bizinikiwi_sub_txpool_validations_invalid",
"Total number of transactions that were removed from the pool as invalid",
)?,
registry,
)?,
block_transactions_pruned: register(
Counter::new(
"bizinikiwi_sub_txpool_block_transactions_pruned",
"Total number of transactions that was requested to be pruned by block events",
)?,
registry,
)?,
block_transactions_resubmitted: register(
Counter::new(
"bizinikiwi_sub_txpool_block_transactions_resubmitted",
"Total number of transactions that was requested to be resubmitted by block events",
)?,
registry,
)?,
}))
}
}
@@ -0,0 +1,26 @@
// 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 single state transaction pool implementation.
mod metrics;
mod revalidation;
pub(crate) mod single_state_txpool;
pub(crate) use single_state_txpool::prune_known_txs_for_block;
pub use single_state_txpool::{BasicPool, RevalidationType};
@@ -0,0 +1,492 @@
// 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/>.
//! Pool periodic revalidation.
use crate::graph::{
BlockHash, ChainApi, ExtrinsicHash, ValidateTransactionPriority, ValidatedTransaction,
};
use futures::prelude::*;
use indexmap::IndexMap;
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
use pezsp_runtime::{
generic::BlockId, traits::SaturatedConversion, transaction_validity::TransactionValidityError,
};
use std::{
collections::{BTreeMap, HashMap, HashSet},
pin::Pin,
sync::Arc,
time::Duration,
};
use tracing::{debug, trace, warn};
const BACKGROUND_REVALIDATION_INTERVAL: Duration = Duration::from_millis(200);
const MIN_BACKGROUND_REVALIDATION_BATCH_SIZE: usize = 20;
const LOG_TARGET: &str = "txpool::revalidation";
type Pool<Api> = crate::graph::Pool<Api, ()>;
/// Payload from queue to worker.
struct WorkerPayload<Api: ChainApi> {
at: BlockHash<Api>,
transactions: Vec<ExtrinsicHash<Api>>,
}
/// Async revalidation worker.
///
/// Implements future and can be spawned in place or in background.
struct RevalidationWorker<Api: ChainApi> {
api: Arc<Api>,
pool: Arc<Pool<Api>>,
best_block: BlockHash<Api>,
block_ordered: BTreeMap<BlockHash<Api>, HashSet<ExtrinsicHash<Api>>>,
members: HashMap<ExtrinsicHash<Api>, BlockHash<Api>>,
}
impl<Api: ChainApi> Unpin for RevalidationWorker<Api> {}
/// Revalidate batch of transaction.
///
/// Each transaction is validated against chain, and invalid are
/// removed from the `pool`, while valid are resubmitted.
async fn batch_revalidate<Api: ChainApi>(
pool: Arc<Pool<Api>>,
api: Arc<Api>,
at: BlockHash<Api>,
batch: impl IntoIterator<Item = ExtrinsicHash<Api>>,
) {
// This conversion should work. Otherwise, for unknown block the revalidation shall be skipped,
// all the transactions will be kept in the validated pool, and can be scheduled for
// revalidation with the next request.
let block_number = match api.block_id_to_number(&BlockId::Hash(at)) {
Ok(Some(n)) => n,
Ok(None) => {
trace!(
target: LOG_TARGET,
?at,
"Revalidation skipped: could not get block number"
);
return;
},
Err(error) => {
trace!(
target: LOG_TARGET,
?at,
?error,
"Revalidation skipped."
);
return;
},
};
let mut invalid_hashes = Vec::new();
let mut revalidated = IndexMap::new();
let validation_results = futures::future::join_all(batch.into_iter().filter_map(|ext_hash| {
pool.validated_pool().ready_by_hash(&ext_hash).map(|ext| {
api.validate_transaction(
at,
ext.source.clone().into(),
ext.data.clone(),
ValidateTransactionPriority::Submitted,
)
.map(move |validation_result| (validation_result, ext_hash, ext))
})
}))
.await;
for (validation_result, tx_hash, ext) in validation_results {
match validation_result {
Ok(Err(TransactionValidityError::Invalid(error))) => {
trace!(
target: LOG_TARGET,
?tx_hash,
?error,
"Revalidation: invalid."
);
invalid_hashes.push(tx_hash);
},
Ok(Err(TransactionValidityError::Unknown(error))) => {
// skipping unknown, they might be pushed by valid or invalid transaction
// when latter resubmitted.
trace!(
target: LOG_TARGET,
?tx_hash,
?error,
"Unknown during revalidation."
);
},
Ok(Ok(validity)) => {
revalidated.insert(
tx_hash,
ValidatedTransaction::valid_at(
block_number.saturated_into::<u64>(),
tx_hash,
ext.source.clone(),
ext.data.clone(),
api.hash_and_length(&ext.data).1,
validity,
),
);
},
Err(error) => {
trace!(
target: LOG_TARGET,
?tx_hash,
?error,
"Removing due to error during revalidation."
);
invalid_hashes.push(tx_hash);
},
}
}
pool.validated_pool().remove_invalid(&invalid_hashes);
if revalidated.len() > 0 {
pool.resubmit(revalidated);
}
}
impl<Api: ChainApi> RevalidationWorker<Api> {
fn new(api: Arc<Api>, pool: Arc<Pool<Api>>, best_block: BlockHash<Api>) -> Self {
Self {
api,
pool,
best_block,
block_ordered: Default::default(),
members: Default::default(),
}
}
fn prepare_batch(&mut self) -> Vec<ExtrinsicHash<Api>> {
let mut queued_exts = Vec::new();
let mut left =
std::cmp::max(MIN_BACKGROUND_REVALIDATION_BATCH_SIZE, self.members.len() / 4);
// Take maximum of count transaction by order
// which they got into the pool
while left > 0 {
let first_block = match self.block_ordered.keys().next().cloned() {
Some(bn) => bn,
None => break,
};
let mut block_drained = false;
if let Some(extrinsics) = self.block_ordered.get_mut(&first_block) {
let to_queue = extrinsics.iter().take(left).cloned().collect::<Vec<_>>();
if to_queue.len() == extrinsics.len() {
block_drained = true;
} else {
for xt in &to_queue {
extrinsics.remove(xt);
}
}
left -= to_queue.len();
queued_exts.extend(to_queue);
}
if block_drained {
self.block_ordered.remove(&first_block);
}
}
for hash in queued_exts.iter() {
self.members.remove(hash);
}
queued_exts
}
fn len(&self) -> usize {
self.block_ordered.iter().map(|b| b.1.len()).sum()
}
fn push(&mut self, worker_payload: WorkerPayload<Api>) {
// we don't add something that already scheduled for revalidation
let transactions = worker_payload.transactions;
let block_number = worker_payload.at;
for tx_hash in transactions {
// we don't add something that already scheduled for revalidation
if self.members.contains_key(&tx_hash) {
trace!(
target: LOG_TARGET,
?tx_hash,
"Skipped adding for revalidation: Already there."
);
continue;
}
self.block_ordered
.entry(block_number)
.and_modify(|value| {
value.insert(tx_hash);
})
.or_insert_with(|| {
let mut bt = HashSet::new();
bt.insert(tx_hash);
bt
});
self.members.insert(tx_hash, block_number);
}
}
/// Background worker main loop.
///
/// It does two things: periodically tries to process some transactions
/// from the queue and also accepts messages to enqueue some more
/// transactions from the pool.
pub async fn run(
mut self,
from_queue: TracingUnboundedReceiver<WorkerPayload<Api>>,
interval: Duration,
) {
let interval_fut = futures_timer::Delay::new(interval);
let from_queue = from_queue.fuse();
futures::pin_mut!(interval_fut, from_queue);
let this = &mut self;
loop {
futures::select! {
// Using `fuse()` in here is okay, because we reset the interval when it has fired.
_ = (&mut interval_fut).fuse() => {
let next_batch = this.prepare_batch();
let batch_len = next_batch.len();
batch_revalidate(this.pool.clone(), this.api.clone(), this.best_block, next_batch).await;
if batch_len > 0 || this.len() > 0 {
trace!(
target: LOG_TARGET,
batch_len,
queue_len = this.len(),
"Revalidated transactions. Left in the queue for revalidation."
);
}
interval_fut.reset(interval);
},
workload = from_queue.next() => {
match workload {
Some(worker_payload) => {
this.best_block = worker_payload.at;
this.push(worker_payload);
if this.members.len() > 0 {
trace!(
target: LOG_TARGET,
at = ?this.best_block,
transactions = ?this.members,
"Updated revalidation queue."
);
}
continue;
},
// R.I.P. worker!
None => break,
}
}
}
}
}
}
/// Revalidation queue.
///
/// Can be configured background (`new_background`)
/// or immediate (just `new`).
pub struct RevalidationQueue<Api: ChainApi> {
pool: Arc<Pool<Api>>,
api: Arc<Api>,
background: Option<TracingUnboundedSender<WorkerPayload<Api>>>,
}
impl<Api: ChainApi> RevalidationQueue<Api>
where
Api: 'static,
{
/// New revalidation queue without background worker.
pub fn new(api: Arc<Api>, pool: Arc<Pool<Api>>) -> Self {
Self { api, pool, background: None }
}
/// New revalidation queue with background worker.
pub fn new_with_interval(
api: Arc<Api>,
pool: Arc<Pool<Api>>,
interval: Duration,
best_block: BlockHash<Api>,
) -> (Self, Pin<Box<dyn Future<Output = ()> + Send>>) {
let (to_worker, from_queue) = tracing_unbounded("mpsc_revalidation_queue", 100_000);
let worker = RevalidationWorker::new(api.clone(), pool.clone(), best_block);
let queue = Self { api, pool, background: Some(to_worker) };
(queue, worker.run(from_queue, interval).boxed())
}
/// New revalidation queue with background worker.
pub fn new_background(
api: Arc<Api>,
pool: Arc<Pool<Api>>,
best_block: BlockHash<Api>,
) -> (Self, Pin<Box<dyn Future<Output = ()> + Send>>) {
Self::new_with_interval(api, pool, BACKGROUND_REVALIDATION_INTERVAL, best_block)
}
/// Queue some transaction for later revalidation.
///
/// If queue configured with background worker, this will return immediately.
/// If queue configured without background worker, this will resolve after
/// revalidation is actually done.
pub async fn revalidate_later(
&self,
at: BlockHash<Api>,
transactions: Vec<ExtrinsicHash<Api>>,
) {
if transactions.len() > 0 {
debug!(
target: LOG_TARGET,
transaction_count = transactions.len(),
"Sent transactions to revalidation queue."
);
}
if let Some(ref to_worker) = self.background {
if let Err(error) = to_worker.unbounded_send(WorkerPayload { at, transactions }) {
warn!(
target: LOG_TARGET,
?error,
"Failed to update background worker."
);
}
} else {
debug!(
target: LOG_TARGET,
"Batch revalidate direct call."
);
let pool = self.pool.clone();
let api = self.api.clone();
batch_revalidate(pool, api, at, transactions).await
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
common::tests::{uxt, TestApi},
graph::Pool,
TimedTransactionSource,
};
use futures::executor::block_on;
use bizinikiwi_test_runtime::{AccountId, Transfer, H256};
use bizinikiwi_test_runtime_client::Sr25519Keyring::{Alice, Bob};
#[test]
fn revalidation_queue_works() {
let api = Arc::new(TestApi::default());
let pool = Arc::new(Pool::new_with_staticly_sized_rotator(
Default::default(),
true.into(),
api.clone(),
));
let queue = Arc::new(RevalidationQueue::new(api.clone(), pool.clone()));
let uxt = uxt(Transfer {
from: Alice.into(),
to: AccountId::from_h256(H256::from_low_u64_be(2)),
amount: 5,
nonce: 0,
});
let han_of_block0 = api.expect_hash_and_number(0);
let uxt_hash = block_on(pool.submit_one(
&han_of_block0,
TimedTransactionSource::new_external(false),
uxt.clone().into(),
))
.expect("Should be valid")
.hash();
block_on(queue.revalidate_later(han_of_block0.hash, vec![uxt_hash]));
// revalidated in sync offload 2nd time
assert_eq!(api.validation_requests().len(), 2);
// number of ready
assert_eq!(pool.validated_pool().status().ready, 1);
}
#[test]
fn revalidation_queue_skips_revalidation_for_unknown_block_hash() {
let api = Arc::new(TestApi::default());
let pool = Arc::new(Pool::new_with_staticly_sized_rotator(
Default::default(),
true.into(),
api.clone(),
));
let queue = Arc::new(RevalidationQueue::new(api.clone(), pool.clone()));
let uxt0 = uxt(Transfer {
from: Alice.into(),
to: AccountId::from_h256(H256::from_low_u64_be(2)),
amount: 5,
nonce: 0,
});
let uxt1 = uxt(Transfer {
from: Bob.into(),
to: AccountId::from_h256(H256::from_low_u64_be(2)),
amount: 4,
nonce: 1,
});
let han_of_block0 = api.expect_hash_and_number(0);
let unknown_block = H256::repeat_byte(0x13);
let source = TimedTransactionSource::new_external(false);
let uxt_hashes = block_on(pool.submit_at(
&han_of_block0,
vec![(source.clone(), uxt0.into()), (source, uxt1.into())],
ValidateTransactionPriority::Submitted,
))
.into_iter()
.map(|r| r.expect("Should be valid").hash())
.collect::<Vec<_>>();
assert_eq!(api.validation_requests().len(), 2);
assert_eq!(pool.validated_pool().status().ready, 2);
// revalidation works fine for block 0:
block_on(queue.revalidate_later(han_of_block0.hash, uxt_hashes.clone()));
assert_eq!(api.validation_requests().len(), 4);
assert_eq!(pool.validated_pool().status().ready, 2);
// revalidation shall be skipped for unknown block:
block_on(queue.revalidate_later(unknown_block, uxt_hashes));
// no revalidation shall be done
assert_eq!(api.validation_requests().len(), 4);
// number of ready shall not change
assert_eq!(pool.validated_pool().status().ready, 2);
}
}
@@ -0,0 +1,826 @@
// 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 super::{metrics::MetricsLink as PrometheusMetrics, revalidation};
pub use crate::{
api::FullChainApi,
graph::{ChainApi, ValidatedTransaction},
};
use crate::{
common::{
enactment_state::{EnactmentAction, EnactmentState},
error,
tracing_log_xt::log_xt_trace,
},
graph::{
self, base_pool::TimedTransactionSource, EventHandler, ExtrinsicHash, IsValidator,
RawExtrinsicFor,
},
ReadyIteratorFor, ValidateTransactionPriority, LOG_TARGET,
};
use async_trait::async_trait;
use futures::{channel::oneshot, future, prelude::*, Future, FutureExt};
use parking_lot::Mutex;
use prometheus_endpoint::Registry as PrometheusRegistry;
use pezsc_transaction_pool_api::{
error::Error as TxPoolError, ChainEvent, ImportNotificationStream, MaintainedTransactionPool,
PoolStatus, TransactionFor, TransactionPool, TransactionSource, TransactionStatusStreamFor,
TxHash, TxInvalidityReportMap,
};
use pezsp_blockchain::{HashAndNumber, TreeRoute};
use pezsp_core::traits::SpawnEssentialNamed;
use pezsp_runtime::{
generic::BlockId,
traits::{
AtLeast32Bit, Block as BlockT, Header as HeaderT, NumberFor, SaturatedConversion, Zero,
},
transaction_validity::{TransactionTag as Tag, TransactionValidityError},
};
use std::{
collections::{HashMap, HashSet},
pin::Pin,
sync::Arc,
time::Instant,
};
use tokio::select;
use tracing::{trace, warn};
/// Basic implementation of transaction pool that can be customized by providing PoolApi.
pub struct BasicPool<PoolApi, Block>
where
Block: BlockT,
PoolApi: graph::ChainApi<Block = Block>,
{
pool: Arc<graph::Pool<PoolApi, ()>>,
api: Arc<PoolApi>,
revalidation_strategy: Arc<Mutex<RevalidationStrategy<NumberFor<Block>>>>,
revalidation_queue: Arc<revalidation::RevalidationQueue<PoolApi>>,
ready_poll: Arc<Mutex<ReadyPoll<ReadyIteratorFor<PoolApi>, Block>>>,
metrics: PrometheusMetrics,
enactment_state: Arc<Mutex<EnactmentState<Block>>>,
}
struct ReadyPoll<T, Block: BlockT> {
updated_at: NumberFor<Block>,
pollers: Vec<(NumberFor<Block>, oneshot::Sender<T>)>,
}
impl<T, Block: BlockT> Default for ReadyPoll<T, Block> {
fn default() -> Self {
Self { updated_at: NumberFor::<Block>::zero(), pollers: Default::default() }
}
}
impl<T, Block: BlockT> ReadyPoll<T, Block> {
fn new(best_block_number: NumberFor<Block>) -> Self {
Self { updated_at: best_block_number, pollers: Default::default() }
}
fn trigger(&mut self, number: NumberFor<Block>, iterator_factory: impl Fn() -> T) {
self.updated_at = number;
let mut idx = 0;
while idx < self.pollers.len() {
if self.pollers[idx].0 <= number {
let poller_sender = self.pollers.swap_remove(idx);
trace!(
target: LOG_TARGET,
?number,
"Sending ready signal."
);
let _ = poller_sender.1.send(iterator_factory());
} else {
idx += 1;
}
}
}
fn add(&mut self, number: NumberFor<Block>) -> oneshot::Receiver<T> {
let (sender, receiver) = oneshot::channel();
self.pollers.push((number, sender));
receiver
}
fn updated_at(&self) -> NumberFor<Block> {
self.updated_at
}
}
/// Type of revalidation.
pub enum RevalidationType {
/// Light revalidation type.
///
/// During maintenance, transaction pool makes periodic revalidation
/// of all transactions depending on number of blocks or time passed.
/// Also this kind of revalidation does not resubmit transactions from
/// retracted blocks, since it is too expensive.
Light,
/// Full revalidation type.
///
/// During maintenance, transaction pool revalidates some fixed amount of
/// transactions from the pool of valid transactions.
Full,
}
impl<PoolApi, Block> BasicPool<PoolApi, Block>
where
Block: BlockT,
PoolApi: graph::ChainApi<Block = Block> + 'static,
{
/// Create new basic transaction pool with provided api, for tests.
pub fn new_test(
pool_api: Arc<PoolApi>,
best_block_hash: Block::Hash,
finalized_hash: Block::Hash,
options: graph::Options,
) -> (Self, Pin<Box<dyn Future<Output = ()> + Send>>) {
let pool = Arc::new(graph::Pool::new_with_staticly_sized_rotator(
options,
true.into(),
pool_api.clone(),
));
let (revalidation_queue, background_task) = revalidation::RevalidationQueue::new_background(
pool_api.clone(),
pool.clone(),
finalized_hash,
);
(
Self {
api: pool_api,
pool,
revalidation_queue: Arc::new(revalidation_queue),
revalidation_strategy: Arc::new(Mutex::new(RevalidationStrategy::Always)),
ready_poll: Default::default(),
metrics: Default::default(),
enactment_state: Arc::new(Mutex::new(EnactmentState::new(
best_block_hash,
finalized_hash,
))),
},
background_task,
)
}
/// Create new basic transaction pool with provided api and custom
/// revalidation type.
pub fn with_revalidation_type(
options: graph::Options,
is_validator: IsValidator,
pool_api: Arc<PoolApi>,
prometheus: Option<&PrometheusRegistry>,
revalidation_type: RevalidationType,
spawner: impl SpawnEssentialNamed,
best_block_number: NumberFor<Block>,
best_block_hash: Block::Hash,
finalized_hash: Block::Hash,
) -> Self {
let pool = Arc::new(graph::Pool::new_with_staticly_sized_rotator(
options,
is_validator,
pool_api.clone(),
));
let (revalidation_queue, background_task) = match revalidation_type {
RevalidationType::Light =>
(revalidation::RevalidationQueue::new(pool_api.clone(), pool.clone()), None),
RevalidationType::Full => {
let (queue, background) = revalidation::RevalidationQueue::new_background(
pool_api.clone(),
pool.clone(),
finalized_hash,
);
(queue, Some(background))
},
};
if let Some(background_task) = background_task {
spawner.spawn_essential("txpool-background", Some("transaction-pool"), background_task);
}
Self {
api: pool_api,
pool,
revalidation_queue: Arc::new(revalidation_queue),
revalidation_strategy: Arc::new(Mutex::new(match revalidation_type {
RevalidationType::Light =>
RevalidationStrategy::Light(RevalidationStatus::NotScheduled),
RevalidationType::Full => RevalidationStrategy::Always,
})),
ready_poll: Arc::new(Mutex::new(ReadyPoll::new(best_block_number))),
metrics: PrometheusMetrics::new(prometheus),
enactment_state: Arc::new(Mutex::new(EnactmentState::new(
best_block_hash,
finalized_hash,
))),
}
}
/// Gets shared reference to the underlying pool.
pub fn pool(&self) -> &Arc<graph::Pool<PoolApi, ()>> {
&self.pool
}
/// Get access to the underlying api
pub fn api(&self) -> &PoolApi {
&self.api
}
async fn ready_at_with_timeout_internal(
&self,
at: Block::Hash,
timeout: std::time::Duration,
) -> ReadyIteratorFor<PoolApi> {
select! {
ready = self.ready_at(at)=> ready,
_ = futures_timer::Delay::new(timeout)=> self.ready()
}
}
}
#[async_trait]
impl<PoolApi, Block> TransactionPool for BasicPool<PoolApi, Block>
where
Block: BlockT,
PoolApi: 'static + graph::ChainApi<Block = Block>,
{
type Block = PoolApi::Block;
type Hash = graph::ExtrinsicHash<PoolApi>;
type InPoolTransaction =
graph::base_pool::Transaction<graph::ExtrinsicHash<PoolApi>, graph::ExtrinsicFor<PoolApi>>;
type Error = PoolApi::Error;
async fn submit_at(
&self,
at: <Self::Block as BlockT>::Hash,
source: TransactionSource,
xts: Vec<TransactionFor<Self>>,
) -> Result<Vec<Result<TxHash<Self>, Self::Error>>, Self::Error> {
let pool = self.pool.clone();
let xts = xts
.into_iter()
.map(|xt| {
(TimedTransactionSource::from_transaction_source(source, false), Arc::from(xt))
})
.collect::<Vec<_>>();
self.metrics
.report(|metrics| metrics.submitted_transactions.inc_by(xts.len() as u64));
let number = self.api.resolve_block_number(at);
let at = HashAndNumber { hash: at, number: number? };
Ok(pool
.submit_at(&at, xts, ValidateTransactionPriority::Submitted)
.await
.into_iter()
.map(|result| result.map(|outcome| outcome.hash()))
.collect())
}
async fn submit_one(
&self,
at: <Self::Block as BlockT>::Hash,
source: TransactionSource,
xt: TransactionFor<Self>,
) -> Result<TxHash<Self>, Self::Error> {
let pool = self.pool.clone();
let xt = Arc::from(xt);
self.metrics.report(|metrics| metrics.submitted_transactions.inc());
let number = self.api.resolve_block_number(at);
let at = HashAndNumber { hash: at, number: number? };
pool.submit_one(&at, TimedTransactionSource::from_transaction_source(source, false), xt)
.await
.map(|outcome| outcome.hash())
}
async fn submit_and_watch(
&self,
at: <Self::Block as BlockT>::Hash,
source: TransactionSource,
xt: TransactionFor<Self>,
) -> Result<Pin<Box<TransactionStatusStreamFor<Self>>>, Self::Error> {
let pool = self.pool.clone();
let xt = Arc::from(xt);
self.metrics.report(|metrics| metrics.submitted_transactions.inc());
let number = self.api.resolve_block_number(at);
let at = HashAndNumber { hash: at, number: number? };
pool.submit_and_watch(
&at,
TimedTransactionSource::from_transaction_source(source, false),
xt,
)
.await
.map(|mut outcome| outcome.expect_watcher().into_stream().boxed())
}
async fn report_invalid(
&self,
_at: Option<<Self::Block as BlockT>::Hash>,
invalid_tx_errors: TxInvalidityReportMap<TxHash<Self>>,
) -> Vec<Arc<Self::InPoolTransaction>> {
let hashes = invalid_tx_errors.keys().map(|h| *h).collect::<Vec<_>>();
let removed = self.pool.validated_pool().remove_invalid(&hashes);
self.metrics
.report(|metrics| metrics.validations_invalid.inc_by(removed.len() as u64));
removed
}
fn status(&self) -> PoolStatus {
self.pool.validated_pool().status()
}
fn import_notification_stream(&self) -> ImportNotificationStream<TxHash<Self>> {
self.pool.validated_pool().import_notification_stream()
}
fn hash_of(&self, xt: &TransactionFor<Self>) -> TxHash<Self> {
self.pool.hash_of(xt)
}
fn on_broadcasted(&self, propagations: HashMap<TxHash<Self>, Vec<String>>) {
self.pool.validated_pool().on_broadcasted(propagations)
}
fn ready_transaction(&self, hash: &TxHash<Self>) -> Option<Arc<Self::InPoolTransaction>> {
self.pool.validated_pool().ready_by_hash(hash)
}
async fn ready_at(&self, at: <Self::Block as BlockT>::Hash) -> ReadyIteratorFor<PoolApi> {
let Ok(at) = self.api.resolve_block_number(at) else {
return Box::new(std::iter::empty()) as Box<_>;
};
let status = self.status();
// If there are no transactions in the pool, it is fine to return early.
//
// There could be transaction being added because of some re-org happening at the relevant
// block, but this is relative unlikely.
if status.ready == 0 && status.future == 0 {
return Box::new(std::iter::empty()) as Box<_>;
}
if self.ready_poll.lock().updated_at() >= at {
trace!(
target: LOG_TARGET,
?at,
"Transaction pool already processed block."
);
let iterator: ReadyIteratorFor<PoolApi> = Box::new(self.pool.validated_pool().ready());
return iterator;
}
let result = self.ready_poll.lock().add(at).map(|received| {
received.unwrap_or_else(|error| {
warn!(target: LOG_TARGET, ?error, "Error receiving pending set.");
Box::new(std::iter::empty())
})
});
result.await
}
fn ready(&self) -> ReadyIteratorFor<PoolApi> {
Box::new(self.pool.validated_pool().ready())
}
fn futures(&self) -> Vec<Self::InPoolTransaction> {
let pool = self.pool.validated_pool().pool.read();
pool.futures().cloned().collect::<Vec<_>>()
}
async fn ready_at_with_timeout(
&self,
at: <Self::Block as BlockT>::Hash,
timeout: std::time::Duration,
) -> ReadyIteratorFor<PoolApi> {
self.ready_at_with_timeout_internal(at, timeout).await
}
}
impl<Block, Client> BasicPool<FullChainApi<Client, Block>, Block>
where
Block: BlockT,
Client: pezsp_api::ProvideRuntimeApi<Block>
+ pezsc_client_api::BlockBackend<Block>
+ pezsc_client_api::blockchain::HeaderBackend<Block>
+ pezsp_runtime::traits::BlockIdTo<Block>
+ pezsc_client_api::ExecutorProvider<Block>
+ pezsc_client_api::UsageProvider<Block>
+ pezsp_blockchain::HeaderMetadata<Block, Error = pezsp_blockchain::Error>
+ Send
+ Sync
+ 'static,
Client::Api: pezsp_transaction_pool::runtime_api::TaggedTransactionQueue<Block>,
{
/// Create new basic transaction pool for a full node with the provided api.
pub fn new_full(
options: graph::Options,
is_validator: IsValidator,
prometheus: Option<&PrometheusRegistry>,
spawner: impl SpawnEssentialNamed,
client: Arc<Client>,
) -> Self {
let pool_api = Arc::new(FullChainApi::new(client.clone(), prometheus, &spawner));
let pool = Self::with_revalidation_type(
options,
is_validator,
pool_api,
prometheus,
RevalidationType::Full,
spawner,
client.usage_info().chain.best_number,
client.usage_info().chain.best_hash,
client.usage_info().chain.finalized_hash,
);
pool
}
}
impl<Block, Client> pezsc_transaction_pool_api::LocalTransactionPool
for BasicPool<FullChainApi<Client, Block>, Block>
where
Block: BlockT,
Client: pezsp_api::ProvideRuntimeApi<Block>
+ pezsc_client_api::BlockBackend<Block>
+ pezsc_client_api::blockchain::HeaderBackend<Block>
+ pezsp_runtime::traits::BlockIdTo<Block>
+ pezsp_blockchain::HeaderMetadata<Block, Error = pezsp_blockchain::Error>,
Client: Send + Sync + 'static,
Client::Api: pezsp_transaction_pool::runtime_api::TaggedTransactionQueue<Block>,
{
type Block = Block;
type Hash = graph::ExtrinsicHash<FullChainApi<Client, Block>>;
type Error = <FullChainApi<Client, Block> as graph::ChainApi>::Error;
fn submit_local(
&self,
at: Block::Hash,
xt: pezsc_transaction_pool_api::LocalTransactionFor<Self>,
) -> Result<Self::Hash, Self::Error> {
let validity = self
.api
.validate_transaction_blocking(at, TransactionSource::Local, Arc::from(xt.clone()))?
.map_err(|e| {
Self::Error::Pool(match e {
TransactionValidityError::Invalid(i) => TxPoolError::InvalidTransaction(i),
TransactionValidityError::Unknown(u) => TxPoolError::UnknownTransaction(u),
})
})?;
let (hash, bytes) = self.pool.validated_pool().api().hash_and_length(&xt);
let block_number = self
.api
.block_id_to_number(&BlockId::hash(at))?
.ok_or_else(|| error::Error::BlockIdConversion(format!("{:?}", at)))?;
let validated = ValidatedTransaction::valid_at(
block_number.saturated_into::<u64>(),
hash,
TimedTransactionSource::new_local(false),
Arc::from(xt),
bytes,
validity,
);
self.pool
.validated_pool()
.submit(vec![validated])
.remove(0)
.map(|outcome| outcome.hash())
}
}
#[cfg_attr(test, derive(Debug))]
enum RevalidationStatus<N> {
/// The revalidation has never been completed.
NotScheduled,
/// The revalidation is scheduled.
Scheduled(Option<Instant>, Option<N>),
/// The revalidation is in progress.
InProgress,
}
enum RevalidationStrategy<N> {
Always,
Light(RevalidationStatus<N>),
}
struct RevalidationAction {
revalidate: bool,
resubmit: bool,
}
impl<N: Clone + Copy + AtLeast32Bit> RevalidationStrategy<N> {
pub fn clear(&mut self) {
if let Self::Light(status) = self {
status.clear()
}
}
pub fn next(
&mut self,
block: N,
revalidate_time_period: Option<std::time::Duration>,
revalidate_block_period: Option<N>,
) -> RevalidationAction {
match self {
Self::Light(status) => RevalidationAction {
revalidate: status.next_required(
block,
revalidate_time_period,
revalidate_block_period,
),
resubmit: false,
},
Self::Always => RevalidationAction { revalidate: true, resubmit: true },
}
}
}
impl<N: Clone + Copy + AtLeast32Bit> RevalidationStatus<N> {
/// Called when revalidation is completed.
pub fn clear(&mut self) {
*self = Self::NotScheduled;
}
/// Returns true if revalidation is required.
pub fn next_required(
&mut self,
block: N,
revalidate_time_period: Option<std::time::Duration>,
revalidate_block_period: Option<N>,
) -> bool {
match *self {
Self::NotScheduled => {
*self = Self::Scheduled(
revalidate_time_period.map(|period| Instant::now() + period),
revalidate_block_period.map(|period| block + period),
);
false
},
Self::Scheduled(revalidate_at_time, revalidate_at_block) => {
let is_required =
revalidate_at_time.map(|at| Instant::now() >= at).unwrap_or(false) ||
revalidate_at_block.map(|at| block >= at).unwrap_or(false);
if is_required {
*self = Self::InProgress;
}
is_required
},
Self::InProgress => false,
}
}
}
/// Prune the known txs from the given pool for the given block.
///
/// Returns the hashes of all transactions included in given block.
pub async fn prune_known_txs_for_block<
Block: BlockT,
Api: graph::ChainApi<Block = Block>,
L: EventHandler<Api>,
>(
at: &HashAndNumber<Block>,
api: &Api,
pool: &graph::Pool<Api, L>,
extrinsics: Option<Vec<RawExtrinsicFor<Api>>>,
known_provides_tags: Option<Arc<HashMap<ExtrinsicHash<Api>, Vec<Tag>>>>,
) -> Vec<ExtrinsicHash<Api>> {
let extrinsics = match extrinsics {
Some(xts) => xts,
None => api
.block_body(at.hash)
.await
.unwrap_or_else(|error| {
warn!(target: LOG_TARGET, ?error, "Prune known transactions: error request.");
None
})
.unwrap_or_default(),
};
let hashes = extrinsics.iter().map(|tx| pool.hash_of(tx)).collect::<Vec<_>>();
let header = match api.block_header(at.hash) {
Ok(Some(h)) => h,
Ok(None) => {
trace!(target: LOG_TARGET, hash = ?at.hash, "Could not find header.");
return hashes;
},
Err(error) => {
trace!(target: LOG_TARGET, hash = ?at.hash, ?error, "Error retrieving header.");
return hashes;
},
};
log_xt_trace!(target: LOG_TARGET, &hashes, "Pruning transaction.");
pool.prune(at, *header.parent_hash(), &extrinsics, known_provides_tags).await;
hashes
}
impl<PoolApi, Block> BasicPool<PoolApi, Block>
where
Block: BlockT,
PoolApi: 'static + graph::ChainApi<Block = Block>,
{
/// Handles enactment and retraction of blocks, prunes stale transactions
/// (that have already been enacted) and resubmits transactions that were
/// retracted.
async fn handle_enactment(&self, tree_route: TreeRoute<Block>) {
trace!(target: LOG_TARGET, ?tree_route, "handle_enactment tree_route.");
let pool = self.pool.clone();
let api = self.api.clone();
let hash_and_number = match tree_route.last() {
Some(hash_and_number) => hash_and_number,
None => {
warn!(target: LOG_TARGET, ?tree_route, "Skipping ChainEvent - no last block in tree route.");
return;
},
};
let next_action = self.revalidation_strategy.lock().next(
hash_and_number.number,
Some(std::time::Duration::from_secs(60)),
Some(20u32.into()),
);
// We keep track of everything we prune so that later we won't add
// transactions with those hashes from the retracted blocks.
let mut pruned_log = HashSet::<ExtrinsicHash<PoolApi>>::new();
// If there is a tree route, we use this to prune known tx based on the enacted
// blocks. Before pruning enacted transactions, we inform the listeners about
// retracted blocks and their transactions. This order is important, because
// if we enact and retract the same transaction at the same time, we want to
// send first the retract and then the prune event.
for retracted in tree_route.retracted() {
// notify txs awaiting finality that it has been retracted
pool.validated_pool().on_block_retracted(retracted.hash);
}
future::join_all(
tree_route
.enacted()
.iter()
.map(|h| prune_known_txs_for_block(h, &*api, &*pool, None, None)),
)
.await
.into_iter()
.for_each(|enacted_log| {
pruned_log.extend(enacted_log);
});
self.metrics
.report(|metrics| metrics.block_transactions_pruned.inc_by(pruned_log.len() as u64));
if next_action.resubmit {
let mut resubmit_transactions = Vec::new();
for retracted in tree_route.retracted() {
let hash = retracted.hash;
let block_transactions = api
.block_body(hash)
.await
.unwrap_or_else(|error| {
warn!(target: LOG_TARGET, ?error, "Failed to fetch block body.");
None
})
.unwrap_or_default()
.into_iter();
let mut resubmitted_to_report = 0;
resubmit_transactions.extend(
//todo: arctx - we need to get ref from somewhere
block_transactions.into_iter().map(Arc::from).filter_map(|tx| {
let tx_hash = pool.hash_of(&tx);
let contains = pruned_log.contains(&tx_hash);
// need to count all transactions, not just filtered, here
resubmitted_to_report += 1;
if !contains {
trace!(target: LOG_TARGET, ?tx_hash, ?hash, "Resubmitting from retracted block.");
Some((
// These transactions are coming from retracted blocks, we should
// simply consider them external.
TimedTransactionSource::new_external(false),
tx,
))
} else {
None
}
}),
);
self.metrics.report(|metrics| {
metrics.block_transactions_resubmitted.inc_by(resubmitted_to_report)
});
}
pool.resubmit_at(
&hash_and_number,
resubmit_transactions,
ValidateTransactionPriority::Submitted,
)
.await;
}
let extra_pool = pool.clone();
// After #5200 lands, this arguably might be moved to the
// handler of "all blocks notification".
self.ready_poll
.lock()
.trigger(hash_and_number.number, move || Box::new(extra_pool.validated_pool().ready()));
if next_action.revalidate {
let hashes = pool.validated_pool().ready().map(|tx| tx.hash).collect();
self.revalidation_queue.revalidate_later(hash_and_number.hash, hashes).await;
self.revalidation_strategy.lock().clear();
}
}
}
#[async_trait]
impl<PoolApi, Block> MaintainedTransactionPool for BasicPool<PoolApi, Block>
where
Block: BlockT,
PoolApi: 'static + graph::ChainApi<Block = Block>,
{
async fn maintain(&self, event: ChainEvent<Self::Block>) {
let prev_finalized_block = self.enactment_state.lock().recent_finalized_block();
let compute_tree_route = |from, to| -> Result<TreeRoute<Block>, String> {
match self.api.tree_route(from, to) {
Ok(tree_route) => Ok(tree_route),
Err(e) =>
return Err(format!(
"Error occurred while computing tree_route from {from:?} to {to:?}: {e}"
)),
}
};
let block_id_to_number =
|hash| self.api.block_id_to_number(&BlockId::Hash(hash)).map_err(|e| format!("{}", e));
let result =
self.enactment_state
.lock()
.update(&event, &compute_tree_route, &block_id_to_number);
match result {
Err(error) => {
trace!(target: LOG_TARGET, %error, "enactment state update");
self.enactment_state.lock().force_update(&event);
},
Ok(EnactmentAction::Skip) => return,
Ok(EnactmentAction::HandleFinalization) => {},
Ok(EnactmentAction::HandleEnactment(tree_route)) => {
self.handle_enactment(tree_route).await;
},
};
if let ChainEvent::Finalized { hash, tree_route } = event {
trace!(
target: LOG_TARGET,
?tree_route,
?prev_finalized_block,
"on-finalized enacted"
);
for hash in tree_route.iter().chain(std::iter::once(&hash)) {
if let Err(error) = self.pool.validated_pool().on_block_finalized(*hash).await {
warn!(
target: LOG_TARGET,
?hash,
?error,
"Error occurred while attempting to notify watchers about finalization"
);
}
}
}
}
}
@@ -0,0 +1,190 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! Transaction pool wrapper. Provides a type for wrapping object providing actual implementation of
//! transaction pool.
use crate::{
builder::FullClientTransactionPool,
graph::{base_pool::Transaction, ExtrinsicFor, ExtrinsicHash},
ChainApi, FullChainApi, ReadyIteratorFor,
};
use async_trait::async_trait;
use pezsc_transaction_pool_api::{
ChainEvent, ImportNotificationStream, LocalTransactionFor, LocalTransactionPool,
MaintainedTransactionPool, PoolStatus, ReadyTransactions, TransactionFor, TransactionPool,
TransactionSource, TransactionStatusStreamFor, TxHash, TxInvalidityReportMap,
};
use pezsp_runtime::traits::Block as BlockT;
use std::{collections::HashMap, pin::Pin, sync::Arc};
/// The wrapper for actual object providing implementation of TransactionPool.
///
/// This wraps actual implementation of the TransactionPool, e.g. fork-aware or single-state.
pub struct TransactionPoolWrapper<Block, Client>(
pub Box<dyn FullClientTransactionPool<Block, Client>>,
)
where
Block: BlockT,
Client: pezsp_api::ProvideRuntimeApi<Block>
+ pezsc_client_api::BlockBackend<Block>
+ pezsc_client_api::blockchain::HeaderBackend<Block>
+ pezsp_runtime::traits::BlockIdTo<Block>
+ pezsp_blockchain::HeaderMetadata<Block, Error = pezsp_blockchain::Error>
+ 'static,
Client::Api: pezsp_transaction_pool::runtime_api::TaggedTransactionQueue<Block>;
#[async_trait]
impl<Block, Client> TransactionPool for TransactionPoolWrapper<Block, Client>
where
Block: BlockT,
Client: pezsp_api::ProvideRuntimeApi<Block>
+ pezsc_client_api::BlockBackend<Block>
+ pezsc_client_api::blockchain::HeaderBackend<Block>
+ pezsp_runtime::traits::BlockIdTo<Block>
+ pezsp_blockchain::HeaderMetadata<Block, Error = pezsp_blockchain::Error>
+ 'static,
Client::Api: pezsp_transaction_pool::runtime_api::TaggedTransactionQueue<Block>,
{
type Block = Block;
type Hash = ExtrinsicHash<FullChainApi<Client, Block>>;
type InPoolTransaction = Transaction<
ExtrinsicHash<FullChainApi<Client, Block>>,
ExtrinsicFor<FullChainApi<Client, Block>>,
>;
type Error = <FullChainApi<Client, Block> as ChainApi>::Error;
async fn submit_at(
&self,
at: <Self::Block as BlockT>::Hash,
source: TransactionSource,
xts: Vec<TransactionFor<Self>>,
) -> Result<Vec<Result<TxHash<Self>, Self::Error>>, Self::Error> {
self.0.submit_at(at, source, xts).await
}
async fn submit_one(
&self,
at: <Self::Block as BlockT>::Hash,
source: TransactionSource,
xt: TransactionFor<Self>,
) -> Result<TxHash<Self>, Self::Error> {
self.0.submit_one(at, source, xt).await
}
async fn submit_and_watch(
&self,
at: <Self::Block as BlockT>::Hash,
source: TransactionSource,
xt: TransactionFor<Self>,
) -> Result<Pin<Box<TransactionStatusStreamFor<Self>>>, Self::Error> {
self.0.submit_and_watch(at, source, xt).await
}
async fn ready_at(
&self,
at: <Self::Block as BlockT>::Hash,
) -> ReadyIteratorFor<FullChainApi<Client, Block>> {
self.0.ready_at(at).await
}
fn ready(&self) -> Box<dyn ReadyTransactions<Item = Arc<Self::InPoolTransaction>> + Send> {
self.0.ready()
}
async fn report_invalid(
&self,
at: Option<<Self::Block as BlockT>::Hash>,
invalid_tx_errors: TxInvalidityReportMap<TxHash<Self>>,
) -> Vec<Arc<Self::InPoolTransaction>> {
self.0.report_invalid(at, invalid_tx_errors).await
}
fn futures(&self) -> Vec<Self::InPoolTransaction> {
self.0.futures()
}
fn status(&self) -> PoolStatus {
self.0.status()
}
fn import_notification_stream(&self) -> ImportNotificationStream<TxHash<Self>> {
self.0.import_notification_stream()
}
fn on_broadcasted(&self, propagations: HashMap<TxHash<Self>, Vec<String>>) {
self.0.on_broadcasted(propagations)
}
fn hash_of(&self, xt: &TransactionFor<Self>) -> TxHash<Self> {
self.0.hash_of(xt)
}
fn ready_transaction(&self, hash: &TxHash<Self>) -> Option<Arc<Self::InPoolTransaction>> {
self.0.ready_transaction(hash)
}
async fn ready_at_with_timeout(
&self,
at: <Self::Block as BlockT>::Hash,
timeout: std::time::Duration,
) -> ReadyIteratorFor<FullChainApi<Client, Block>> {
self.0.ready_at_with_timeout(at, timeout).await
}
}
#[async_trait]
impl<Block, Client> MaintainedTransactionPool for TransactionPoolWrapper<Block, Client>
where
Block: BlockT,
Client: pezsp_api::ProvideRuntimeApi<Block>
+ pezsc_client_api::BlockBackend<Block>
+ pezsc_client_api::blockchain::HeaderBackend<Block>
+ pezsp_runtime::traits::BlockIdTo<Block>
+ pezsp_blockchain::HeaderMetadata<Block, Error = pezsp_blockchain::Error>
+ 'static,
Client::Api: pezsp_transaction_pool::runtime_api::TaggedTransactionQueue<Block>,
{
async fn maintain(&self, event: ChainEvent<Self::Block>) {
self.0.maintain(event).await;
}
}
impl<Block, Client> LocalTransactionPool for TransactionPoolWrapper<Block, Client>
where
Block: BlockT,
Client: pezsp_api::ProvideRuntimeApi<Block>
+ pezsc_client_api::BlockBackend<Block>
+ pezsc_client_api::blockchain::HeaderBackend<Block>
+ pezsp_runtime::traits::BlockIdTo<Block>
+ pezsp_blockchain::HeaderMetadata<Block, Error = pezsp_blockchain::Error>
+ 'static,
Client::Api: pezsp_transaction_pool::runtime_api::TaggedTransactionQueue<Block>,
{
type Block = Block;
type Hash = ExtrinsicHash<FullChainApi<Client, Block>>;
type Error = <FullChainApi<Client, Block> as ChainApi>::Error;
fn submit_local(
&self,
at: <Self::Block as BlockT>::Hash,
xt: LocalTransactionFor<Self>,
) -> Result<Self::Hash, Self::Error> {
self.0.submit_local(at, xt)
}
}