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