feat: initialize Kurdistan SDK - independent fork of Polkadot SDK

This commit is contained in:
Kurdistan Tech Ministry committed 2025-12-13 15:44:15 +03:00
commit e4778b4576
6838 files changed
+1847450

No files matched your search

@@ -0,0 +1,86 @@
[package]
name = "sc-transaction-pool"
version = "28.0.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
description = "Substrate transaction pool implementation."
readme = "README.md"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[[bench]]
name = "basics"
harness = false
[dependencies]
async-trait = { workspace = true }
codec = { workspace = true, default-features = true }
futures = { workspace = true }
futures-timer = { workspace = true }
indexmap = { workspace = true }
itertools = { workspace = true }
linked-hash-map = { workspace = true }
parking_lot = { workspace = true, default-features = true }
prometheus-endpoint = { workspace = true, default-features = true }
sc-client-api = { workspace = true, default-features = true }
sc-transaction-pool-api = { workspace = true, default-features = true }
sc-utils = { workspace = true, default-features = true }
serde = { features = ["derive"], workspace = true, default-features = true }
sp-api = { workspace = true, default-features = true }
sp-blockchain = { workspace = true, default-features = true }
sp-core = { workspace = true, default-features = true }
sp-crypto-hashing = { workspace = true, default-features = true }
sp-runtime = { workspace = true, default-features = true }
sp-tracing = { workspace = true, default-features = true }
sp-transaction-pool = { workspace = true, default-features = true }
thiserror = { workspace = true }
tokio = { workspace = true, default-features = true, features = [
"macros",
"time",
] }
tokio-stream = { workspace = true }
tracing = { workspace = true, default-features = true }
[dev-dependencies]
anyhow = { workspace = true }
assert_matches = { workspace = true }
chrono = { workspace = true }
criterion = { workspace = true, default-features = true }
cumulus-zombienet-sdk-helpers = { workspace = true }
env_logger = { workspace = true }
rstest = { workspace = true }
sc-block-builder = { workspace = true, default-features = true }
serde = { workspace = true }
serde_json = { workspace = true }
sp-consensus = { workspace = true, default-features = true }
substrate-test-runtime = { workspace = true }
substrate-test-runtime-client = { workspace = true }
substrate-test-runtime-transaction-pool = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread"] }
tracing-subscriber = { workspace = true }
txtesttool = { workspace = true }
zombienet-configuration = { workspace = true }
zombienet-sdk = { workspace = true }
[features]
runtime-benchmarks = [
"cumulus-zombienet-sdk-helpers/runtime-benchmarks",
"sc-block-builder/runtime-benchmarks",
"sc-client-api/runtime-benchmarks",
"sc-transaction-pool-api/runtime-benchmarks",
"sp-api/runtime-benchmarks",
"sp-blockchain/runtime-benchmarks",
"sp-consensus/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"sp-transaction-pool/runtime-benchmarks",
"substrate-test-runtime-client/runtime-benchmarks",
"substrate-test-runtime-transaction-pool/runtime-benchmarks",
"substrate-test-runtime/runtime-benchmarks",
]
+368
View File
@@ -0,0 +1,368 @@
Substrate transaction pool implementation.
License: GPL-3.0-or-later WITH Classpath-exception-2.0
# Problem Statement
The transaction pool is responsible for maintaining a set of transactions that
possible to include by block authors in upcoming blocks. Transactions are received
either from networking (gossiped by other peers) or RPC (submitted locally).
The main task of the pool is to prepare an ordered list of transactions for block
authorship module. The same list is useful for gossiping to other peers, but note
that it's not a hard requirement for the gossiped transactions to be exactly the
same (see implementation notes below).
It's within block author incentives to have the transactions stored and ordered in
such a way to:
1. Maximize block author's profits (value of the produced block)
2. Minimize block author's amount of work (time to produce block)
In the case of FRAME the first property is simply making sure that the fee per weight
unit is the highest (high `tip` values), the second is about avoiding feeding
transactions that cannot be part of the next block (they are invalid, obsolete, etc).
From the transaction pool PoV, transactions are simply opaque blob of bytes,
it's required to query the runtime (via `TaggedTransactionQueue` Runtime API) to
verify transaction's mere correctness and extract any information about how the
transaction relates to other transactions in the pool and current on-chain state.
Only valid transactions should be stored in the pool.
Each imported block can affect validity of transactions already in the pool. Block
authors expect from the pool to get most up to date information about transactions
that can be included in the block that they are going to build on top of the just
imported one. The process of ensuring this property is called *pruning*. During
pruning the pool should remove transactions which are considered invalid by the
runtime (queried at current best imported block).
Since the blockchain is not always linear, forks need to be correctly handled by
the transaction pool as well. In case of a fork, some blocks are *retracted*
from the canonical chain, and some other blocks get *enacted* on top of some
common ancestor. The transactions from retracted blocks could simply be discarded,
but it's desirable to make sure they are still considered for inclusion in case they
are deemed valid by the runtime state at best, recently enacted block (fork the
chain re-organized to).
Transaction pool should also offer a way of tracking transaction lifecycle in the
pool, it's broadcasting status, block inclusion, finality, etc.
## Transaction Validity details
Information retrieved from the runtime are encapsulated in the `TransactionValidity`
type.
```rust
pub type TransactionValidity = Result<ValidTransaction, TransactionValidityError>;
pub struct ValidTransaction {
pub requires: Vec<TransactionTag>,
pub provides: Vec<TransactionTag>,
pub priority: TransactionPriority,
pub longevity: TransactionLongevity,
pub propagate: bool,
}
pub enum TransactionValidityError {
Invalid(/* details */),
Unknown(/* details */),
}
```
We will go through each of the parameter now to understand the requirements they
create for transaction ordering.
The runtime is expected to return these values in a deterministic fashion. Calling
the API multiple times given exactly the same state must return same results.
Field-specific rules are described below.
### `requires` / `provides`
These two fields contain a set of `TransactionTag`s (opaque blobs) associated with
a given transaction. This is a mechanism for the runtime to be able to
express dependencies between transactions (that this transaction pool can take
account of). By looking at these fields we can establish a transaction's readiness
for block inclusion.
The `provides` set contains properties that will be *satisfied* in case the transaction
is successfully added to a block. Only a transaction in a block may provide a specific
tag. `requires` contains properties that must be satisfied **before** the transaction
can be included to a block.
Note that a transaction with empty `requires` set can be added to a block immediately,
there are no other transactions that it expects to be included before.
For some given series of transactions the `provides` and `requires` fields will create
a (simple) directed acyclic graph. The *sources* in such graph, if they don't have
any extra `requires` tags (i.e. they have their all dependencies *satisfied*), should
be considered for block inclusion first. Multiple transactions that are ready for
block inclusion should be ordered by `priority` (see below).
Note the process of including transactions to a block is basically building the graph,
then selecting "the best" source vertex (transaction) with all tags satisfied and
removing it from that graph.
#### Examples
- A transaction in Bitcoin-like chain will `provide` generated UTXOs and will `require`
UTXOs it is still awaiting for (note that it's not necessarily all require inputs,
since some of them might already be spendable (i.e. the UTXO is in state))
- A transaction in account-based chain will `provide` a `(sender, transaction_index/nonce)`
(as one tag), and will `require` `(sender, nonce - 1)` in case
`on_chain_nonce < nonce - 1`.
#### Rules & caveats
- `provides` must not be empty
- transactions with an overlap in `provides` tags are mutually exclusive
- checking validity of transaction that `requires` tag `A` after including
transaction that provides that tag must not return `A` in `requires` again
- runtime developers should avoid re-using `provides` tag (i.e. it should be unique)
- there should be no cycles in transaction dependencies
- caveat: on-chain state conditions may render transaction invalid despite no
`requires` tags
- caveat: on-chain state conditions may render transaction valid despite some
`requires` tags
- caveat: including transactions to a chain might make them valid again right away
(for instance UTXO transaction gets in, but since we don't store spent outputs
it will be valid again, awaiting the same inputs/tags to be satisfied)
### `priority`
Transaction priority describes importance of the transaction relative to other transactions
in the pool. Block authors can expect benefiting from including such transactions
before others.
Note that we can't simply order transactions in the pool by `priority`, because first
we need to make sure that all of the transaction requirements are satisfied (see
`requires/provides` section). However if we consider a set of transactions
which all have their requirements (tags) satisfied, the block author should be
choosing the ones with highest priority to include to the next block first.
`priority` can be any number between `0` (lowest inclusion priority) to `u64::MAX`
(highest inclusion priority).
#### Rules & caveats
- `priority` of transaction may change over time
- on-chain conditions may affect `priority`
- given two transactions with overlapping `provides` tags, the one with higher
`priority` should be preferred. However we can also look at the total priority
of a subtree rooted at that transaction and compare that instead (i.e. even though
the transaction itself has lower `priority` it "unlocks" other high priority transactions).
### `longevity`
Longevity describes how long (in blocks) the transaction is expected to be
valid. This parameter only gives a hint to the transaction pool how long
current transaction may still be valid. Note that it does not guarantee
the transaction is valid all that time though.
#### Rules & caveats
- `longevity` of transaction may change over time
- on-chain conditions may affect `longevity`
- after `longevity` lapses, the transaction may still be valid
### `propagate`
This parameter instructs the pool propagate/gossip a transaction to node peers.
By default this should be `true`, however in some cases it might be undesirable
to propagate transactions further. Examples might include heavy transactions
produced by block authors in offchain workers (DoS) or risking being front
ran by someone else after finding some non trivial solution or equivocation,
etc.
### 'TransactionSource`
To make it possible for the runtime to distinguish if the transaction that is
being validated was received over the network or submitted using local RPC or
maybe it's simply part of a block that is being imported, the transaction pool
should pass additional `TransactionSource` parameter to the validity function
runtime call.
This can be used by runtime developers to quickly reject transactions that for
instance are not expected to be gossiped in the network.
### `Invalid` transaction
In case the runtime returns an `Invalid` error it means the transaction cannot
be added to a block at all. Extracting the actual reason of invalidity gives
more details about the source. For instance `Stale` transaction just indicates
the transaction was already included in a block, while `BadProof` signifies
invalid signature.
Invalidity might also be temporary. In case of `ExhaustsResources` the
transaction does not fit to the current block, but it might be okay for the next
one.
### `Unknown` transaction
In case of `Unknown` validity, the runtime cannot determine if the transaction
is valid or not in current block. However this situation might be temporary, so
it is expected for the transaction to be retried in the future.
# Implementation
An ideal transaction pool should be storing only transactions that are considered
valid by the runtime at current best imported block.
After every block is imported, the pool should:
1. Revalidate all transactions in the pool and remove the invalid ones.
1. Construct the transaction inclusion graph based on `provides/requires` tags.
Some transactions might not be reachable (have unsatisfied dependencies),
they should be just left out in the pool.
1. On block author request, the graph should be copied and transactions should
be removed one-by-one from the graph starting from the one with highest
priority and all conditions satisfied.
With current gossip protocol, networking should propagate transactions in the
same order as block author would include them. Most likely it's fine if we
propagate transactions with cumulative weight not exceeding upcoming `N`
blocks (choosing `N` is subject to networking conditions and block times).
Note that it's not a strict requirement though to propagate exactly the same
transactions that are prepared for block inclusion. Propagation is best
effort, especially for block authors and is not directly incentivised.
However the networking protocol might penalise peers that send invalid or
useless transactions so we should be nice to others. Also see below a proposal
to instead of gossiping everything have other peers request transactions they
are interested in.
Since the pool is expected to store more transactions than what can fit
in a single block, validating the entire pool on every block might not be
feasible. This means that the actual implementation might need to take some
shortcuts.
## Suggestions & caveats
1. The validity of a transaction should not change significantly from block to
block. I.e. changes in validity should happen predictably, e.g. `longevity`
decrements by 1, `priority` stays the same, `requires` changes if transaction
that provided a tag was included in block, `provides` does not change, etc.
1. That means we don't have to revalidate every transaction after every block
import, but we need to take care of removing potentially stale transactions.
1. Transactions with exactly the same bytes are most likely going to give the
same validity results. We can essentially treat them as identical.
1. Watch out for re-organisations and re-importing transactions from retracted
blocks.
1. In the past there were many issues found when running small networks with a
lot of re-orgs. Make sure that transactions are never lost.
1. The UTXO model is quite challenging. A transaction becomes valid right after
it's included in a block, however it is waiting for exactly the same inputs
to be spent, so it will never really be included again.
1. Note that in a non-ideal implementation the state of the pool will most
likely always be a bit off, i.e. some transactions might be still in the pool,
but they are invalid. The hard decision is about trade-offs you take.
1. Note that import notification is not reliable - you might not receive a
notification about every imported block.
## Potential implementation ideas
1. Block authors remove transactions from the pool when they author a block. We
still store them around to re-import in case the block does not end up
canonical. This only works if the block is actively authoring blocks (also
see below).
1. We don't prune, but rather remove a fixed amount of transactions from the front
of the pool (number based on average/max transactions per block from the
past) and re-validate them, reimporting the ones that are still valid.
1. We periodically validate all transactions in the pool in batches.
1. To minimize runtime calls, we introduce the batch-verify call. Note it should
reset the state (overlay) after every verification.
1. Consider leveraging finality. Maybe we could verify against latest finalised
block instead. With this the pool in different nodes can be more similar
which might help with gossiping (see set reconciliation). Note that finality
is not a strict requirement for a Substrate chain to have though.
1. Perhaps we could avoid maintaining ready/future queues as currently, but
rather if a transaction doesn't have all requirements satisfied by existing
transactions we attempt to re-import it in the future.
1. Instead of maintaining a full pool with total ordering we attempt to maintain
a set of next (couple of) blocks. We could introduce batch-validate runtime
api method that pretty much attempts to simulate actual block inclusion of
a set of such transactions (without necessarily fully running/dispatching
them). Importing a transaction would consist of figuring out which next block
this transaction has a chance to be included in and then attempting to
either push it back or replace some existing transactions.
1. Perhaps we could use some immutable graph structure to easily add/remove
transactions. We need some traversal method that takes priority and
reachability into account.
1. It was discussed in the past to use set reconciliation strategies instead of
simply broadcasting all/some transactions to all/selected peers. An Ethereum's
[EIP-2464](https://github.com/ethereum/EIPs/blob/5b9685bb9c7ba0f5f921e4d3f23504f7ef08d5b1/EIPS/eip-2464.md)
might be a good first approach to reduce transaction gossip.
# Current implementation
Current implementation of the pool is a result of experiences from Ethereum's
pool implementation, but also has some warts coming from the learning process of
Substrate's generic nature and light client support.
The pool consists of basically two independent parts:
1. The transaction pool itself.
2. Maintenance background task.
The pool is split into `ready` pool and `future` pool. The latter contains
transactions that don't have their requirements satisfied, and the former holds
transactions that can be used to build a graph of dependencies. Note that the
graph is built ad-hoc during the traversal process (using the `ready`
iterator). This makes the importing process cheaper (we don't need to find the
exact position in the queue or graph), but traversal process slower
(logarithmic). However most of the time we will only need the beginning of the
total ordering of transactions for block inclusion or network propagation, hence
the decision.
The maintenance task is responsible for:
1. Periodically revalidating pool's transactions (revalidation queue).
1. Handling block import notifications and doing pruning + re-importing of
transactions from retracted blocks.
1. Handling finality notifications and relaying that to transaction-specific
listeners.
Additionally we maintain a list of recently included/rejected transactions
(`PoolRotator`) to quickly reject transactions that are unlikely to be valid
to limit number of runtime verification calls.
Each time a transaction is imported, we first verify it's validity and later
find if the tags it `requires` can be satisfied by transactions already in
`ready` pool. In case the transaction is imported to the `ready` pool we
additionally *promote* transactions from the `future` pool if the transaction
happened to fulfill their requirements.
Note we need to cater for cases where a transaction might replace an already
existing transaction in the pool. In such case we check the entire sub-tree of
transactions that we are about to replace, compare their cumulative priority to
determine which subtree to keep.
After a block is imported we kick-off the pruning procedure. We first attempt to
figure out what tags were satisfied by a transaction in that block. For each block
transaction we either call into the runtime to get it's `ValidTransaction` object,
or we check the pool if that transaction is already known to spare the runtime
call. From this we gather the full set of `provides` tags and perform pruning of
the `ready` pool based on that. Also, we promote all transactions from `future`
that have their tags satisfied.
In case we remove transactions that we are unsure if they were already included
in the current block or some block in the past, it gets added to the revalidation
queue and attempts to be re-imported by the background task in the future.
Runtime calls to verify transactions are performed from a separate (limited)
thread pool to avoid interfering too much with other subsystems of the node. We
definitely don't want to have all cores validating network transactions, because
all of these transactions need to be considered untrusted (potentially DoS).
@@ -0,0 +1,33 @@
[package]
name = "sc-transaction-pool-api"
version = "28.0.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
homepage.workspace = true
repository.workspace = true
description = "Transaction pool client facing API."
[lints]
workspace = true
[dependencies]
async-trait = { workspace = true }
codec = { workspace = true, default-features = true }
futures = { workspace = true }
indexmap = { workspace = true }
log = { workspace = true, default-features = true }
serde = { features = ["derive"], workspace = true, default-features = true }
sp-blockchain = { workspace = true, default-features = true }
sp-core = { workspace = true }
sp-runtime = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
serde_json = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"sp-blockchain/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
@@ -0,0 +1,114 @@
// This file is part of Substrate.
// 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 errors.
use sp_runtime::transaction_validity::{
InvalidTransaction, TransactionPriority as Priority, UnknownTransaction,
};
/// 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("Unknown transaction validity: {0:?}")]
UnknownTransaction(UnknownTransaction),
#[error("Invalid transaction validity: {0:?}")]
InvalidTransaction(InvalidTransaction),
/// The transaction validity returned no "provides" tag.
///
/// Such transactions are not accepted to the pool, since we use those tags
/// to define identity of transactions (occupancy of the same "slot").
#[error("Transaction does not provide any tags, so the pool can't identify it")]
NoTagsProvided,
#[error("Transaction temporarily Banned")]
TemporarilyBanned,
#[error("[{0:?}] Already imported")]
AlreadyImported(Box<dyn std::any::Any + Send + Sync>),
#[error("Too low priority ({} > {})", old, new)]
TooLowPriority {
/// Transaction already in the pool.
old: Priority,
/// Transaction entering the pool.
new: Priority,
},
#[error("Transaction with cyclic dependency")]
CycleDetected,
#[error("Transaction couldn't enter the pool because of the limit")]
ImmediatelyDropped,
#[error("Transaction cannot be propagated and the local node does not author blocks")]
Unactionable,
#[error("{0}")]
InvalidBlockId(String),
#[error("The pool is not accepting future transactions")]
RejectedFutureTransaction,
}
impl Error {
/// Returns true if the transaction could be re-submitted to the pool in the future.
///
/// For example, `Error::ImmediatelyDropped` is retriable, because the transaction
/// may enter the pool if there is space for it in the future.
pub fn is_retriable(&self) -> bool {
match self {
// An invalid transaction is temporarily banned, however it can
// become valid at a later time.
Error::TemporarilyBanned |
// The pool is full at the moment.
Error::ImmediatelyDropped |
// The block id is not known to the pool.
// The node might be lagging behind, or during a warp sync.
Error::InvalidBlockId(_) |
// The pool is configured to not accept future transactions.
Error::RejectedFutureTransaction => {
true
}
_ => false
}
}
}
/// Transaction pool error conversion.
pub trait IntoPoolError: std::error::Error + Send + Sized + Sync {
/// Try to extract original `Error`
///
/// This implementation is optional and used only to
/// provide more descriptive error messages for end users
/// of RPC API.
fn into_pool_error(self) -> std::result::Result<Error, Self> {
Err(self)
}
}
impl IntoPoolError for Error {
fn into_pool_error(self) -> std::result::Result<Error, Self> {
Ok(self)
}
}
@@ -0,0 +1,607 @@
// This file is part of Substrate.
// 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 client facing API.
#![warn(missing_docs)]
pub mod error;
use async_trait::async_trait;
use codec::Codec;
use futures::Stream;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use sp_core::offchain::TransactionPoolExt;
use sp_runtime::traits::{Block as BlockT, Member};
use std::{collections::HashMap, hash::Hash, marker::PhantomData, pin::Pin, sync::Arc};
const LOG_TARGET: &str = "txpool::api";
pub use sp_runtime::transaction_validity::{
TransactionLongevity, TransactionPriority, TransactionSource, TransactionTag,
TransactionValidityError,
};
/// Transaction pool status.
#[derive(Debug, Clone)]
pub struct PoolStatus {
/// Number of transactions in the ready queue.
pub ready: usize,
/// Sum of bytes of ready transaction encodings.
pub ready_bytes: usize,
/// Number of transactions in the future queue.
pub future: usize,
/// Sum of bytes of ready transaction encodings.
pub future_bytes: usize,
}
impl PoolStatus {
/// Returns true if there are no transactions in the pool.
pub fn is_empty(&self) -> bool {
self.ready == 0 && self.future == 0
}
}
/// Possible transaction status events.
///
/// These events are being emitted by `TransactionPool` watchers,
/// which are also exposed over RPC.
///
/// The status events can be grouped based on their kinds as:
/// 1. Entering/Moving within the pool:
/// - [Future](TransactionStatus::Future)
/// - [Ready](TransactionStatus::Ready)
/// 2. Inside `Ready` queue:
/// - [Broadcast](TransactionStatus::Broadcast)
/// 3. Leaving the pool:
/// - [InBlock](TransactionStatus::InBlock)
/// - [Invalid](TransactionStatus::Invalid)
/// - [Usurped](TransactionStatus::Usurped)
/// - [Dropped](TransactionStatus::Dropped)
/// 4. Re-entering the pool:
/// - [Retracted](TransactionStatus::Retracted)
/// 5. Block finalized:
/// - [Finalized](TransactionStatus::Finalized)
/// - [FinalityTimeout](TransactionStatus::FinalityTimeout)
///
/// Transactions are first placed in either the `Ready` or `Future` queues of the transaction pool.
/// Substrate validates the transaction before it enters the pool.
///
/// A transaction is placed in the `Future` queue if it will become valid at a future time.
/// For example, submitting a transaction with a higher account nonce than the current
/// expected nonce will place the transaction in the `Future` queue.
///
/// The events will always be received in the order described above, however
/// there might be cases where transactions alternate between `Future` and `Ready`
/// pool, and are `Broadcast` in the meantime.
///
/// There is also only single event causing the transaction to leave the pool.
/// I.e. only one of the listed ones should be triggered.
///
/// Note that there are conditions that may cause transactions to reappear in the pool.
/// 1. Due to possible forks, the transaction that ends up being in included
/// in one block, may later re-enter the pool or be marked as invalid.
/// 2. Transaction `Dropped` at one point, may later re-enter the pool if some other
/// transactions are removed. A `Dropped` transaction may re-enter the pool only if it is
/// resubmitted.
/// 3. `Invalid` transaction may become valid at some point in the future.
/// (Note that runtimes are encouraged to use `UnknownValidity` to inform the pool about
/// such case). An `Invalid` transaction may re-enter the pool only if it is resubmitted.
/// 4. `Retracted` transactions might be included in some next block.
///
/// The `FinalityTimeout` event will be emitted when the block did not reach finality
/// within 512 blocks. This either indicates that finality is not available for your chain,
/// or that finality gadget is lagging behind. If you choose to wait for finality longer, you can
/// re-subscribe for a particular transaction hash manually again.
///
/// ### Last Event
///
/// The stream is considered finished when one of the following events happen:
/// - [Finalized](TransactionStatus::Finalized)
/// - [FinalityTimeout](TransactionStatus::FinalityTimeout)
/// - [Usurped](TransactionStatus::Usurped)
/// - [Invalid](TransactionStatus::Invalid)
/// - [Dropped](TransactionStatus::Dropped)
///
/// See [`TransactionStatus::is_final`] for more details.
///
/// ### Resubmit Transactions
///
/// Users might resubmit the transaction at a later time for the following events:
/// - [FinalityTimeout](TransactionStatus::FinalityTimeout)
/// - [Invalid](TransactionStatus::Invalid)
/// - [Dropped](TransactionStatus::Dropped)
///
/// See [`TransactionStatus::is_retriable`] for more details.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TransactionStatus<Hash, BlockHash> {
/// Transaction is part of the future queue.
Future,
/// Transaction is part of the ready queue.
Ready,
/// The transaction has been broadcast to the given peers.
Broadcast(Vec<String>),
/// Transaction has been included in block with given hash
/// at the given position.
#[serde(with = "v1_compatible")]
InBlock((BlockHash, TxIndex)),
/// The block this transaction was included in has been retracted.
Retracted(BlockHash),
/// Maximum number of finality watchers has been reached,
/// old watchers are being removed.
FinalityTimeout(BlockHash),
/// Transaction has been finalized by a finality-gadget, e.g. GRANDPA.
#[serde(with = "v1_compatible")]
Finalized((BlockHash, TxIndex)),
/// Transaction has been replaced in the pool, by another transaction
/// that provides the same tags. (e.g. same (sender, nonce)).
Usurped(Hash),
/// Transaction has been dropped from the pool because of the limit.
Dropped,
/// Transaction is no longer valid in the current state.
Invalid,
}
impl<Hash, BlockHash> TransactionStatus<Hash, BlockHash> {
/// Returns true if this is the last event emitted by [`TransactionStatusStream`].
pub fn is_final(&self) -> bool {
// The state must be kept in sync with `crate::graph::Sender`.
match self {
Self::Usurped(_) |
Self::Finalized(_) |
Self::FinalityTimeout(_) |
Self::Invalid |
Self::Dropped => true,
_ => false,
}
}
/// Returns true if the transaction could be re-submitted to the pool in the future.
///
/// For example, `TransactionStatus::Dropped` is retriable, because the transaction
/// may enter the pool if there is space for it in the future.
pub fn is_retriable(&self) -> bool {
match self {
// The number of finality watchers has been reached.
Self::FinalityTimeout(_) |
// An invalid transaction might be valid at a later time.
Self::Invalid |
// The transaction was dropped because of the limits of the pool.
// It can reenter the pool when other transactions are removed / finalized.
Self::Dropped => true,
_ => false,
}
}
}
/// The stream of transaction events.
pub type TransactionStatusStream<Hash, BlockHash> =
dyn Stream<Item = TransactionStatus<Hash, BlockHash>> + Send;
/// The import notification event stream.
pub type ImportNotificationStream<H> = futures::channel::mpsc::Receiver<H>;
/// Transaction hash type for a pool.
pub type TxHash<P> = <P as TransactionPool>::Hash;
/// Block hash type for a pool.
pub type BlockHash<P> = <<P as TransactionPool>::Block as BlockT>::Hash;
/// Transaction type for a pool.
pub type TransactionFor<P> = <<P as TransactionPool>::Block as BlockT>::Extrinsic;
/// Type of transactions event stream for a pool.
pub type TransactionStatusStreamFor<P> = TransactionStatusStream<TxHash<P>, BlockHash<P>>;
/// Transaction type for a local pool.
pub type LocalTransactionFor<P> = <<P as LocalTransactionPool>::Block as BlockT>::Extrinsic;
/// Transaction's index within the block in which it was included.
pub type TxIndex = usize;
/// Map containing validity errors associated with transaction hashes. Used to report invalid
/// transactions to the pool.
pub type TxInvalidityReportMap<H> = indexmap::IndexMap<H, Option<TransactionValidityError>>;
/// In-pool transaction interface.
///
/// The pool is container of transactions that are implementing this trait.
/// See `sp_runtime::ValidTransaction` for details about every field.
pub trait InPoolTransaction {
/// Transaction type.
type Transaction;
/// Transaction hash type.
type Hash;
/// Get the reference to the transaction data.
fn data(&self) -> &Self::Transaction;
/// Get hash of the transaction.
fn hash(&self) -> &Self::Hash;
/// Get priority of the transaction.
fn priority(&self) -> &TransactionPriority;
/// Get longevity of the transaction.
fn longevity(&self) -> &TransactionLongevity;
/// Get transaction dependencies.
fn requires(&self) -> &[TransactionTag];
/// Get tags that transaction provides.
fn provides(&self) -> &[TransactionTag];
/// Return a flag indicating if the transaction should be propagated to other peers.
fn is_propagable(&self) -> bool;
}
/// Transaction pool interface.
#[async_trait]
pub trait TransactionPool: Send + Sync {
/// Block type.
type Block: BlockT;
/// Transaction hash type.
type Hash: Hash + Eq + Member + Serialize + DeserializeOwned + Codec;
/// In-pool transaction type.
type InPoolTransaction: InPoolTransaction<
Transaction = Arc<TransactionFor<Self>>,
Hash = TxHash<Self>,
>;
/// Error type.
type Error: From<crate::error::Error> + crate::error::IntoPoolError;
// *** RPC
/// Asynchronously imports a bunch of unverified transactions to the pool.
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>;
/// Asynchronously imports one unverified transaction to the pool.
async fn submit_one(
&self,
at: <Self::Block as BlockT>::Hash,
source: TransactionSource,
xt: TransactionFor<Self>,
) -> Result<TxHash<Self>, Self::Error>;
/// Asynchronously imports a single transaction and starts to watch their progress in the
/// pool.
async fn submit_and_watch(
&self,
at: <Self::Block as BlockT>::Hash,
source: TransactionSource,
xt: TransactionFor<Self>,
) -> Result<Pin<Box<TransactionStatusStreamFor<Self>>>, Self::Error>;
// *** Block production / Networking
/// Get an iterator for ready transactions ordered by priority.
///
/// Guaranteed to resolve only when transaction pool got updated at `at` block.
/// Guaranteed to resolve immediately when `None` is passed.
async fn ready_at(
&self,
at: <Self::Block as BlockT>::Hash,
) -> Box<dyn ReadyTransactions<Item = Arc<Self::InPoolTransaction>> + Send>;
/// Get an iterator for ready transactions ordered by priority.
fn ready(&self) -> Box<dyn ReadyTransactions<Item = Arc<Self::InPoolTransaction>> + Send>;
// *** Block production
/// Reports invalid transactions to the transaction pool.
///
/// This function takes a map where the key is a transaction hash and the value is an
/// optional error encountered during the transaction execution, possibly within a specific
/// block.
///
/// The transaction pool implementation decides which transactions to remove. Transactions
/// removed from the pool will be notified with `TransactionStatus::Invalid` event (if
/// `submit_and_watch` was used for submission).
///
/// If the error associated to transaction is `None`, the transaction will be forcibly removed
/// from the pool.
///
/// The optional `at` parameter provides additional context regarding the block where the error
/// occurred.
///
/// Function returns the transactions actually removed from the pool.
async fn report_invalid(
&self,
at: Option<<Self::Block as BlockT>::Hash>,
invalid_tx_errors: TxInvalidityReportMap<TxHash<Self>>,
) -> Vec<Arc<Self::InPoolTransaction>>;
// *** logging
/// Get futures transaction list.
fn futures(&self) -> Vec<Self::InPoolTransaction>;
/// Returns pool status.
fn status(&self) -> PoolStatus;
// *** logging / RPC / networking
/// Return an event stream of transactions imported to the pool.
fn import_notification_stream(&self) -> ImportNotificationStream<TxHash<Self>>;
// *** networking
/// Notify the pool about transactions broadcast.
fn on_broadcasted(&self, propagations: HashMap<TxHash<Self>, Vec<String>>);
/// Returns transaction hash
fn hash_of(&self, xt: &TransactionFor<Self>) -> TxHash<Self>;
/// Return specific ready transaction by hash, if there is one.
fn ready_transaction(&self, hash: &TxHash<Self>) -> Option<Arc<Self::InPoolTransaction>>;
/// Asynchronously returns a set of ready transaction at given block within given timeout.
///
/// If the timeout is hit during method execution, then the best effort (without executing full
/// maintain process) set of ready transactions for given block is returned.
async fn ready_at_with_timeout(
&self,
at: <Self::Block as BlockT>::Hash,
timeout: std::time::Duration,
) -> Box<dyn ReadyTransactions<Item = Arc<Self::InPoolTransaction>> + Send>;
}
/// An iterator of ready transactions.
///
/// The trait extends regular [`std::iter::Iterator`] trait and allows reporting
/// last-returned element as invalid.
///
/// The implementation is then allowed, for performance reasons, to change the elements
/// returned next, by e.g. skipping elements that are known to depend on the reported
/// transaction, which yields them invalid as well.
pub trait ReadyTransactions: Iterator {
/// Report given transaction as invalid.
///
/// This might affect subsequent elements returned by the iterator, so dependent transactions
/// are skipped for performance reasons.
fn report_invalid(&mut self, _tx: &Self::Item);
}
/// A no-op implementation for an empty iterator.
impl<T> ReadyTransactions for std::iter::Empty<T> {
fn report_invalid(&mut self, _tx: &T) {}
}
/// Events that the transaction pool listens for.
#[derive(Debug)]
pub enum ChainEvent<B: BlockT> {
/// New best block have been added to the chain.
NewBestBlock {
/// Hash of the block.
hash: B::Hash,
/// Tree route from old best to new best parent that was calculated on import.
///
/// If `None`, no re-org happened on import.
tree_route: Option<Arc<sp_blockchain::TreeRoute<B>>>,
},
/// An existing block has been finalized.
Finalized {
/// Hash of just finalized block.
hash: B::Hash,
/// Path from old finalized to new finalized parent.
tree_route: Arc<[B::Hash]>,
},
}
impl<B: BlockT> ChainEvent<B> {
/// Returns the block hash associated to the event.
pub fn hash(&self) -> B::Hash {
match self {
Self::NewBestBlock { hash, .. } | Self::Finalized { hash, .. } => *hash,
}
}
/// Is `self == Self::Finalized`?
pub fn is_finalized(&self) -> bool {
matches!(self, Self::Finalized { .. })
}
}
/// Trait for transaction pool maintenance.
#[async_trait]
pub trait MaintainedTransactionPool: TransactionPool {
/// Perform maintenance
async fn maintain(&self, event: ChainEvent<Self::Block>);
}
/// Transaction pool interface for submitting local transactions that exposes a
/// blocking interface for submission.
pub trait LocalTransactionPool: Send + Sync {
/// Block type.
type Block: BlockT;
/// Transaction hash type.
type Hash: Hash + Eq + Member + Serialize;
/// Error type.
type Error: From<crate::error::Error> + crate::error::IntoPoolError;
/// Submits the given local unverified transaction to the pool blocking the
/// current thread for any necessary pre-verification.
/// NOTE: It MUST NOT be used for transactions that originate from the
/// network or RPC, since the validation is performed with
/// `TransactionSource::Local`.
fn submit_local(
&self,
at: <Self::Block as BlockT>::Hash,
xt: LocalTransactionFor<Self>,
) -> Result<Self::Hash, Self::Error>;
}
impl<T: LocalTransactionPool> LocalTransactionPool for Arc<T> {
type Block = T::Block;
type Hash = T::Hash;
type Error = T::Error;
fn submit_local(
&self,
at: <Self::Block as BlockT>::Hash,
xt: LocalTransactionFor<Self>,
) -> Result<Self::Hash, Self::Error> {
(**self).submit_local(at, xt)
}
}
/// An abstraction for [`LocalTransactionPool`]
///
/// We want to use a transaction pool in [`OffchainTransactionPoolFactory`] in a `Arc` without
/// bleeding the associated types besides the `Block`. Thus, this abstraction here exists to achieve
/// the wrapping in a `Arc`.
trait OffchainSubmitTransaction<Block: BlockT>: Send + Sync {
/// Submit transaction.
///
/// The transaction will end up in the pool and be propagated to others.
fn submit_at(&self, at: Block::Hash, extrinsic: Block::Extrinsic) -> Result<(), ()>;
}
impl<TPool: LocalTransactionPool> OffchainSubmitTransaction<TPool::Block> for TPool {
fn submit_at(
&self,
at: <TPool::Block as BlockT>::Hash,
extrinsic: <TPool::Block as BlockT>::Extrinsic,
) -> Result<(), ()> {
log::trace!(
target: LOG_TARGET,
"(offchain call) Submitting a transaction to the pool: {:?}",
extrinsic
);
let result = self.submit_local(at, extrinsic);
result.map(|_| ()).map_err(|e| {
log::warn!(
target: LOG_TARGET,
"(offchain call) Error submitting a transaction to the pool: {}",
e
)
})
}
}
/// Factory for creating [`TransactionPoolExt`]s.
///
/// This provides an easy way for creating [`TransactionPoolExt`] extensions for registering them in
/// the wasm execution environment to send transactions from an offchain call to the runtime.
#[derive(Clone)]
pub struct OffchainTransactionPoolFactory<Block: BlockT> {
pool: Arc<dyn OffchainSubmitTransaction<Block>>,
}
impl<Block: BlockT> OffchainTransactionPoolFactory<Block> {
/// Creates a new instance using the given `tx_pool`.
pub fn new<T: LocalTransactionPool<Block = Block> + 'static>(tx_pool: T) -> Self {
Self { pool: Arc::new(tx_pool) as Arc<_> }
}
/// Returns an instance of [`TransactionPoolExt`] bound to the given `block_hash`.
///
/// Transactions that are being submitted by this instance will be submitted with `block_hash`
/// as context for validation.
pub fn offchain_transaction_pool(&self, block_hash: Block::Hash) -> TransactionPoolExt {
TransactionPoolExt::new(OffchainTransactionPool { pool: self.pool.clone(), block_hash })
}
}
/// Wraps a `pool` and `block_hash` to implement [`sp_core::offchain::TransactionPool`].
struct OffchainTransactionPool<Block: BlockT> {
block_hash: Block::Hash,
pool: Arc<dyn OffchainSubmitTransaction<Block>>,
}
impl<Block: BlockT> sp_core::offchain::TransactionPool for OffchainTransactionPool<Block> {
fn submit_transaction(&mut self, extrinsic: Vec<u8>) -> Result<(), ()> {
let extrinsic = match codec::Decode::decode(&mut &extrinsic[..]) {
Ok(t) => t,
Err(e) => {
log::error!(
target: LOG_TARGET,
"Failed to decode extrinsic in `OffchainTransactionPool::submit_transaction`: {e:?}"
);
return Err(());
},
};
self.pool.submit_at(self.block_hash, extrinsic)
}
}
/// Wrapper functions to keep the API backwards compatible over the wire for the old RPC spec.
mod v1_compatible {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S, H>(data: &(H, usize), serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
H: Serialize,
{
let (hash, _) = data;
serde::Serialize::serialize(&hash, serializer)
}
pub fn deserialize<'de, D, H>(deserializer: D) -> Result<(H, usize), D::Error>
where
D: Deserializer<'de>,
H: Deserialize<'de>,
{
let hash: H = serde::Deserialize::deserialize(deserializer)?;
Ok((hash, 0))
}
}
/// Transaction pool that rejects all submitted transactions.
///
/// Could be used for example in tests.
pub struct RejectAllTxPool<Block>(PhantomData<Block>);
impl<Block> Default for RejectAllTxPool<Block> {
fn default() -> Self {
Self(PhantomData)
}
}
impl<Block: BlockT> LocalTransactionPool for RejectAllTxPool<Block> {
type Block = Block;
type Hash = Block::Hash;
type Error = error::Error;
fn submit_local(&self, _: Block::Hash, _: Block::Extrinsic) -> Result<Self::Hash, Self::Error> {
Err(error::Error::ImmediatelyDropped)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tx_status_compatibility() {
let event: TransactionStatus<u8, u8> = TransactionStatus::InBlock((1, 2));
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"inBlock":1}"#;
assert_eq!(ser, exp);
let event_dec: TransactionStatus<u8, u8> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, TransactionStatus::InBlock((1, 0)));
let event: TransactionStatus<u8, u8> = TransactionStatus::Finalized((1, 2));
let ser = serde_json::to_string(&event).unwrap();
let exp = r#"{"finalized":1}"#;
assert_eq!(ser, exp);
let event_dec: TransactionStatus<u8, u8> = serde_json::from_str(exp).unwrap();
assert_eq!(event_dec, TransactionStatus::Finalized((1, 0)));
}
}
@@ -0,0 +1,221 @@
// This file is part of Substrate.
// 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 async_trait::async_trait;
use codec::Encode;
use criterion::{criterion_group, criterion_main, Criterion};
use futures::executor::block_on;
use sc_transaction_pool::*;
use sp_blockchain::HashAndNumber;
use sp_crypto_hashing::blake2_256;
use sp_runtime::{
generic::BlockId,
traits::{Block as BlockT, NumberFor},
transaction_validity::{
InvalidTransaction, TransactionSource, TransactionTag as Tag, TransactionValidity,
ValidTransaction,
},
};
use std::sync::Arc;
use substrate_test_runtime::{AccountId, Block, Extrinsic, ExtrinsicBuilder, TransferData, H256};
#[derive(Clone, Debug, Default)]
struct TestApi {
nonce_dependant: bool,
}
impl TestApi {
fn new_dependant() -> Self {
TestApi { nonce_dependant: true }
}
}
fn to_tag(nonce: u64, from: AccountId) -> Tag {
let mut data = [0u8; 40];
data[..8].copy_from_slice(&nonce.to_le_bytes()[..]);
data[8..].copy_from_slice(&from.0[..]);
data.to_vec()
}
#[async_trait]
impl ChainApi for TestApi {
type Block = Block;
type Error = sc_transaction_pool_api::error::Error;
async fn validate_transaction(
&self,
at: <Self::Block as BlockT>::Hash,
_: TransactionSource,
uxt: Arc<<Self::Block as BlockT>::Extrinsic>,
_: ValidateTransactionPriority,
) -> Result<TransactionValidity, Self::Error> {
let uxt = (*uxt).clone();
let transfer = TransferData::try_from(&uxt)
.expect("uxt is expected to be bench_call (carrying TransferData)");
let nonce = transfer.nonce;
let from = transfer.from;
match self.block_id_to_number(&BlockId::Hash(at)) {
Ok(Some(num)) if num > 5 => return Ok(Err(InvalidTransaction::Stale.into())),
_ => {},
}
Ok(Ok(ValidTransaction {
priority: 4,
requires: if nonce > 1 && self.nonce_dependant {
vec![to_tag(nonce - 1, from)]
} else {
vec![]
},
provides: vec![to_tag(nonce, from)],
longevity: 10,
propagate: true,
}))
}
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!();
}
fn block_id_to_number(
&self,
at: &BlockId<Self::Block>,
) -> Result<Option<NumberFor<Self::Block>>, 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,
})
}
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),
})
}
fn hash_and_length(&self, uxt: &<Self::Block as BlockT>::Extrinsic) -> (H256, usize) {
let encoded = uxt.encode();
(blake2_256(&encoded).into(), 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<sp_blockchain::TreeRoute<Self::Block>, Self::Error> {
unimplemented!()
}
}
fn uxt(transfer: TransferData) -> Extrinsic {
ExtrinsicBuilder::new_bench_call(transfer).build()
}
fn bench_configured(pool: Pool<TestApi, ()>, number: u64, api: Arc<TestApi>) {
let source = TimedTransactionSource::new_external(false);
let mut futures = Vec::new();
let mut tags = Vec::new();
let at = HashAndNumber {
hash: api.block_id_to_hash(&BlockId::Number(1)).unwrap().unwrap(),
number: 1,
};
for nonce in 1..=number {
let xt = uxt(TransferData {
from: AccountId::from_h256(H256::from_low_u64_be(1)),
to: AccountId::from_h256(H256::from_low_u64_be(2)),
amount: 5,
nonce,
})
.into();
tags.push(to_tag(nonce, AccountId::from_h256(H256::from_low_u64_be(1))));
futures.push(pool.submit_one(&at, source.clone(), xt));
}
let res = block_on(futures::future::join_all(futures.into_iter()));
assert!(res.iter().all(Result::is_ok));
assert_eq!(pool.validated_pool().status().future, 0);
assert_eq!(pool.validated_pool().status().ready, number as usize);
// Prune all transactions.
let block_num = 6;
let at = HashAndNumber {
hash: api.block_id_to_hash(&BlockId::Number(block_num)).unwrap().unwrap(),
number: block_num,
};
block_on(pool.prune_tags(&at, tags, vec![]));
// pool is empty
assert_eq!(pool.validated_pool().status().ready, 0);
assert_eq!(pool.validated_pool().status().future, 0);
}
fn benchmark_main(c: &mut Criterion) {
c.bench_function("sequential 50 tx", |b| {
b.iter(|| {
let api = Arc::from(TestApi::new_dependant());
bench_configured(
Pool::new_with_staticly_sized_rotator(Default::default(), true.into(), api.clone()),
50,
api,
);
});
});
c.bench_function("random 100 tx", |b| {
b.iter(|| {
let api = Arc::from(TestApi::default());
bench_configured(
Pool::new_with_staticly_sized_rotator(Default::default(), true.into(), api.clone()),
100,
api,
);
});
});
}
criterion_group!(benches, benchmark_main);
criterion_main!(benches);
@@ -0,0 +1,251 @@
// This file is part of Substrate.
// 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 substrate 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 sc_transaction_pool_api::{LocalTransactionPool, MaintainedTransactionPool};
use sp_core::traits::SpawnEssentialNamed;
use sp_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: sp_api::ProvideRuntimeApi<Block>
+ sc_client_api::BlockBackend<Block>
+ sc_client_api::blockchain::HeaderBackend<Block>
+ sp_runtime::traits::BlockIdTo<Block>
+ sp_blockchain::HeaderMetadata<Block, Error = sp_blockchain::Error>
+ 'static,
Client::Api: sp_transaction_pool::runtime_api::TaggedTransactionQueue<Block>,
{
}
impl<Block, Client, P> FullClientTransactionPool<Block, Client> for P
where
Block: BlockT,
Client: sp_api::ProvideRuntimeApi<Block>
+ sc_client_api::BlockBackend<Block>
+ sc_client_api::blockchain::HeaderBackend<Block>
+ sp_runtime::traits::BlockIdTo<Block>
+ sp_blockchain::HeaderMetadata<Block, Error = sp_blockchain::Error>
+ 'static,
Client::Api: sp_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: sp_api::ProvideRuntimeApi<Block>
+ sc_client_api::BlockBackend<Block>
+ sc_client_api::blockchain::HeaderBackend<Block>
+ sp_runtime::traits::BlockIdTo<Block>
+ sc_client_api::ExecutorProvider<Block>
+ sc_client_api::UsageProvider<Block>
+ sp_blockchain::HeaderMetadata<Block, Error = sp_blockchain::Error>
+ Send
+ Sync
+ 'static,
<Block as BlockT>::Hash: std::marker::Unpin,
Client::Api: sp_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 Substrate.
// 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 sc_client_api::{blockchain::HeaderBackend, BlockBackend};
use sp_api::{ApiExt, ProvideRuntimeApi};
use sp_blockchain::{HeaderMetadata, TreeRoute};
use sp_core::traits::SpawnEssentialNamed;
use sp_runtime::{
generic::BlockId,
traits::{self, Block as BlockT, BlockIdTo},
transaction_validity::{TransactionSource, TransactionValidity},
};
use sp_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 = sp_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> {
sp_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 = sp_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 = sp_tracing::within_span!(sp_tracing::Level::TRACE, "validate_transaction";
{
let runtime_api = client.runtime_api();
let api_version = sp_tracing::within_span! { sp_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 sp_api::Core;
sp_tracing::within_span!(
sp_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, &sp_runtime::traits::Header::new(
block_number + sp_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 Substrate.
// 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/>.
//! Substrate transaction pool implementation.
use crate::LOG_TARGET;
use sc_transaction_pool_api::ChainEvent;
use sp_blockchain::TreeRoute;
use sp_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 sc_transaction_pool_api::ChainEvent;
use sp_blockchain::{HashAndNumber, TreeRoute};
use sp_runtime::traits::NumberFor;
use std::sync::Arc;
use substrate_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() {
sp_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() {
sp_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() {
sp_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() {
sp_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() {
sp_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() {
sp_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() {
sp_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() {
sp_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() {
sp_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() {
sp_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 Substrate.
// 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 sc_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] sp_blockchain::Error),
#[error("Block conversion error: {0}")]
BlockIdConversion(String),
#[error("Runtime error: {0}")]
RuntimeApi(String),
}
impl sc_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 Substrate.
// 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(
"substrate_sub_txpool_validations_scheduled",
"Total number of transactions scheduled for validation",
)?,
registry,
)?,
validations_finished: register(
Counter::new(
"substrate_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 Substrate.
// 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: sp_runtime::traits::Block,
Client: sc_client_api::BlockchainEvents<Block>,
Pool: sc_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 Substrate.
// 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 Substrate.
// 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 sc_transaction_pool_api::error;
use sp_blockchain::{HashAndNumber, TreeRoute};
use sp_runtime::{
generic::BlockId,
traits::{Block as BlockT, Hash},
transaction_validity::{
InvalidTransaction, TransactionSource, TransactionValidity, ValidTransaction,
},
};
use std::{collections::HashSet, sync::Arc};
use substrate_test_runtime::{
substrate_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::SubstrateTest(PalletCall::include_data { .. }),
..
} => Ok(ValidTransaction {
priority: 9001,
requires: vec![],
provides: vec![vec![42]],
longevity: 9001,
propagate: false,
}),
Extrinsic {
function: RuntimeCall::SubstrateTest(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 Substrate.
// 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 Substrate.
// 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 sc_transaction_pool_api::TransactionStatus;
use sc_utils::mpsc;
use sp_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) = sc_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 sp_core::H256;
type MultiViewDroppedWatcher = super::MultiViewDroppedWatcherController<TestApi>;
#[tokio::test]
async fn test01() {
sp_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() {
sp_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() {
sp_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() {
sp_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() {
sp_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 Substrate.
// 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 sc_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) =
sc_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() {
sp_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() {
sp_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() {
sp_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 Substrate.
// 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 sc_transaction_pool_api::TransactionPool;
use sc_transaction_pool_api::TransactionStatus;
use sc_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!(
"substrate_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!(
"substrate_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!(
"substrate_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!(
"substrate_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!(
"substrate_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!(
"substrate_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!(
"substrate_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!(
"substrate_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!(
"substrate_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!(
"substrate_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(
"substrate_sub_txpool_submitted_txs_total",
"Total number of transactions submitted",
)?,
registry,
)?,
active_views: register(
Gauge::new(
"substrate_sub_txpool_active_views",
"Total number of currently maintained views.",
)?,
registry,
)?,
inactive_views: register(
Gauge::new(
"substrate_sub_txpool_inactive_views",
"Total number of current inactive views.",
)?,
registry,
)?,
watched_txs: register(
Gauge::new(
"substrate_sub_txpool_watched_txs",
"Total number of watched transactions in txpool.",
)?,
registry,
)?,
unwatched_txs: register(
Gauge::new(
"substrate_sub_txpool_unwatched_txs",
"Total number of unwatched transactions in txpool.",
)?,
registry,
)?,
reported_invalid_txs: register(
Counter::new(
"substrate_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(
"substrate_sub_txpool_removed_invalid_txs_total",
"Total number of transactions removed as invalid.",
)?,
registry,
)?,
unknown_from_block_import_txs: register(
Counter::new(
"substrate_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(
"substrate_sub_txpool_finalized_txs_total",
"Total number of finalized transactions.",
)?,
registry,
)?,
maintain_duration: register(
Histogram::with_opts(histogram_opts!(
"substrate_sub_txpool_maintain_duration_seconds",
"Histogram of maintain durations.",
linear_buckets(0.0, 0.25, 13).unwrap()
))?,
registry,
)?,
resubmitted_retracted_txs: register(
Counter::new(
"substrate_sub_txpool_resubmitted_retracted_txs_total",
"Total number of transactions resubmitted from retracted forks.",
)?,
registry,
)?,
submitted_from_mempool_txs: register(
Counter::new(
"substrate_sub_txpool_submitted_from_mempool_txs_total",
"Total number of transactions submitted from mempool to views.",
)?,
registry,
)?,
mempool_revalidation_invalid_txs: register(
Counter::new(
"substrate_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(
"substrate_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(
"substrate_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!(
"substrate_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(
"substrate_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 Substrate.
// 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/>.
//! Substrate 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`]: sc_transaction_pool_api::ChainEvent
//! [`TransactionStatusStreamFor`]: sc_transaction_pool_api::TransactionStatusStreamFor
//! [`api_submit`]: sc_transaction_pool_api::TransactionPool::submit_at
//! [`api_submit_and_watch`]: sc_transaction_pool_api::TransactionPool::submit_and_watch
//! [`ready_at_with_timeout`]: sc_transaction_pool_api::TransactionPool::ready_at_with_timeout
//! [`TransactionSource`]: sc_transaction_pool_api::TransactionSource
//! [TransactionPool API]: sc_transaction_pool_api::TransactionPool
//! [`TransactionStatus`]:sc_transaction_pool_api::TransactionStatus
//! [`Ready`]:sc_transaction_pool_api::TransactionStatus::Ready
//! [`Future`]:sc_transaction_pool_api::TransactionStatus::Future
//! [`Broadcast`]:sc_transaction_pool_api::TransactionStatus::Broadcast
//! [`Invalid`]:sc_transaction_pool_api::TransactionStatus::Invalid
//! [`InBlock`]:sc_transaction_pool_api::TransactionStatus::InBlock
//! [`Finalized`]:sc_transaction_pool_api::TransactionStatus::Finalized
//! [`Dropped`]:sc_transaction_pool_api::TransactionStatus::Dropped
//! [`ReadyTransactions`]:sc_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`]: sp_blockchain::TreeRoute
//! [runtime_api::validate]: sp_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 Substrate.
// 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 sc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
use sp_blockchain::HashAndNumber;
use sp_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 substrate_test_runtime::{AccountId, Transfer, H256};
use substrate_test_runtime_client::Sr25519Keyring::Alice;
#[test]
fn revalidation_queue_works() {
let api = Arc::new(TestApi::default());
let block0 = api.expect_hash_and_number(0);
let view = Arc::new(
View::new(api.clone(), block0, Default::default(), Default::default(), false.into()).0,
);
let queue = Arc::new(RevalidationQueue::new());
let uxt = uxt(Transfer {
from: Alice.into(),
to: AccountId::from_h256(H256::from_low_u64_be(2)),
amount: 5,
nonce: 0,
});
let _ = block_on(view.submit_many(
std::iter::once((TimedTransactionSource::new_external(false), uxt.clone().into())),
ValidateTransactionPriority::Submitted,
));
assert_eq!(api.validation_requests().len(), 1);
let (finish_revalidation_request_tx, finish_revalidation_request_rx) =
tokio::sync::mpsc::channel(1);
let (revalidation_result_tx, revalidation_result_rx) = tokio::sync::mpsc::channel(1);
let finish_revalidation_worker_channels = FinishRevalidationWorkerChannels::new(
finish_revalidation_request_rx,
revalidation_result_tx,
);
let _finish_revalidation_local_channels = FinishRevalidationLocalChannels::new(
finish_revalidation_request_tx,
revalidation_result_rx,
);
block_on(queue.revalidate_view(view.clone(), finish_revalidation_worker_channels));
assert_eq!(api.validation_requests().len(), 2);
// number of ready
assert_eq!(view.status().ready, 1);
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,764 @@
// This file is part of Substrate.
// 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 Substrate.
// 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 sc_transaction_pool_api::{error::Error as TxPoolError, PoolStatus, TransactionStatus};
use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
use sp_blockchain::HashAndNumber;
use sp_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,
sc_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 Substrate.
// 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 sp_core::hexdisplay::HexDisplay;
use sp_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 Substrate.
// 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 Substrate.
// 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 Substrate.
// 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 sc_transaction_pool_api::error;
use serde::Serialize;
use sp_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> sc_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 Substrate.
// 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 Substrate.
// 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 Substrate.
// 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 sc_transaction_pool_api::{error, PoolStatus, ReadyTransactions, TransactionPriority};
use sp_blockchain::HashAndNumber;
use sp_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 Substrate.
// 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 sc_transaction_pool_api::TransactionStatus;
use sc_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 Substrate.
// 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/>.
//! Substrate 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 sc_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 Substrate.
// 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(
"substrate_sub_txpool_submitted_transactions",
"Total number of transactions submitted",
)?,
registry,
)?,
validations_invalid: register(
Counter::new(
"substrate_sub_txpool_validations_invalid",
"Total number of transactions that were removed from the pool as invalid",
)?,
registry,
)?,
block_transactions_pruned: register(
Counter::new(
"substrate_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(
"substrate_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 Substrate.
// 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/>.
//! Substrate 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 Substrate.
// 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 sc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
use sp_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 substrate_test_runtime::{AccountId, Transfer, H256};
use substrate_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 Substrate.
// 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/>.
//! Substrate 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 sc_transaction_pool_api::{
error::Error as TxPoolError, ChainEvent, ImportNotificationStream, MaintainedTransactionPool,
PoolStatus, TransactionFor, TransactionPool, TransactionSource, TransactionStatusStreamFor,
TxHash, TxInvalidityReportMap,
};
use sp_blockchain::{HashAndNumber, TreeRoute};
use sp_core::traits::SpawnEssentialNamed;
use sp_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: sp_api::ProvideRuntimeApi<Block>
+ sc_client_api::BlockBackend<Block>
+ sc_client_api::blockchain::HeaderBackend<Block>
+ sp_runtime::traits::BlockIdTo<Block>
+ sc_client_api::ExecutorProvider<Block>
+ sc_client_api::UsageProvider<Block>
+ sp_blockchain::HeaderMetadata<Block, Error = sp_blockchain::Error>
+ Send
+ Sync
+ 'static,
Client::Api: sp_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> sc_transaction_pool_api::LocalTransactionPool
for BasicPool<FullChainApi<Client, Block>, Block>
where
Block: BlockT,
Client: sp_api::ProvideRuntimeApi<Block>
+ sc_client_api::BlockBackend<Block>
+ sc_client_api::blockchain::HeaderBackend<Block>
+ sp_runtime::traits::BlockIdTo<Block>
+ sp_blockchain::HeaderMetadata<Block, Error = sp_blockchain::Error>,
Client: Send + Sync + 'static,
Client::Api: sp_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: sc_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 Substrate.
// 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 sc_transaction_pool_api::{
ChainEvent, ImportNotificationStream, LocalTransactionFor, LocalTransactionPool,
MaintainedTransactionPool, PoolStatus, ReadyTransactions, TransactionFor, TransactionPool,
TransactionSource, TransactionStatusStreamFor, TxHash, TxInvalidityReportMap,
};
use sp_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: sp_api::ProvideRuntimeApi<Block>
+ sc_client_api::BlockBackend<Block>
+ sc_client_api::blockchain::HeaderBackend<Block>
+ sp_runtime::traits::BlockIdTo<Block>
+ sp_blockchain::HeaderMetadata<Block, Error = sp_blockchain::Error>
+ 'static,
Client::Api: sp_transaction_pool::runtime_api::TaggedTransactionQueue<Block>;
#[async_trait]
impl<Block, Client> TransactionPool for TransactionPoolWrapper<Block, Client>
where
Block: BlockT,
Client: sp_api::ProvideRuntimeApi<Block>
+ sc_client_api::BlockBackend<Block>
+ sc_client_api::blockchain::HeaderBackend<Block>
+ sp_runtime::traits::BlockIdTo<Block>
+ sp_blockchain::HeaderMetadata<Block, Error = sp_blockchain::Error>
+ 'static,
Client::Api: sp_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: sp_api::ProvideRuntimeApi<Block>
+ sc_client_api::BlockBackend<Block>
+ sc_client_api::blockchain::HeaderBackend<Block>
+ sp_runtime::traits::BlockIdTo<Block>
+ sp_blockchain::HeaderMetadata<Block, Error = sp_blockchain::Error>
+ 'static,
Client::Api: sp_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: sp_api::ProvideRuntimeApi<Block>
+ sc_client_api::BlockBackend<Block>
+ sc_client_api::blockchain::HeaderBackend<Block>
+ sp_runtime::traits::BlockIdTo<Block>
+ sp_blockchain::HeaderMetadata<Block, Error = sp_blockchain::Error>
+ 'static,
Client::Api: sp_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)
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,341 @@
// This file is part of Substrate.
// 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/>.
//! Tests for fork-aware transaction pool.
use sc_transaction_pool::{ChainApi, PoolLimit};
use sc_transaction_pool_api::ChainEvent;
use sp_runtime::transaction_validity::TransactionSource;
use std::sync::Arc;
use substrate_test_runtime_client::{
runtime::{Block, Hash, Header},
Sr25519Keyring::*,
};
use substrate_test_runtime_transaction_pool::{uxt, TestApi};
pub const LOG_TARGET: &str = "txpool";
use sc_transaction_pool::ForkAwareTxPool;
pub fn invalid_hash() -> Hash {
Default::default()
}
pub fn new_best_block_event(
pool: &ForkAwareTxPool<TestApi, Block>,
from: Option<Hash>,
to: Hash,
) -> ChainEvent<Block> {
ChainEvent::NewBestBlock {
hash: to,
tree_route: from.map(|from| {
// note: real tree route in NewBestBlock event does not contain 'to' block.
Arc::from(
pool.api()
.tree_route(from, pool.api().block_header(to).unwrap().unwrap().parent_hash)
.expect("Tree route exists"),
)
}),
}
}
pub fn finalized_block_event(
pool: &ForkAwareTxPool<TestApi, Block>,
from: Hash,
to: Hash,
) -> ChainEvent<Block> {
let t = pool.api().tree_route(from, to).expect("Tree route exists");
let e = t.enacted().iter().map(|h| h.hash).collect::<Vec<_>>();
ChainEvent::Finalized { hash: to, tree_route: Arc::from(&e[0..e.len() - 1]) }
}
pub struct TestPoolBuilder {
api: Option<Arc<TestApi>>,
use_default_limits: bool,
ready_limits: sc_transaction_pool::PoolLimit,
future_limits: sc_transaction_pool::PoolLimit,
mempool_max_transactions_count: usize,
finality_timeout_threshold: Option<usize>,
}
impl Default for TestPoolBuilder {
fn default() -> Self {
Self {
api: None,
use_default_limits: true,
ready_limits: PoolLimit { count: 8192, total_bytes: 20 * 1024 * 1024 },
future_limits: PoolLimit { count: 512, total_bytes: 1 * 1024 * 1024 },
mempool_max_transactions_count: usize::MAX,
finality_timeout_threshold: None,
}
}
}
impl TestPoolBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_api(mut self, api: Arc<TestApi>) -> Self {
self.api = Some(api);
self
}
pub fn with_mempool_count_limit(mut self, mempool_count_limit: usize) -> Self {
self.mempool_max_transactions_count = mempool_count_limit;
self.use_default_limits = false;
self
}
pub fn with_ready_count(mut self, ready_count: usize) -> Self {
self.ready_limits.count = ready_count;
self.use_default_limits = false;
self
}
pub fn with_ready_bytes_size(mut self, ready_bytes_size: usize) -> Self {
self.ready_limits.total_bytes = ready_bytes_size;
self.use_default_limits = false;
self
}
pub fn with_future_count(mut self, future_count: usize) -> Self {
self.future_limits.count = future_count;
self.use_default_limits = false;
self
}
pub fn with_future_bytes_size(mut self, future_bytes_size: usize) -> Self {
self.future_limits.total_bytes = future_bytes_size;
self.use_default_limits = false;
self
}
pub fn with_finality_timeout_threshold(mut self, threshold: usize) -> Self {
self.finality_timeout_threshold = Some(threshold);
self
}
pub fn build(
self,
) -> (ForkAwareTxPool<TestApi, Block>, Arc<TestApi>, futures::executor::ThreadPool) {
let api = self
.api
.unwrap_or(Arc::from(TestApi::with_alice_nonce(200).enable_stale_check()));
let genesis_hash = api
.chain()
.read()
.block_by_number
.get(&0)
.map(|blocks| blocks[0].0.header.hash())
.expect("there is block 0. qed");
let (pool, [txpool_task, blocking_task]) = if self.use_default_limits {
ForkAwareTxPool::new_test(
api.clone(),
genesis_hash,
genesis_hash,
self.finality_timeout_threshold,
)
} else {
ForkAwareTxPool::new_test_with_limits(
api.clone(),
genesis_hash,
genesis_hash,
self.ready_limits,
self.future_limits,
self.mempool_max_transactions_count,
self.finality_timeout_threshold,
)
};
let thread_pool = futures::executor::ThreadPool::new().unwrap();
thread_pool.spawn_ok(txpool_task);
thread_pool.spawn_ok(blocking_task);
(pool, api, thread_pool)
}
}
pub fn pool_with_api(
test_api: Arc<TestApi>,
) -> (ForkAwareTxPool<TestApi, Block>, futures::executor::ThreadPool) {
let builder = TestPoolBuilder::new();
let (pool, _, threadpool) = builder.with_api(test_api).build();
(pool, threadpool)
}
pub fn pool() -> (ForkAwareTxPool<TestApi, Block>, Arc<TestApi>, futures::executor::ThreadPool) {
let builder = TestPoolBuilder::new();
builder.build()
}
#[macro_export]
macro_rules! assert_pool_status {
($hash:expr, $pool:expr, $ready:expr, $future:expr) => {
{
tracing::debug!(target: LOG_TARGET, stats = ?$pool.status_all(), "Pool stats");
let status = &$pool.status_all()[&$hash];
assert_eq!(status.ready, $ready, "ready");
assert_eq!(status.future, $future, "future");
}
}
}
#[macro_export]
macro_rules! assert_ready_at_light_iterator {
($hash:expr, $pool:expr, [$( $xt:expr ),*]) => {{
let ready_iterator = $pool.ready_at_light($hash).now_or_never().unwrap();
let expected = vec![ $($pool.api().hash_and_length(&$xt).0),*];
let output: Vec<_> = ready_iterator.collect();
tracing::debug!(target: LOG_TARGET, ?expected, "expected");
tracing::debug!(target: LOG_TARGET, ?output, "output");
let output = output.into_iter().map(|t|t.hash).collect::<Vec<_>>();
assert_eq!(expected.len(), output.len());
assert_eq!(output,expected);
}};
}
#[macro_export]
macro_rules! assert_ready_iterator {
($hash:expr, $pool:expr, [$( $xt:expr ),*]) => {{
let ready_iterator = $pool.ready_at($hash).now_or_never().unwrap();
let expected = vec![ $($pool.api().hash_and_length(&$xt).0),*];
let output: Vec<_> = ready_iterator.collect();
tracing::debug!(target: LOG_TARGET, ?expected, "expected");
tracing::debug!(target: LOG_TARGET, ?output, "output");
let output = output.into_iter().map(|t|t.hash).collect::<Vec<_>>();
assert_eq!(expected.len(), output.len());
assert_eq!(output,expected);
}};
}
#[macro_export]
macro_rules! assert_future_iterator {
($hash:expr, $pool:expr, [$( $xt:expr ),*]) => {{
let futures = $pool.futures_at($hash).unwrap();
let expected = vec![ $($pool.api().hash_and_length(&$xt).0),*];
tracing::debug!(target: LOG_TARGET, ?expected, "expected");
tracing::debug!(target: LOG_TARGET, ?futures, "output");
assert_eq!(expected.len(), futures.len());
let hsf = futures.iter().map(|a| a.hash).collect::<std::collections::HashSet<_>>();
let hse = expected.into_iter().collect::<std::collections::HashSet<_>>();
assert_eq!(hse,hsf);
}};
}
#[macro_export]
macro_rules! assert_watcher_stream {
($stream:ident, [$( $event:expr ),*]) => {{
let expected = vec![ $($event),*];
tracing::debug!(
target: LOG_TARGET,
?expected,
expected_len = expected.len(),
"block now"
);
let output = futures::executor::block_on_stream($stream).take(expected.len()).collect::<Vec<_>>();
tracing::debug!(target: LOG_TARGET, ?output, "Output");
assert_eq!(expected.len(), output.len());
assert_eq!(output, expected);
}};
}
pub const SOURCE: TransactionSource = TransactionSource::External;
#[cfg(test)]
pub mod test_chain_with_forks {
use super::*;
use tracing::debug;
pub fn chain(
include_xts: Option<&dyn Fn(usize, usize) -> bool>,
) -> (Arc<TestApi>, Vec<Vec<Header>>) {
// Fork layout:
//
// (fork 0)
// F01 - F02 - F03 - F04 - F05 | Alice nonce increasing, alice's txs
// /
// F00
// \ (fork 1)
// F11 - F12 - F13 - F14 - F15 | Bob nonce increasing, Bob's txs
//
//
// e.g. F03 contains uxt(Alice, 202), nonces: Alice = 203, Bob = 200
// F12 contains uxt(Bob, 201), nonces: Alice = 200, Bob = 202
let api = Arc::from(TestApi::empty().enable_stale_check());
let genesis = api.genesis_hash();
let mut forks = vec![Vec::with_capacity(6), Vec::with_capacity(6)];
let accounts = vec![Alice, Bob];
accounts.iter().for_each(|a| api.set_nonce(genesis, (*a).into(), 200));
for fork in 0..2 {
let account = accounts[fork];
forks[fork].push(api.block_header(genesis).unwrap().unwrap());
let mut parent = genesis;
for block in 1..6 {
let xts = if include_xts.map_or(true, |v| v(fork, block)) {
debug!(fork = %fork, block = %block, "-> add");
vec![uxt(account, (200 + block - 1) as u64)]
} else {
debug!(fork = %fork, block = %block, "-> skip");
vec![]
};
let header = api.push_block_with_parent(parent, xts, true);
parent = header.hash();
api.set_nonce(header.hash(), account.into(), (200 + block) as u64);
forks[fork].push(header);
}
}
(api, forks)
}
pub fn print_block(api: Arc<TestApi>, hash: Hash) {
let accounts = vec![Alice.into(), Bob.into()];
let header = api.block_header(hash).unwrap().unwrap();
let nonces = accounts
.iter()
.map(|a| api.chain().read().nonces.get(&hash).unwrap().get(a).map(Clone::clone))
.collect::<Vec<_>>();
debug!(
number = ?header.number,
hash = ?header.hash(),
parent = ?header.parent_hash,
?nonces
);
}
#[test]
fn test_chain_works() {
sp_tracing::try_init_simple();
let (api, f) = chain(None);
debug!(forks = ?f, "forks");
f[0].iter().for_each(|h| print_block(api.clone(), h.hash()));
f[1].iter().for_each(|h| print_block(api.clone(), h.hash()));
let tr = api.tree_route(f[0][5].hash(), f[1][5].hash()).unwrap();
debug!(?tr);
debug!(enacted = ?tr.enacted());
debug!(retracted = ?tr.retracted());
}
}
@@ -0,0 +1,273 @@
// This file is part of Substrate.
// 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/>.
//! Tests for finality timeout handling for fork-aware transaction pool.
pub mod fatp_common;
use std::cmp::min;
use fatp_common::{
finalized_block_event, invalid_hash, new_best_block_event, TestPoolBuilder, LOG_TARGET, SOURCE,
};
use futures::{executor::block_on, FutureExt};
use sc_transaction_pool::ChainApi;
use sc_transaction_pool_api::{MaintainedTransactionPool, TransactionPool, TransactionStatus};
use substrate_test_runtime_client::Sr25519Keyring::*;
use substrate_test_runtime_transaction_pool::uxt;
#[test]
fn fatp_finality_timeout_works() {
sp_tracing::try_init_simple();
const FINALITY_TIMEOUT_THRESHOLD: usize = 10;
let (pool, api, _) = TestPoolBuilder::new()
.with_finality_timeout_threshold(FINALITY_TIMEOUT_THRESHOLD)
.build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
let header01 = api.push_block(1, vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, None, header01.hash())));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Bob, 300);
let xt2 = uxt(Charlie, 400);
let xt3 = uxt(Dave, 500);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let xt3_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
assert_pool_status!(header01.hash(), &pool, 4, 0);
assert_ready_iterator!(header01.hash(), pool, [xt0, xt1, xt2, xt3]);
let header02a = api.push_block_with_parent(
header01.hash(),
vec![xt0.clone(), xt1.clone(), xt2.clone(), xt3.clone()],
true,
);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02a.hash())));
assert_pool_status!(header02a.hash(), &pool, 0, 0);
let header02b = api.push_block_with_parent(header01.hash(), vec![xt0, xt1, xt2, xt3], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header02a.hash()), header02b.hash())));
assert_pool_status!(header02b.hash(), &pool, 0, 0);
let mut prev_header = header02b.clone();
for n in 3..66 {
let header = api.push_block_with_parent(prev_header.hash(), vec![], true);
let event = new_best_block_event(&pool, Some(prev_header.hash()), header.hash());
block_on(pool.maintain(event));
prev_header = header;
if n < 3 + FINALITY_TIMEOUT_THRESHOLD {
assert_eq!(pool.active_views_count(), 2);
} else {
assert_eq!(pool.active_views_count(), 1);
assert_eq!(pool.inactive_views_count(), FINALITY_TIMEOUT_THRESHOLD);
}
}
for (i, watcher) in
vec![xt0_watcher, xt1_watcher, xt2_watcher, xt3_watcher].into_iter().enumerate()
{
assert_watcher_stream!(
watcher,
[
TransactionStatus::Ready,
TransactionStatus::InBlock((header02a.hash(), i)),
TransactionStatus::InBlock((header02b.hash(), i)),
TransactionStatus::FinalityTimeout(min(header02a.hash(), header02b.hash()))
]
);
}
}
#[test]
fn fatp_finalized_still_works_after_finality_stall() {
sp_tracing::try_init_simple();
const FINALITY_TIMEOUT_THRESHOLD: usize = 10;
let (pool, api, _) = TestPoolBuilder::new()
.with_finality_timeout_threshold(FINALITY_TIMEOUT_THRESHOLD)
.build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
let header01 = api.push_block(1, vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, None, header01.hash())));
block_on(pool.maintain(finalized_block_event(&pool, api.genesis_hash(), header01.hash())));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Bob, 300);
let xt2 = uxt(Charlie, 400);
let xt3 = uxt(Dave, 500);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let xt3_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
assert_pool_status!(header01.hash(), &pool, 4, 0);
assert_ready_iterator!(header01.hash(), pool, [xt0, xt1, xt2, xt3]);
let header02a = api.push_block_with_parent(
header01.hash(),
vec![xt0.clone(), xt1.clone(), xt2.clone()],
true,
);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02a.hash())));
assert_pool_status!(header02a.hash(), &pool, 1, 0);
let header02b = api.push_block_with_parent(header01.hash(), vec![xt0, xt1, xt2], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header02a.hash()), header02b.hash())));
assert_pool_status!(header02b.hash(), &pool, 1, 0);
let header03b = api.push_block_with_parent(header02b.hash(), vec![xt3], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header02b.hash()), header03b.hash())));
assert_pool_status!(header03b.hash(), &pool, 0, 0);
let mut prev_header = header03b.clone();
for block_n in 4..=3 + FINALITY_TIMEOUT_THRESHOLD {
let header = api.push_block_with_parent(prev_header.hash(), vec![], true);
let event = new_best_block_event(&pool, Some(prev_header.hash()), header.hash());
block_on(pool.maintain(event));
prev_header = header;
if block_n == 3 + FINALITY_TIMEOUT_THRESHOLD {
//finality timeout triggered
assert_eq!(pool.active_views_count(), 1);
assert_eq!(pool.inactive_views_count(), FINALITY_TIMEOUT_THRESHOLD);
} else {
assert_eq!(pool.active_views_count(), 2);
}
}
block_on(pool.maintain(finalized_block_event(&pool, header01.hash(), header03b.hash())));
for (i, watcher) in vec![xt0_watcher, xt1_watcher, xt2_watcher].into_iter().enumerate() {
assert_watcher_stream!(
watcher,
[
TransactionStatus::Ready,
TransactionStatus::InBlock((header02a.hash(), i)),
TransactionStatus::InBlock((header02b.hash(), i)),
TransactionStatus::FinalityTimeout(min(header02a.hash(), header02b.hash()))
]
);
}
assert_watcher_stream!(
xt3_watcher,
[
TransactionStatus::Ready,
TransactionStatus::InBlock((header03b.hash(), 0)),
TransactionStatus::Finalized((header03b.hash(), 0))
]
);
}
#[test]
fn fatp_finality_timeout_works_for_txs_included_before_finalized() {
sp_tracing::try_init_simple();
const FINALITY_TIMEOUT_THRESHOLD: usize = 10;
let (pool, api, _) = TestPoolBuilder::new()
.with_finality_timeout_threshold(FINALITY_TIMEOUT_THRESHOLD)
.build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
let header01 = api.push_block(1, vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, None, header01.hash())));
block_on(pool.maintain(finalized_block_event(&pool, api.genesis_hash(), header01.hash())));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Bob, 300);
let xt2 = uxt(Charlie, 400);
let xt3 = uxt(Dave, 500);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let xt3_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
assert_pool_status!(header01.hash(), &pool, 4, 0);
assert_ready_iterator!(header01.hash(), pool, [xt0, xt1, xt2, xt3]);
let header02a = api.push_block_with_parent(
header01.hash(),
vec![xt0.clone(), xt1.clone(), xt2.clone()],
true,
);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02a.hash())));
assert_pool_status!(header02a.hash(), &pool, 1, 0);
let header02b = api.push_block_with_parent(header01.hash(), vec![xt0, xt1, xt2], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header02a.hash()), header02b.hash())));
assert_pool_status!(header02b.hash(), &pool, 1, 0);
let header03b = api.push_block_with_parent(header02b.hash(), vec![xt3], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header02b.hash()), header03b.hash())));
assert_pool_status!(header03b.hash(), &pool, 0, 0);
block_on(pool.maintain(finalized_block_event(&pool, header01.hash(), header02b.hash())));
let mut prev_header = header03b.clone();
for block_n in 4..=4 + FINALITY_TIMEOUT_THRESHOLD {
let header = api.push_block_with_parent(prev_header.hash(), vec![], true);
let event = new_best_block_event(&pool, Some(prev_header.hash()), header.hash());
block_on(pool.maintain(event));
prev_header = header;
assert_eq!(pool.active_views_count(), 1);
if block_n == 4 + FINALITY_TIMEOUT_THRESHOLD {
//finality timeout triggered
assert_eq!(pool.inactive_views_count(), FINALITY_TIMEOUT_THRESHOLD);
}
}
for (i, watcher) in vec![xt0_watcher, xt1_watcher, xt2_watcher].into_iter().enumerate() {
assert_watcher_stream!(
watcher,
[
TransactionStatus::Ready,
TransactionStatus::InBlock((header02a.hash(), i)),
TransactionStatus::InBlock((header02b.hash(), i)),
TransactionStatus::Finalized((header02b.hash(), i))
]
);
}
assert_watcher_stream!(
xt3_watcher,
[
TransactionStatus::Ready,
TransactionStatus::InBlock((header03b.hash(), 0)),
TransactionStatus::FinalityTimeout(header03b.hash())
]
);
}
@@ -0,0 +1,673 @@
// This file is part of Substrate.
// 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/>.
//! Tests of invalid transactions handling for fork-aware transaction pool.
pub mod fatp_common;
use fatp_common::{
finalized_block_event, invalid_hash, new_best_block_event, pool, TestPoolBuilder, LOG_TARGET,
SOURCE,
};
use futures::{executor::block_on, FutureExt};
use sc_transaction_pool::ChainApi;
use sc_transaction_pool_api::{
error::{Error as TxPoolError, IntoPoolError},
MaintainedTransactionPool, TransactionPool, TransactionStatus,
};
use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidityError};
use substrate_test_runtime_client::Sr25519Keyring::*;
use substrate_test_runtime_transaction_pool::uxt;
use tracing::debug;
#[test]
fn fatp_invalid_three_views_stale_gets_rejected() {
sp_tracing::try_init_simple();
let (pool, api, _) = pool();
let header01 = api.push_block(1, vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, None, header01.hash())));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Alice, 200);
let header02a = api.push_block_with_parent(header01.hash(), vec![], true);
let header02b = api.push_block_with_parent(header01.hash(), vec![], true);
let header02c = api.push_block_with_parent(header01.hash(), vec![], true);
api.set_nonce(header02a.hash(), Alice.into(), 201);
api.set_nonce(header02b.hash(), Alice.into(), 201);
api.set_nonce(header02c.hash(), Alice.into(), 201);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02a.hash())));
block_on(pool.maintain(new_best_block_event(&pool, Some(header02a.hash()), header02b.hash())));
block_on(pool.maintain(new_best_block_event(&pool, Some(header02b.hash()), header02c.hash())));
let result0 = block_on(pool.submit_one(invalid_hash(), SOURCE, xt0.clone()));
let result1 = block_on(pool.submit_one(invalid_hash(), SOURCE, xt1.clone()));
assert!(matches!(
result0.as_ref().unwrap_err().0,
TxPoolError::InvalidTransaction(InvalidTransaction::Stale)
));
assert!(matches!(
result1.as_ref().unwrap_err().0,
TxPoolError::InvalidTransaction(InvalidTransaction::Stale)
));
}
#[test]
fn fatp_invalid_three_views_invalid_gets_rejected() {
sp_tracing::try_init_simple();
let (pool, api, _) = pool();
let header01 = api.push_block(1, vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, None, header01.hash())));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Alice, 200);
let header02a = api.push_block_with_parent(header01.hash(), vec![], true);
let header02b = api.push_block_with_parent(header01.hash(), vec![], true);
let header02c = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02a.hash())));
block_on(pool.maintain(new_best_block_event(&pool, Some(header02a.hash()), header02b.hash())));
block_on(pool.maintain(new_best_block_event(&pool, Some(header02b.hash()), header02c.hash())));
api.add_invalid(&xt0);
api.add_invalid(&xt1);
let result0 = block_on(pool.submit_one(invalid_hash(), SOURCE, xt0.clone()));
let result1 = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).map(|_| ());
assert!(matches!(
result0.as_ref().unwrap_err().0,
TxPoolError::InvalidTransaction(InvalidTransaction::Custom(_))
));
assert!(matches!(
result1.as_ref().unwrap_err().0,
TxPoolError::InvalidTransaction(InvalidTransaction::Custom(_))
));
}
#[test]
fn fatp_transactions_purging_invalid_on_finalization_works() {
sp_tracing::try_init_simple();
let (pool, api, _) = pool();
let xt1 = uxt(Alice, 200);
let xt2 = uxt(Alice, 201);
let xt3 = uxt(Alice, 202);
let header01 = api.push_block(1, vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, None, header01.hash())));
let watcher1 = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let watcher2 = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let watcher3 = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
assert_eq!(api.validation_requests().len(), 3);
assert_eq!(pool.status_all()[&header01.hash()].ready, 3);
assert_eq!(block_on(pool.mempool_len()), (0, 3));
let header02 = api.push_block(2, vec![], true);
api.add_invalid(&xt1);
api.add_invalid(&xt2);
api.add_invalid(&xt3);
block_on(pool.maintain(finalized_block_event(&pool, header01.hash(), header02.hash())));
// wait 10 blocks for revalidation
let mut prev_header = header02.clone();
for n in 3..=11 {
let header = api.push_block(n, vec![], true);
let event = finalized_block_event(&pool, prev_header.hash(), header.hash());
block_on(pool.maintain(event));
prev_header = header;
}
assert_eq!(block_on(pool.mempool_len()), (0, 0));
assert_watcher_stream!(watcher1, [TransactionStatus::Ready, TransactionStatus::Invalid]);
assert_watcher_stream!(watcher2, [TransactionStatus::Ready, TransactionStatus::Invalid]);
assert_watcher_stream!(watcher3, [TransactionStatus::Ready, TransactionStatus::Invalid]);
}
#[test]
fn fatp_transactions_purging_invalid_on_finalization_works2() {
sp_tracing::try_init_simple();
let (pool, api, _) = pool();
let xt1 = uxt(Alice, 200);
let xt2 = uxt(Alice, 201);
let header01 = api.push_block(1, vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, None, header01.hash())));
block_on(pool.submit_one(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
assert_eq!(pool.status_all()[&header01.hash()].ready, 2);
assert_eq!(api.validation_requests().len(), 2);
let header02 = api.push_block(2, vec![xt1.clone()], true);
api.add_invalid(&xt2);
block_on(pool.maintain(finalized_block_event(&pool, api.genesis_hash(), header02.hash())));
assert_eq!(pool.status_all()[&header02.hash()].ready, 1);
// wait 10 blocks for revalidation
let mut prev_header = header02.clone();
for _ in 3..=11 {
let header = api.push_block_with_parent(prev_header.hash(), vec![], true);
let event = finalized_block_event(&pool, prev_header.hash(), header.hash());
block_on(pool.maintain(event));
prev_header = header;
}
assert_watcher_stream!(watcher, [TransactionStatus::Ready, TransactionStatus::Invalid]);
assert_eq!(pool.status_all()[&prev_header.hash()].ready, 0);
}
#[test]
fn should_not_retain_invalid_hashes_from_retracted() {
sp_tracing::try_init_simple();
let (pool, api, _) = pool();
let xt = uxt(Alice, 200);
let header01 = api.push_block(1, vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, None, header01.hash())));
let watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt.clone())).unwrap();
let header02a = api.push_block_with_parent(header01.hash(), vec![xt.clone()], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02a.hash())));
assert_eq!(pool.status_all()[&header02a.hash()].ready, 0);
api.add_invalid(&xt);
let header02b = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(finalized_block_event(&pool, api.genesis_hash(), header02b.hash())));
// wait 10 blocks for revalidation
let mut prev_header = header02b.clone();
for _ in 3..=11 {
let header = api.push_block_with_parent(prev_header.hash(), vec![], true);
let event = finalized_block_event(&pool, prev_header.hash(), header.hash());
block_on(pool.maintain(event));
prev_header = header;
}
assert_watcher_stream!(
watcher,
[
TransactionStatus::Ready,
TransactionStatus::InBlock((header02a.hash(), 0)),
TransactionStatus::Invalid
]
);
//todo: shall revalidation check finalized (fork's tip) view?
assert_eq!(pool.status_all()[&prev_header.hash()].ready, 0);
}
#[test]
fn fatp_watcher_invalid_many_revalidation() {
sp_tracing::try_init_simple();
let (pool, api, _) = pool();
let header01 = api.push_block(1, vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, None, header01.hash())));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Alice, 201);
let xt2 = uxt(Alice, 202);
let xt3 = uxt(Alice, 203);
let xt4 = uxt(Alice, 204);
let submissions = vec![
pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone()),
pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone()),
pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone()),
pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone()),
pool.submit_and_watch(invalid_hash(), SOURCE, xt4.clone()),
];
let submissions = block_on(futures::future::join_all(submissions));
assert_eq!(pool.status_all()[&header01.hash()].ready, 5);
let mut watchers = submissions.into_iter().map(Result::unwrap).collect::<Vec<_>>();
let xt4_watcher = watchers.remove(4);
let xt3_watcher = watchers.remove(3);
let xt2_watcher = watchers.remove(2);
let xt1_watcher = watchers.remove(1);
let xt0_watcher = watchers.remove(0);
api.add_invalid(&xt3);
api.add_invalid(&xt4);
let header02 = api.push_block(2, vec![], true);
block_on(pool.maintain(finalized_block_event(&pool, header01.hash(), header02.hash())));
//todo: shall revalidation check finalized (fork's tip) view?
assert_eq!(pool.status_all()[&header02.hash()].ready, 5);
let header03 = api.push_block(3, vec![xt0.clone(), xt1.clone(), xt2.clone()], true);
block_on(pool.maintain(finalized_block_event(&pool, header02.hash(), header03.hash())));
// wait 10 blocks for revalidation
let mut prev_header = header03.clone();
for n in 4..=11 {
let header = api.push_block(n, vec![], true);
let event = finalized_block_event(&pool, prev_header.hash(), header.hash());
block_on(pool.maintain(event));
prev_header = header;
}
assert_watcher_stream!(
xt0_watcher,
[
TransactionStatus::Ready,
TransactionStatus::InBlock((header03.hash(), 0)),
TransactionStatus::Finalized((header03.hash(), 0))
]
);
assert_watcher_stream!(
xt1_watcher,
[
TransactionStatus::Ready,
TransactionStatus::InBlock((header03.hash(), 1)),
TransactionStatus::Finalized((header03.hash(), 1))
]
);
assert_watcher_stream!(
xt2_watcher,
[
TransactionStatus::Ready,
TransactionStatus::InBlock((header03.hash(), 2)),
TransactionStatus::Finalized((header03.hash(), 2))
]
);
assert_watcher_stream!(xt3_watcher, [TransactionStatus::Ready, TransactionStatus::Invalid]);
assert_watcher_stream!(xt4_watcher, [TransactionStatus::Ready, TransactionStatus::Invalid]);
}
#[test]
fn fatp_watcher_invalid_fails_on_submission() {
sp_tracing::try_init_simple();
let (pool, api, _) = pool();
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Alice, 150);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone()));
let xt0_watcher = xt0_watcher.map(|_| ());
assert_pool_status!(header01.hash(), &pool, 0, 0);
// Alice's nonce in state is 200, tx is 150.
assert!(matches!(
xt0_watcher.unwrap_err().into_pool_error(),
Ok(TxPoolError::InvalidTransaction(InvalidTransaction::Stale))
));
}
#[test]
fn fatp_watcher_invalid_single_revalidation() {
sp_tracing::try_init_simple();
let (pool, api, _) = pool();
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, Some(api.genesis_hash()), header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Alice, 200);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
api.add_invalid(&xt0);
let header02 = api.push_block_with_parent(header01.hash(), vec![], true);
let event = finalized_block_event(&pool, header01.hash(), header02.hash());
block_on(pool.maintain(event));
// wait 10 blocks for revalidation
let mut prev_header = header02;
for n in 3..=11 {
let header = api.push_block(n, vec![], true);
let event = finalized_block_event(&pool, prev_header.hash(), header.hash());
block_on(pool.maintain(event));
prev_header = header;
}
let xt0_events = futures::executor::block_on_stream(xt0_watcher).collect::<Vec<_>>();
debug!(target: LOG_TARGET, ?xt0_events, "xt0_events");
assert_eq!(xt0_events, vec![TransactionStatus::Ready, TransactionStatus::Invalid]);
}
#[test]
fn fatp_watcher_invalid_single_revalidation2() {
sp_tracing::try_init_simple();
let (pool, api, _) = pool();
let xt0 = uxt(Alice, 200);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
assert_eq!(block_on(pool.mempool_len()), (0, 1));
let header01 = api.push_block(1, vec![], true);
let event = finalized_block_event(&pool, api.genesis_hash(), header01.hash());
block_on(pool.maintain(event));
api.add_invalid(&xt0);
// note: the tx will be revalidated in view::revalidation, not in mempool revalidation (which
// would require waiting 10 blocks).
// waiting 10 blocks is excessive, but we may want to keep it.
let mut prev_header = header01;
for n in 2..=11 {
let header = api.push_block(n, vec![], true);
let event = finalized_block_event(&pool, prev_header.hash(), header.hash());
block_on(pool.maintain(event));
prev_header = header;
}
let xt0_events = futures::executor::block_on_stream(xt0_watcher).collect::<Vec<_>>();
debug!(target: LOG_TARGET, ?xt0_events, "xt0_events");
assert_eq!(xt0_events, vec![TransactionStatus::Ready, TransactionStatus::Invalid]);
assert_eq!(block_on(pool.mempool_len()), (0, 0));
}
#[test]
fn fatp_invalid_report_stale_or_future_works_as_expected() {
sp_tracing::try_init_simple();
let (pool, api, _) = TestPoolBuilder::new().build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
let header01 = api.push_block(1, vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, None, header01.hash())));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Bob, 300);
let xt2 = uxt(Charlie, 400);
let xt3 = uxt(Dave, 500);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let xt3_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
assert_pool_status!(header01.hash(), &pool, 4, 0);
assert_ready_iterator!(header01.hash(), pool, [xt0, xt1, xt2, xt3]);
// future/stale are ignored when at is None
let xt0_report = (
pool.api().hash_and_length(&xt0).0,
Some(TransactionValidityError::Invalid(InvalidTransaction::Future)),
);
let invalid_txs = [xt0_report].into();
let result = block_on(pool.report_invalid(None, invalid_txs));
assert!(result.is_empty());
assert_ready_iterator!(header01.hash(), pool, [xt0, xt1, xt2, xt3]);
// future/stale are applied when at is provided
let xt0_report = (
pool.api().hash_and_length(&xt0).0,
Some(TransactionValidityError::Invalid(InvalidTransaction::Future)),
);
let xt1_report = (
pool.api().hash_and_length(&xt1).0,
Some(TransactionValidityError::Invalid(InvalidTransaction::Stale)),
);
let invalid_txs = [xt0_report, xt1_report].into();
let result = block_on(pool.report_invalid(Some(header01.hash()), invalid_txs));
// stale/future does not cause tx to be removed from the pool
assert!(result.is_empty());
// assert_eq!(result[0].hash, pool.api().hash_and_length(&xt0).0);
assert_ready_iterator!(header01.hash(), pool, [xt2, xt3]);
// None error means force removal
// todo
assert_watcher_stream!(xt0_watcher, [TransactionStatus::Ready]);
assert_watcher_stream!(xt1_watcher, [TransactionStatus::Ready]);
assert_watcher_stream!(xt2_watcher, [TransactionStatus::Ready]);
assert_watcher_stream!(xt3_watcher, [TransactionStatus::Ready]);
}
#[test]
fn fatp_invalid_report_future_dont_remove_from_pool() {
sp_tracing::try_init_simple();
let (pool, api, _) = TestPoolBuilder::new().build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
api.set_nonce(api.genesis_hash(), Eve.into(), 600);
let header01 = api.push_block(1, vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, None, header01.hash())));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Bob, 300);
let xt2 = uxt(Charlie, 400);
let xt3 = uxt(Dave, 500);
let xt4 = uxt(Eve, 600);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let xt3_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
let xt4_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt4.clone())).unwrap();
assert_pool_status!(header01.hash(), &pool, 5, 0);
assert_ready_iterator!(header01.hash(), pool, [xt0, xt1, xt2, xt3, xt4]);
let header02 = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02.hash())));
assert_pool_status!(header02.hash(), &pool, 5, 0);
assert_ready_iterator!(header02.hash(), pool, [xt0, xt1, xt2, xt3, xt4]);
let xt0_report = (
pool.api().hash_and_length(&xt0).0,
Some(TransactionValidityError::Invalid(InvalidTransaction::Stale)),
);
let xt1_report = (
pool.api().hash_and_length(&xt1).0,
Some(TransactionValidityError::Invalid(InvalidTransaction::Future)),
);
let xt4_report = (
pool.api().hash_and_length(&xt4).0,
Some(TransactionValidityError::Invalid(InvalidTransaction::BadProof)),
);
let invalid_txs = [xt0_report, xt1_report, xt4_report].into();
let result = block_on(pool.report_invalid(Some(header01.hash()), invalid_txs));
assert_watcher_stream!(xt4_watcher, [TransactionStatus::Ready, TransactionStatus::Invalid]);
// future does not cause tx to be removed from the pool
assert!(result.len() == 1);
assert!(result[0].hash == pool.api().hash_and_length(&xt4).0);
assert_ready_iterator!(header01.hash(), pool, [xt2, xt3]);
assert_pool_status!(header02.hash(), &pool, 4, 0);
assert_ready_iterator!(header02.hash(), pool, [xt0, xt1, xt2, xt3]);
assert_watcher_stream!(xt0_watcher, [TransactionStatus::Ready]);
assert_watcher_stream!(xt1_watcher, [TransactionStatus::Ready]);
assert_watcher_stream!(xt2_watcher, [TransactionStatus::Ready]);
assert_watcher_stream!(xt3_watcher, [TransactionStatus::Ready]);
}
#[test]
fn fatp_invalid_tx_is_removed_from_the_pool() {
sp_tracing::try_init_simple();
let (pool, api, _) = TestPoolBuilder::new().build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
let header01 = api.push_block(1, vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, None, header01.hash())));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Bob, 300);
let xt2 = uxt(Charlie, 400);
let xt3 = uxt(Dave, 500);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let xt3_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
assert_pool_status!(header01.hash(), &pool, 4, 0);
assert_ready_iterator!(header01.hash(), pool, [xt0, xt1, xt2, xt3]);
let xt0_report = (
pool.api().hash_and_length(&xt0).0,
Some(TransactionValidityError::Invalid(InvalidTransaction::BadProof)),
);
let xt1_report = (pool.api().hash_and_length(&xt1).0, None);
let invalid_txs = [xt0_report, xt1_report].into();
let result = block_on(pool.report_invalid(Some(header01.hash()), invalid_txs));
assert!(result.iter().any(|tx| tx.hash == pool.api().hash_and_length(&xt0).0));
assert_pool_status!(header01.hash(), &pool, 2, 0);
assert_ready_iterator!(header01.hash(), pool, [xt2, xt3]);
let header02 = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02.hash())));
assert_pool_status!(header02.hash(), &pool, 2, 0);
assert_ready_iterator!(header02.hash(), pool, [xt2, xt3]);
assert_watcher_stream!(xt0_watcher, [TransactionStatus::Ready, TransactionStatus::Invalid]);
assert_watcher_stream!(xt1_watcher, [TransactionStatus::Ready, TransactionStatus::Invalid]);
assert_watcher_stream!(xt2_watcher, [TransactionStatus::Ready]);
assert_watcher_stream!(xt3_watcher, [TransactionStatus::Ready]);
}
#[test]
fn fatp_invalid_tx_is_removed_from_the_pool_future_subtree_stays() {
sp_tracing::try_init_simple();
let (pool, api, _) = TestPoolBuilder::new().build();
let header01 = api.push_block(1, vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, None, header01.hash())));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Alice, 201);
let xt2 = uxt(Alice, 202);
let xt3 = uxt(Alice, 203);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let xt3_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
assert_pool_status!(header01.hash(), &pool, 4, 0);
assert_ready_iterator!(header01.hash(), pool, [xt0, xt1, xt2, xt3]);
let xt0_report = (
pool.api().hash_and_length(&xt0).0,
Some(TransactionValidityError::Invalid(InvalidTransaction::BadProof)),
);
let invalid_txs = [xt0_report].into();
let result = block_on(pool.report_invalid(Some(header01.hash()), invalid_txs));
assert_eq!(result[0].hash, pool.api().hash_and_length(&xt0).0);
assert_pool_status!(header01.hash(), &pool, 0, 0);
assert_ready_iterator!(header01.hash(), pool, []);
let header02 = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02.hash())));
assert_pool_status!(header02.hash(), &pool, 0, 3);
assert_future_iterator!(header02.hash(), pool, [xt1, xt2, xt3]);
assert_watcher_stream!(xt0_watcher, [TransactionStatus::Ready, TransactionStatus::Invalid]);
assert_watcher_stream!(xt1_watcher, [TransactionStatus::Ready]);
assert_watcher_stream!(xt2_watcher, [TransactionStatus::Ready]);
assert_watcher_stream!(xt3_watcher, [TransactionStatus::Ready]);
}
#[test]
fn fatp_invalid_tx_is_removed_from_the_pool2() {
sp_tracing::try_init_simple();
let (pool, api, _) = TestPoolBuilder::new().build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
let header01 = api.push_block(1, vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, None, header01.hash())));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Bob, 300);
let xt2 = uxt(Charlie, 400);
let xt3 = uxt(Dave, 500);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let xt3_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
assert_pool_status!(header01.hash(), &pool, 4, 0);
assert_ready_iterator!(header01.hash(), pool, [xt0, xt1, xt2, xt3]);
let header02a = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02a.hash())));
assert_pool_status!(header02a.hash(), &pool, 4, 0);
assert_ready_iterator!(header02a.hash(), pool, [xt0, xt1, xt2, xt3]);
let header02b = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header02a.hash()), header02b.hash())));
assert_pool_status!(header02b.hash(), &pool, 4, 0);
assert_ready_iterator!(header02b.hash(), pool, [xt0, xt1, xt2, xt3]);
let xt0_report = (
pool.api().hash_and_length(&xt0).0,
Some(TransactionValidityError::Invalid(InvalidTransaction::BadProof)),
);
let xt1_report = (pool.api().hash_and_length(&xt1).0, None);
let invalid_txs = [xt0_report, xt1_report].into();
let result = block_on(pool.report_invalid(Some(header01.hash()), invalid_txs));
assert!(result.iter().any(|tx| tx.hash == pool.api().hash_and_length(&xt0).0));
assert_ready_iterator!(header01.hash(), pool, [xt2, xt3]);
assert_pool_status!(header02a.hash(), &pool, 2, 0);
assert_ready_iterator!(header02a.hash(), pool, [xt2, xt3]);
assert_pool_status!(header02b.hash(), &pool, 2, 0);
assert_ready_iterator!(header02b.hash(), pool, [xt2, xt3]);
let header03 = api.push_block_with_parent(header02b.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header02b.hash()), header03.hash())));
assert_pool_status!(header03.hash(), &pool, 2, 0);
assert_ready_iterator!(header03.hash(), pool, [xt2, xt3]);
assert_watcher_stream!(xt0_watcher, [TransactionStatus::Ready, TransactionStatus::Invalid]);
assert_watcher_stream!(xt1_watcher, [TransactionStatus::Ready, TransactionStatus::Invalid]);
assert_watcher_stream!(xt2_watcher, [TransactionStatus::Ready]);
assert_watcher_stream!(xt3_watcher, [TransactionStatus::Ready]);
}
@@ -0,0 +1,832 @@
// This file is part of Substrate.
// 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/>.
//! Tests of limits for fork-aware transaction pool.
pub mod fatp_common;
use fatp_common::{
finalized_block_event, invalid_hash, new_best_block_event, TestPoolBuilder, LOG_TARGET, SOURCE,
};
use futures::{executor::block_on, FutureExt};
use sc_transaction_pool::ChainApi;
use sc_transaction_pool_api::{
error::Error as TxPoolError, MaintainedTransactionPool, TransactionPool, TransactionStatus,
};
use std::thread::sleep;
use substrate_test_runtime_client::Sr25519Keyring::*;
use substrate_test_runtime_transaction_pool::uxt;
use tracing::debug;
#[test]
fn fatp_limits_no_views_mempool_count() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(2).build();
let header = api.push_block(1, vec![], true);
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Alice, 201);
let xt2 = uxt(Alice, 202);
let submissions = vec![
pool.submit_one(header.hash(), SOURCE, xt0.clone()),
pool.submit_one(header.hash(), SOURCE, xt1.clone()),
pool.submit_one(header.hash(), SOURCE, xt2.clone()),
];
let results = block_on(futures::future::join_all(submissions));
let mut results = results.iter();
assert!(results.next().unwrap().is_ok());
assert!(results.next().unwrap().is_ok());
assert!(matches!(
results.next().unwrap().as_ref().unwrap_err().0,
TxPoolError::ImmediatelyDropped
));
}
#[test]
fn fatp_limits_ready_count_works() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(3).with_ready_count(2).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 200);
api.set_nonce(api.genesis_hash(), Charlie.into(), 500);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
//note: we need Charlie to be first as the oldest is removed.
//For 3x alice, all tree would be removed.
//(alice,bob,charlie would work too)
let xt0 = uxt(Charlie, 500);
let xt1 = uxt(Alice, 200);
let xt2 = uxt(Alice, 201);
let submissions = vec![
pool.submit_one(header01.hash(), SOURCE, xt0.clone()),
pool.submit_one(header01.hash(), SOURCE, xt1.clone()),
pool.submit_one(header01.hash(), SOURCE, xt2.clone()),
];
let results = block_on(futures::future::join_all(submissions));
assert!(results.iter().all(Result::is_ok));
//charlie was not included into view:
assert_pool_status!(header01.hash(), &pool, 2, 0);
assert_ready_iterator!(header01.hash(), pool, [xt1, xt2]);
//todo: can we do better? We don't have API to check if event was processed internally.
let mut counter = 0;
while block_on(pool.mempool_len()).0 == 3 {
sleep(std::time::Duration::from_millis(1));
counter = counter + 1;
if counter > 20 {
assert!(false, "timeout");
}
}
assert_eq!(block_on(pool.mempool_len()).0, 2);
//branch with alice transactions:
let header02b = api.push_block(2, vec![xt1.clone(), xt2.clone()], true);
let event = new_best_block_event(&pool, Some(header01.hash()), header02b.hash());
block_on(pool.maintain(event));
assert_eq!(block_on(pool.mempool_len()).0, 2);
assert_pool_status!(header02b.hash(), &pool, 0, 0);
assert_ready_iterator!(header02b.hash(), pool, []);
//branch with alice/charlie transactions shall also work:
let header02a = api.push_block(2, vec![xt0.clone(), xt1.clone()], true);
api.set_nonce(header02a.hash(), Alice.into(), 201);
let event = new_best_block_event(&pool, Some(header02b.hash()), header02a.hash());
block_on(pool.maintain(event));
assert_eq!(block_on(pool.mempool_len()).0, 2);
// assert_pool_status!(header02a.hash(), &pool, 1, 0);
assert_ready_iterator!(header02a.hash(), pool, [xt2]);
}
#[test]
fn fatp_limits_ready_count_works_for_submit_at() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(3).with_ready_count(2).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 200);
api.set_nonce(api.genesis_hash(), Charlie.into(), 500);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Charlie, 500);
let xt1 = uxt(Alice, 200);
let xt2 = uxt(Alice, 201);
let results = block_on(pool.submit_at(
header01.hash(),
SOURCE,
vec![xt0.clone(), xt1.clone(), xt2.clone()],
))
.unwrap();
assert!(matches!(results[0].as_ref().unwrap_err().0, TxPoolError::ImmediatelyDropped));
assert!(results[1].as_ref().is_ok());
assert!(results[2].as_ref().is_ok());
assert_eq!(block_on(pool.mempool_len()).0, 2);
//charlie was not included into view:
assert_pool_status!(header01.hash(), &pool, 2, 0);
assert_ready_iterator!(header01.hash(), pool, [xt1, xt2]);
}
#[test]
fn fatp_limits_ready_count_works_for_submit_and_watch() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(3).with_ready_count(2).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 500);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Charlie, 500);
let xt1 = uxt(Alice, 200);
let xt2 = uxt(Bob, 300);
api.set_priority(&xt0, 2);
api.set_priority(&xt1, 2);
api.set_priority(&xt2, 1);
let result0 = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone()));
let result1 = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone()));
let result2 = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).map(|_| ());
assert!(matches!(result2.unwrap_err().0, TxPoolError::ImmediatelyDropped));
assert!(result0.is_ok());
assert!(result1.is_ok());
assert_eq!(block_on(pool.mempool_len()).1, 2);
//charlie was not included into view:
assert_pool_status!(header01.hash(), &pool, 2, 0);
assert_ready_iterator!(header01.hash(), pool, [xt0, xt1]);
}
#[test]
fn fatp_limits_future_count_works() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(3).with_future_count(2).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 200);
api.set_nonce(api.genesis_hash(), Charlie.into(), 500);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Charlie, 501);
let xt2 = uxt(Alice, 201);
let xt3 = uxt(Alice, 202);
block_on(pool.submit_one(header01.hash(), SOURCE, xt1.clone())).unwrap();
block_on(pool.submit_one(header01.hash(), SOURCE, xt2.clone())).unwrap();
block_on(pool.submit_one(header01.hash(), SOURCE, xt3.clone())).unwrap();
//charlie was not included into view due to limits:
assert_pool_status!(header01.hash(), &pool, 0, 2);
//todo: can we do better? We don't have API to check if event was processed internally.
let mut counter = 0;
while block_on(pool.mempool_len()).0 != 2 {
sleep(std::time::Duration::from_millis(1));
counter = counter + 1;
if counter > 20 {
assert!(false, "timeout");
}
}
let header02 = api.push_block(2, vec![xt0], true);
api.set_nonce(header02.hash(), Alice.into(), 201); //redundant
let event = new_best_block_event(&pool, Some(header01.hash()), header02.hash());
block_on(pool.maintain(event));
assert_pool_status!(header02.hash(), &pool, 2, 0);
assert_eq!(block_on(pool.mempool_len()).0, 2);
}
#[test]
fn fatp_limits_watcher_mempool_doesnt_prevent_dropping() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(3).with_ready_count(2).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Charlie, 400);
let xt1 = uxt(Bob, 300);
let xt2 = uxt(Alice, 200);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
assert_pool_status!(header01.hash(), &pool, 2, 0);
let xt0_status = futures::executor::block_on_stream(xt0_watcher).take(2).collect::<Vec<_>>();
debug!(target: LOG_TARGET, ?xt0_status, "xt0_status");
assert_eq!(xt0_status, vec![TransactionStatus::Ready, TransactionStatus::Dropped]);
let xt1_status = futures::executor::block_on_stream(xt1_watcher).take(1).collect::<Vec<_>>();
assert_eq!(xt1_status, vec![TransactionStatus::Ready]);
let xt2_status = futures::executor::block_on_stream(xt2_watcher).take(1).collect::<Vec<_>>();
debug!(target: LOG_TARGET, ?xt2_status, "xt2_status");
assert_eq!(xt2_status, vec![TransactionStatus::Ready]);
}
#[test]
fn fatp_limits_watcher_non_intial_view_drops_transaction() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_ready_count(2).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Dave, 500);
let xt1 = uxt(Charlie, 400);
let xt2 = uxt(Bob, 300);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
// make sure tx0 is actually dropped before checking iterator
let xt0_status = futures::executor::block_on_stream(xt0_watcher).take(2).collect::<Vec<_>>();
assert_eq!(xt0_status, vec![TransactionStatus::Ready, TransactionStatus::Dropped]);
assert_ready_iterator!(header01.hash(), pool, [xt1, xt2]);
let header02 = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(finalized_block_event(&pool, api.genesis_hash(), header02.hash())));
assert_pool_status!(header02.hash(), &pool, 2, 0);
assert_ready_iterator!(header02.hash(), pool, [xt1, xt2]);
let xt1_status = futures::executor::block_on_stream(xt1_watcher).take(1).collect::<Vec<_>>();
assert_eq!(xt1_status, vec![TransactionStatus::Ready]);
let xt2_status = futures::executor::block_on_stream(xt2_watcher).take(1).collect::<Vec<_>>();
assert_eq!(xt2_status, vec![TransactionStatus::Ready]);
}
#[test]
fn fatp_limits_watcher_finalized_transaction_frees_ready_space() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_ready_count(2).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Dave, 500);
let xt1 = uxt(Charlie, 400);
let xt2 = uxt(Bob, 300);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
assert_ready_iterator!(header01.hash(), pool, [xt1, xt2]);
let xt0_status = futures::executor::block_on_stream(xt0_watcher).take(2).collect::<Vec<_>>();
assert_eq!(xt0_status, vec![TransactionStatus::Ready, TransactionStatus::Dropped]);
let header02 = api.push_block_with_parent(header01.hash(), vec![xt0.clone()], true);
block_on(pool.maintain(finalized_block_event(&pool, api.genesis_hash(), header02.hash())));
assert_pool_status!(header02.hash(), &pool, 2, 0);
assert_ready_iterator!(header02.hash(), pool, [xt1, xt2]);
let xt1_status = futures::executor::block_on_stream(xt1_watcher).take(1).collect::<Vec<_>>();
assert_eq!(xt1_status, vec![TransactionStatus::Ready]);
let xt2_status = futures::executor::block_on_stream(xt2_watcher).take(1).collect::<Vec<_>>();
assert_eq!(xt2_status, vec![TransactionStatus::Ready]);
}
#[test]
fn fatp_limits_watcher_view_can_drop_transcation() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(3).with_ready_count(2).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Dave, 500);
let xt1 = uxt(Charlie, 400);
let xt2 = uxt(Bob, 300);
let xt3 = uxt(Alice, 200);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let xt0_status = futures::executor::block_on_stream(xt0_watcher).take(2).collect::<Vec<_>>();
assert_eq!(xt0_status, vec![TransactionStatus::Ready, TransactionStatus::Dropped,]);
assert_ready_iterator!(header01.hash(), pool, [xt1, xt2]);
let xt3_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
let header02 = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(finalized_block_event(&pool, api.genesis_hash(), header02.hash())));
let xt1_status = futures::executor::block_on_stream(xt1_watcher).take(2).collect::<Vec<_>>();
assert_eq!(xt1_status, vec![TransactionStatus::Ready, TransactionStatus::Dropped]);
assert_pool_status!(header02.hash(), pool, 2, 0);
assert_ready_iterator!(header02.hash(), pool, [xt2, xt3]);
let xt2_status = futures::executor::block_on_stream(xt2_watcher).take(1).collect::<Vec<_>>();
assert_eq!(xt2_status, vec![TransactionStatus::Ready]);
let xt3_status = futures::executor::block_on_stream(xt3_watcher).take(1).collect::<Vec<_>>();
assert_eq!(xt3_status, vec![TransactionStatus::Ready]);
}
#[test]
fn fatp_limits_watcher_empty_and_full_view_immediately_drops() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(4).with_ready_count(2).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
api.set_nonce(api.genesis_hash(), Eve.into(), 600);
api.set_nonce(api.genesis_hash(), Ferdie.into(), 700);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Bob, 300);
let xt2 = uxt(Charlie, 400);
let xt3 = uxt(Dave, 500);
let xt4 = uxt(Eve, 600);
let xt5 = uxt(Ferdie, 700);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let xt0_status = futures::executor::block_on_stream(xt0_watcher).take(2).collect::<Vec<_>>();
assert_eq!(xt0_status, vec![TransactionStatus::Ready, TransactionStatus::Dropped]);
assert_pool_status!(header01.hash(), &pool, 2, 0);
assert_eq!(block_on(pool.mempool_len()).1, 2);
let header02e = api.push_block_with_parent(
header01.hash(),
vec![xt0.clone(), xt1.clone(), xt2.clone()],
true,
);
api.set_nonce(header02e.hash(), Alice.into(), 201);
api.set_nonce(header02e.hash(), Bob.into(), 301);
api.set_nonce(header02e.hash(), Charlie.into(), 401);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02e.hash())));
assert_pool_status!(header02e.hash(), &pool, 0, 0);
let header02f = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02f.hash())));
assert_pool_status!(header02f.hash(), &pool, 2, 0);
assert_ready_iterator!(header02f.hash(), pool, [xt1, xt2]);
let xt3_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
let xt4_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt4.clone())).unwrap();
let result5 = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt5.clone())).map(|_| ());
//xt5 hits internal mempool limit
assert!(matches!(result5.unwrap_err().0, TxPoolError::ImmediatelyDropped));
assert_pool_status!(header02e.hash(), &pool, 2, 0);
assert_ready_iterator!(header02e.hash(), pool, [xt3, xt4]);
assert_eq!(block_on(pool.mempool_len()).1, 4);
let xt1_status = futures::executor::block_on_stream(xt1_watcher).take(2).collect::<Vec<_>>();
assert_eq!(
xt1_status,
vec![TransactionStatus::Ready, TransactionStatus::InBlock((header02e.hash(), 1))]
);
let xt2_status = futures::executor::block_on_stream(xt2_watcher).take(2).collect::<Vec<_>>();
assert_eq!(
xt2_status,
vec![TransactionStatus::Ready, TransactionStatus::InBlock((header02e.hash(), 2))]
);
let xt3_status = futures::executor::block_on_stream(xt3_watcher).take(1).collect::<Vec<_>>();
assert_eq!(xt3_status, vec![TransactionStatus::Ready]);
let xt4_status = futures::executor::block_on_stream(xt4_watcher).take(1).collect::<Vec<_>>();
assert_eq!(xt4_status, vec![TransactionStatus::Ready]);
}
#[test]
fn fatp_limits_watcher_empty_and_full_view_drops_with_event() {
// it is almost copy of fatp_limits_watcher_empty_and_full_view_immediately_drops, but the
// mempool_count limit is set to 5 (vs 4).
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(5).with_ready_count(2).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
api.set_nonce(api.genesis_hash(), Eve.into(), 600);
api.set_nonce(api.genesis_hash(), Ferdie.into(), 700);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Bob, 300);
let xt2 = uxt(Charlie, 400);
let xt3 = uxt(Dave, 500);
let xt4 = uxt(Eve, 600);
let xt5 = uxt(Ferdie, 700);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let xt0_status = futures::executor::block_on_stream(xt0_watcher).take(2).collect::<Vec<_>>();
assert_eq!(xt0_status, vec![TransactionStatus::Ready, TransactionStatus::Dropped]);
assert_pool_status!(header01.hash(), &pool, 2, 0);
assert_eq!(block_on(pool.mempool_len()).1, 2);
let header02e = api.push_block_with_parent(
header01.hash(),
vec![xt0.clone(), xt1.clone(), xt2.clone()],
true,
);
api.set_nonce(header02e.hash(), Alice.into(), 201);
api.set_nonce(header02e.hash(), Bob.into(), 301);
api.set_nonce(header02e.hash(), Charlie.into(), 401);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02e.hash())));
assert_pool_status!(header02e.hash(), &pool, 0, 0);
let header02f = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02f.hash())));
assert_pool_status!(header02f.hash(), &pool, 2, 0);
assert_ready_iterator!(header02f.hash(), pool, [xt1, xt2]);
let xt3_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
let xt4_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt4.clone())).unwrap();
let xt5_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt5.clone())).unwrap();
assert_pool_status!(header02e.hash(), &pool, 2, 0);
assert_ready_iterator!(header02e.hash(), pool, [xt4, xt5]);
let xt3_status = futures::executor::block_on_stream(xt3_watcher).take(2).collect::<Vec<_>>();
assert_eq!(xt3_status, vec![TransactionStatus::Ready, TransactionStatus::Dropped]);
//xt5 got dropped
assert_eq!(block_on(pool.mempool_len()).1, 4);
let xt1_status = futures::executor::block_on_stream(xt1_watcher).take(2).collect::<Vec<_>>();
assert_eq!(
xt1_status,
vec![TransactionStatus::Ready, TransactionStatus::InBlock((header02e.hash(), 1))]
);
let xt2_status = futures::executor::block_on_stream(xt2_watcher).take(2).collect::<Vec<_>>();
assert_eq!(
xt2_status,
vec![TransactionStatus::Ready, TransactionStatus::InBlock((header02e.hash(), 2))]
);
let xt4_status = futures::executor::block_on_stream(xt4_watcher).take(1).collect::<Vec<_>>();
assert_eq!(xt4_status, vec![TransactionStatus::Ready]);
let xt5_status = futures::executor::block_on_stream(xt5_watcher).take(1).collect::<Vec<_>>();
assert_eq!(xt5_status, vec![TransactionStatus::Ready]);
}
fn large_uxt(x: usize) -> substrate_test_runtime::Extrinsic {
substrate_test_runtime::ExtrinsicBuilder::new_include_data(vec![x as u8; 1024]).build()
}
#[test]
fn fatp_limits_ready_size_works() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_ready_bytes_size(3390).with_future_bytes_size(0).build();
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = large_uxt(0);
let xt1 = large_uxt(1);
let xt2 = large_uxt(2);
let submissions = vec![
pool.submit_one(header01.hash(), SOURCE, xt0.clone()),
pool.submit_one(header01.hash(), SOURCE, xt1.clone()),
pool.submit_one(header01.hash(), SOURCE, xt2.clone()),
];
let results = block_on(futures::future::join_all(submissions));
assert!(results.iter().all(Result::is_ok));
//charlie was not included into view:
assert_pool_status!(header01.hash(), &pool, 3, 0);
assert_ready_iterator!(header01.hash(), pool, [xt0, xt1, xt2]);
let xt3 = large_uxt(3);
let result3 = block_on(pool.submit_one(header01.hash(), SOURCE, xt3.clone()));
assert!(matches!(result3.as_ref().unwrap_err().0, TxPoolError::ImmediatelyDropped));
}
#[test]
fn fatp_limits_future_size_works() {
sp_tracing::try_init_simple();
const UXT_SIZE: usize = 137;
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder
.with_ready_bytes_size(UXT_SIZE)
.with_future_bytes_size(3 * UXT_SIZE)
.build();
api.set_nonce(api.genesis_hash(), Bob.into(), 200);
api.set_nonce(api.genesis_hash(), Charlie.into(), 500);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Bob, 201);
let xt1 = uxt(Charlie, 501);
let xt2 = uxt(Alice, 201);
let xt3 = uxt(Alice, 202);
assert_eq!(api.hash_and_length(&xt0).1, UXT_SIZE);
assert_eq!(api.hash_and_length(&xt1).1, UXT_SIZE);
assert_eq!(api.hash_and_length(&xt2).1, UXT_SIZE);
assert_eq!(api.hash_and_length(&xt3).1, UXT_SIZE);
let _ = block_on(pool.submit_one(header01.hash(), SOURCE, xt0.clone())).unwrap();
let _ = block_on(pool.submit_one(header01.hash(), SOURCE, xt1.clone())).unwrap();
let _ = block_on(pool.submit_one(header01.hash(), SOURCE, xt2.clone())).unwrap();
let _ = block_on(pool.submit_one(header01.hash(), SOURCE, xt3.clone())).unwrap();
//todo: can we do better? We don't have API to check if event was processed internally.
let mut counter = 0;
while block_on(pool.mempool_len()).0 == 4 {
sleep(std::time::Duration::from_millis(1));
counter = counter + 1;
if counter > 20 {
assert!(false, "timeout");
}
}
assert_pool_status!(header01.hash(), &pool, 0, 3);
assert_eq!(block_on(pool.mempool_len()).0, 3);
}
#[test]
fn fatp_limits_watcher_ready_transactions_are_not_droped_when_view_is_dropped() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(6).with_ready_count(2).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
api.set_nonce(api.genesis_hash(), Eve.into(), 600);
api.set_nonce(api.genesis_hash(), Ferdie.into(), 700);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Bob, 300);
let xt2 = uxt(Charlie, 400);
let xt3 = uxt(Dave, 500);
let xt4 = uxt(Eve, 600);
let xt5 = uxt(Ferdie, 700);
let _xt0_watcher =
block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let _xt1_watcher =
block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
assert_pool_status!(header01.hash(), &pool, 2, 0);
assert_eq!(block_on(pool.mempool_len()).1, 2);
let header02 = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02.hash())));
let _xt2_watcher =
block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let _xt3_watcher =
block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
assert_pool_status!(header02.hash(), &pool, 2, 0);
assert_eq!(block_on(pool.mempool_len()).1, 4);
let header03 = api.push_block_with_parent(header02.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header02.hash()), header03.hash())));
let _xt4_watcher =
block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt4.clone())).unwrap();
let _xt5_watcher =
block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt5.clone())).unwrap();
assert_pool_status!(header03.hash(), &pool, 2, 0);
assert_eq!(block_on(pool.mempool_len()).1, 6);
let header04 =
api.push_block_with_parent(header03.hash(), vec![xt4.clone(), xt5.clone()], true);
api.set_nonce(header04.hash(), Alice.into(), 201);
api.set_nonce(header04.hash(), Bob.into(), 301);
api.set_nonce(header04.hash(), Charlie.into(), 401);
api.set_nonce(header04.hash(), Dave.into(), 501);
api.set_nonce(header04.hash(), Eve.into(), 601);
api.set_nonce(header04.hash(), Ferdie.into(), 701);
block_on(pool.maintain(new_best_block_event(&pool, Some(header03.hash()), header04.hash())));
assert_ready_iterator!(header01.hash(), pool, [xt0, xt1]);
assert_ready_iterator!(header02.hash(), pool, [xt2, xt3]);
assert_ready_iterator!(header03.hash(), pool, [xt4, xt5]);
assert_ready_iterator!(header04.hash(), pool, []);
block_on(pool.maintain(finalized_block_event(&pool, api.genesis_hash(), header01.hash())));
assert!(!pool.status_all().contains_key(&header01.hash()));
block_on(pool.maintain(finalized_block_event(&pool, header01.hash(), header02.hash())));
assert!(!pool.status_all().contains_key(&header02.hash()));
//view 01 was dropped
assert!(pool.ready_at(header01.hash()).now_or_never().is_none());
assert_eq!(block_on(pool.mempool_len()).1, 6);
block_on(pool.maintain(finalized_block_event(&pool, header02.hash(), header03.hash())));
//no revalidation has happened yet, all txs are kept
assert_eq!(block_on(pool.mempool_len()).1, 6);
//view 03 is still there
assert!(!pool.status_all().contains_key(&header03.hash()));
//view 02 was dropped
assert!(pool.ready_at(header02.hash()).now_or_never().is_none());
let mut prev_header = header03;
for n in 5..=11 {
let header = api.push_block(n, vec![], true);
let event = finalized_block_event(&pool, prev_header.hash(), header.hash());
block_on(pool.maintain(event));
prev_header = header;
}
//now revalidation has happened, all txs are dropped
assert_eq!(block_on(pool.mempool_len()).1, 0);
}
#[test]
fn fatp_limits_watcher_future_transactions_are_droped_when_view_is_dropped() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(6).with_future_count(2).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
api.set_nonce(api.genesis_hash(), Eve.into(), 600);
api.set_nonce(api.genesis_hash(), Ferdie.into(), 700);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Alice, 201);
let xt1 = uxt(Bob, 301);
let xt2 = uxt(Charlie, 401);
let xt3 = uxt(Dave, 501);
let xt4 = uxt(Eve, 601);
let xt5 = uxt(Ferdie, 701);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
assert_pool_status!(header01.hash(), &pool, 0, 2);
assert_eq!(block_on(pool.mempool_len()).1, 2);
assert_future_iterator!(header01.hash(), pool, [xt0, xt1]);
let header02 = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02.hash())));
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let xt3_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
assert_pool_status!(header02.hash(), &pool, 0, 2);
assert_eq!(block_on(pool.mempool_len()).1, 4);
assert_future_iterator!(header02.hash(), pool, [xt2, xt3]);
let header03 = api.push_block_with_parent(header02.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header02.hash()), header03.hash())));
let xt4_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt4.clone())).unwrap();
let xt5_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt5.clone())).unwrap();
assert_pool_status!(header03.hash(), &pool, 0, 2);
assert_eq!(block_on(pool.mempool_len()).1, 6);
assert_future_iterator!(header03.hash(), pool, [xt4, xt5]);
let header04 = api.push_block_with_parent(header03.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header03.hash()), header04.hash())));
assert_pool_status!(header04.hash(), &pool, 0, 2);
assert_eq!(pool.futures().len(), 2);
assert_future_iterator!(header04.hash(), pool, [xt4, xt5]);
block_on(pool.maintain(finalized_block_event(&pool, api.genesis_hash(), header04.hash())));
assert_eq!(pool.active_views_count(), 1);
assert_eq!(pool.inactive_views_count(), 0);
//todo: can we do better? We don't have API to check if event was processed internally.
let mut counter = 0;
while block_on(pool.mempool_len()).1 != 2 {
sleep(std::time::Duration::from_millis(1));
counter = counter + 1;
if counter > 20 {
assert!(false, "timeout {}", block_on(pool.mempool_len()).1);
}
}
assert_eq!(block_on(pool.mempool_len()).1, 2);
assert_pool_status!(header04.hash(), &pool, 0, 2);
assert_eq!(pool.futures().len(), 2);
let to_be_checked = vec![xt0_watcher, xt1_watcher, xt2_watcher, xt3_watcher];
for x in to_be_checked {
let x_status = futures::executor::block_on_stream(x).take(2).collect::<Vec<_>>();
assert_eq!(x_status, vec![TransactionStatus::Future, TransactionStatus::Dropped]);
}
let to_be_checked = vec![xt4_watcher, xt5_watcher];
for x in to_be_checked {
let x_status = futures::executor::block_on_stream(x).take(1).collect::<Vec<_>>();
assert_eq!(x_status, vec![TransactionStatus::Future]);
}
}
@@ -0,0 +1,561 @@
// This file is part of Substrate.
// 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/>.
//! Tests of priorities for fork-aware transaction pool.
pub mod fatp_common;
use fatp_common::{invalid_hash, new_best_block_event, TestPoolBuilder, LOG_TARGET, SOURCE};
use futures::{executor::block_on, FutureExt};
use sc_transaction_pool::ChainApi;
use sc_transaction_pool_api::{
error::Error as TxPoolError, LocalTransactionPool, MaintainedTransactionPool, TransactionPool,
TransactionStatus,
};
use substrate_test_runtime_client::Sr25519Keyring::*;
use substrate_test_runtime_transaction_pool::uxt;
use tracing::info;
#[test]
fn fatp_prio_ready_higher_evicts_lower() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(3).with_ready_count(2).build();
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Alice, 200);
api.set_priority(&xt0, 2);
api.set_priority(&xt1, 3);
let result0 = block_on(pool.submit_one(header01.hash(), SOURCE, xt0.clone()));
let result1 = block_on(pool.submit_one(header01.hash(), SOURCE, xt1.clone()));
info!(target: LOG_TARGET, ?result0, "r0");
info!(target: LOG_TARGET, ?result1, "r1");
info!(target: LOG_TARGET, len = ?block_on(pool.mempool_len()), "len");
info!(target: LOG_TARGET, status = ?pool.status_all()[&header01.hash()], "len");
assert_ready_iterator!(header01.hash(), pool, [xt1]);
assert_pool_status!(header01.hash(), &pool, 1, 0);
}
#[test]
fn fatp_prio_watcher_ready_higher_evicts_lower() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(3).with_ready_count(2).build();
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Alice, 200);
api.set_priority(&xt0, 2);
api.set_priority(&xt1, 3);
let xt0_watcher =
block_on(pool.submit_and_watch(header01.hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher =
block_on(pool.submit_and_watch(header01.hash(), SOURCE, xt1.clone())).unwrap();
let xt0_status = futures::executor::block_on_stream(xt0_watcher).take(2).collect::<Vec<_>>();
assert_eq!(
xt0_status,
vec![TransactionStatus::Ready, TransactionStatus::Usurped(api.hash_and_length(&xt1).0)]
);
let xt1_status = futures::executor::block_on_stream(xt1_watcher).take(1).collect::<Vec<_>>();
assert_eq!(xt1_status, vec![TransactionStatus::Ready]);
info!(target: LOG_TARGET, len = ?block_on(pool.mempool_len()), "len");
info!(target: LOG_TARGET, pool_status = ?pool.status_all()[&header01.hash()], "len");
assert_ready_iterator!(header01.hash(), pool, [xt1]);
assert_pool_status!(header01.hash(), &pool, 1, 0);
}
#[test]
fn fatp_prio_watcher_future_higher_evicts_lower() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(3).with_ready_count(3).build();
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Alice, 201);
let xt1 = uxt(Alice, 201);
let xt2 = uxt(Alice, 200);
api.set_priority(&xt0, 2);
api.set_priority(&xt1, 3);
let xt0_watcher =
block_on(pool.submit_and_watch(header01.hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher =
block_on(pool.submit_and_watch(header01.hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher =
block_on(pool.submit_and_watch(header01.hash(), SOURCE, xt2.clone())).unwrap();
let xt0_status = futures::executor::block_on_stream(xt0_watcher).take(2).collect::<Vec<_>>();
assert_eq!(
xt0_status,
vec![TransactionStatus::Future, TransactionStatus::Usurped(api.hash_and_length(&xt2).0)]
);
let xt1_status = futures::executor::block_on_stream(xt1_watcher).take(2).collect::<Vec<_>>();
assert_eq!(xt1_status, vec![TransactionStatus::Future, TransactionStatus::Ready]);
let xt2_status = futures::executor::block_on_stream(xt2_watcher).take(1).collect::<Vec<_>>();
assert_eq!(xt2_status, vec![TransactionStatus::Ready]);
assert_eq!(block_on(pool.mempool_len()).1, 2);
assert_ready_iterator!(header01.hash(), pool, [xt2, xt1]);
assert_pool_status!(header01.hash(), &pool, 2, 0);
}
#[test]
fn fatp_prio_watcher_ready_lower_prio_gets_dropped_from_all_views() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(3).with_ready_count(2).build();
let header01 = api.push_block(1, vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, None, header01.hash())));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Alice, 200);
api.set_priority(&xt0, 2);
api.set_priority(&xt1, 3);
let xt0_watcher =
block_on(pool.submit_and_watch(header01.hash(), SOURCE, xt0.clone())).unwrap();
let header02 = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02.hash())));
let header03a = api.push_block_with_parent(header02.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header03a.hash())));
let header03b = api.push_block_with_parent(header02.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header03a.hash()), header03b.hash())));
assert_pool_status!(header03a.hash(), &pool, 1, 0);
assert_ready_iterator!(header03a.hash(), pool, [xt0]);
assert_pool_status!(header03b.hash(), &pool, 1, 0);
assert_ready_iterator!(header03b.hash(), pool, [xt0]);
assert_ready_iterator!(header01.hash(), pool, [xt0]);
assert_ready_iterator!(header02.hash(), pool, [xt0]);
let xt1_watcher =
block_on(pool.submit_and_watch(header01.hash(), SOURCE, xt1.clone())).unwrap();
let xt1_status = futures::executor::block_on_stream(xt1_watcher).take(1).collect::<Vec<_>>();
assert_eq!(xt1_status, vec![TransactionStatus::Ready]);
let xt0_status = futures::executor::block_on_stream(xt0_watcher).take(2).collect::<Vec<_>>();
assert_eq!(
xt0_status,
vec![TransactionStatus::Ready, TransactionStatus::Usurped(api.hash_and_length(&xt1).0)]
);
assert_ready_iterator!(header03a.hash(), pool, [xt1]);
assert_ready_iterator!(header03b.hash(), pool, [xt1]);
assert_ready_iterator!(header01.hash(), pool, [xt1]);
assert_ready_iterator!(header02.hash(), pool, [xt1]);
}
#[test]
fn fatp_prio_watcher_future_lower_prio_gets_dropped_from_all_views() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(3).with_ready_count(2).build();
let header01 = api.push_block(1, vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, None, header01.hash())));
let xt0 = uxt(Alice, 201);
let xt1 = uxt(Alice, 201);
let xt2 = uxt(Alice, 200);
api.set_priority(&xt0, 2);
api.set_priority(&xt1, 3);
let xt0_watcher =
block_on(pool.submit_and_watch(header01.hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher =
block_on(pool.submit_and_watch(header01.hash(), SOURCE, xt1.clone())).unwrap();
let header02 = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02.hash())));
let header03a = api.push_block_with_parent(header02.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header03a.hash())));
let header03b = api.push_block_with_parent(header02.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header03a.hash()), header03b.hash())));
assert_pool_status!(header03a.hash(), &pool, 0, 2);
assert_future_iterator!(header03a.hash(), pool, [xt0, xt1]);
assert_pool_status!(header03b.hash(), &pool, 0, 2);
assert_future_iterator!(header03b.hash(), pool, [xt0, xt1]);
assert_future_iterator!(header01.hash(), pool, [xt0, xt1]);
assert_future_iterator!(header02.hash(), pool, [xt0, xt1]);
let xt2_watcher =
block_on(pool.submit_and_watch(header01.hash(), SOURCE, xt2.clone())).unwrap();
let xt2_status = futures::executor::block_on_stream(xt2_watcher).take(1).collect::<Vec<_>>();
assert_eq!(xt2_status, vec![TransactionStatus::Ready]);
let xt1_status = futures::executor::block_on_stream(xt1_watcher).take(1).collect::<Vec<_>>();
assert_eq!(xt1_status, vec![TransactionStatus::Future]);
let xt0_status = futures::executor::block_on_stream(xt0_watcher).take(2).collect::<Vec<_>>();
assert_eq!(
xt0_status,
vec![TransactionStatus::Future, TransactionStatus::Usurped(api.hash_and_length(&xt2).0)]
);
assert_future_iterator!(header03a.hash(), pool, []);
assert_future_iterator!(header03b.hash(), pool, []);
assert_future_iterator!(header01.hash(), pool, []);
assert_future_iterator!(header02.hash(), pool, []);
assert_ready_iterator!(header03a.hash(), pool, [xt2, xt1]);
assert_ready_iterator!(header03b.hash(), pool, [xt2, xt1]);
assert_ready_iterator!(header01.hash(), pool, [xt2, xt1]);
assert_ready_iterator!(header02.hash(), pool, [xt2, xt1]);
}
#[test]
fn fatp_prios_watcher_full_mempool_higher_prio_is_accepted() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(4).with_ready_count(2).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
api.set_nonce(api.genesis_hash(), Eve.into(), 600);
api.set_nonce(api.genesis_hash(), Ferdie.into(), 700);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Bob, 300);
let xt2 = uxt(Charlie, 400);
let xt3 = uxt(Dave, 500);
let xt4 = uxt(Eve, 600);
let xt5 = uxt(Ferdie, 700);
api.set_priority(&xt0, 1);
api.set_priority(&xt1, 2);
api.set_priority(&xt2, 3);
api.set_priority(&xt3, 4);
api.set_priority(&xt4, 5);
api.set_priority(&xt5, 6);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
assert_pool_status!(header01.hash(), &pool, 2, 0);
assert_eq!(block_on(pool.mempool_len()).1, 2);
let header02 = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02.hash())));
let _xt2_watcher =
block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let _xt3_watcher =
block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
assert_pool_status!(header02.hash(), &pool, 2, 0);
assert_eq!(block_on(pool.mempool_len()).1, 4);
let header03 = api.push_block_with_parent(header02.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header02.hash()), header03.hash())));
let _xt4_watcher =
block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt4.clone())).unwrap();
let _xt5_watcher =
block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt5.clone())).unwrap();
assert_pool_status!(header03.hash(), &pool, 2, 0);
assert_eq!(block_on(pool.mempool_len()).1, 4);
assert_watcher_stream!(xt0_watcher, [TransactionStatus::Ready, TransactionStatus::Dropped]);
assert_watcher_stream!(xt1_watcher, [TransactionStatus::Ready, TransactionStatus::Dropped]);
assert_ready_iterator!(header01.hash(), pool, []);
assert_ready_iterator!(header02.hash(), pool, [xt3, xt2]);
assert_ready_iterator!(header03.hash(), pool, [xt5, xt4]);
}
#[test]
fn fatp_prios_watcher_full_mempool_higher_prio_is_accepted_with_subtree() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(4).with_ready_count(4).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Alice, 201);
let xt2 = uxt(Alice, 202);
let xt3 = uxt(Bob, 300);
let xt4 = uxt(Charlie, 400);
api.set_priority(&xt0, 1);
api.set_priority(&xt1, 3);
api.set_priority(&xt2, 3);
api.set_priority(&xt3, 2);
api.set_priority(&xt4, 2);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let xt3_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
assert_ready_iterator!(header01.hash(), pool, [xt3, xt0, xt1, xt2]);
assert_pool_status!(header01.hash(), &pool, 4, 0);
assert_eq!(block_on(pool.mempool_len()).1, 4);
let xt4_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt4.clone())).unwrap();
assert_pool_status!(header01.hash(), &pool, 2, 0);
assert_ready_iterator!(header01.hash(), pool, [xt3, xt4]);
assert_watcher_stream!(xt0_watcher, [TransactionStatus::Ready, TransactionStatus::Dropped]);
assert_watcher_stream!(xt1_watcher, [TransactionStatus::Ready, TransactionStatus::Dropped]);
assert_watcher_stream!(xt2_watcher, [TransactionStatus::Ready, TransactionStatus::Dropped]);
assert_watcher_stream!(xt3_watcher, [TransactionStatus::Ready]);
assert_watcher_stream!(xt4_watcher, [TransactionStatus::Ready]);
}
#[test]
fn fatp_prios_watcher_full_mempool_higher_prio_is_accepted_with_subtree2() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(4).with_ready_count(4).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Alice, 201);
let xt2 = uxt(Alice, 202);
let xt3 = uxt(Bob, 300);
let xt4 = uxt(Charlie, 400);
api.set_priority(&xt0, 1);
api.set_priority(&xt1, 3);
api.set_priority(&xt2, 3);
api.set_priority(&xt3, 2);
api.set_priority(&xt4, 2);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let xt3_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
assert_ready_iterator!(header01.hash(), pool, [xt3, xt0, xt1, xt2]);
assert_pool_status!(header01.hash(), &pool, 4, 0);
assert_eq!(block_on(pool.mempool_len()).1, 4);
let header02 = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02.hash())));
let xt4_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt4.clone())).unwrap();
assert_ready_iterator!(header01.hash(), pool, [xt3]);
assert_pool_status!(header02.hash(), &pool, 2, 0);
assert_ready_iterator!(header02.hash(), pool, [xt3, xt4]);
assert_watcher_stream!(xt0_watcher, [TransactionStatus::Ready, TransactionStatus::Dropped]);
assert_watcher_stream!(xt1_watcher, [TransactionStatus::Ready, TransactionStatus::Dropped]);
assert_watcher_stream!(xt2_watcher, [TransactionStatus::Ready, TransactionStatus::Dropped]);
assert_watcher_stream!(xt3_watcher, [TransactionStatus::Ready]);
assert_watcher_stream!(xt4_watcher, [TransactionStatus::Ready]);
}
#[test]
fn fatp_prios_watcher_full_mempool_lower_prio_gets_rejected() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(2).with_ready_count(2).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Bob, 300);
let xt2 = uxt(Charlie, 400);
let xt3 = uxt(Dave, 500);
api.set_priority(&xt0, 2);
api.set_priority(&xt1, 2);
api.set_priority(&xt2, 2);
api.set_priority(&xt3, 1);
let _xt0_watcher =
block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let _xt1_watcher =
block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
assert_pool_status!(header01.hash(), &pool, 2, 0);
assert_eq!(block_on(pool.mempool_len()).1, 2);
let header02 = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02.hash())));
assert_pool_status!(header02.hash(), &pool, 2, 0);
assert_eq!(block_on(pool.mempool_len()).1, 2);
assert_ready_iterator!(header01.hash(), pool, [xt0, xt1]);
assert_ready_iterator!(header02.hash(), pool, [xt0, xt1]);
let result2 = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).map(|_| ());
assert!(matches!(result2.as_ref().unwrap_err().0, TxPoolError::ImmediatelyDropped));
let result3 = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).map(|_| ());
assert!(matches!(result3.as_ref().unwrap_err().0, TxPoolError::ImmediatelyDropped));
}
#[test]
fn fatp_prios_watcher_full_mempool_does_not_keep_dropped_transaction() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(4).with_ready_count(2).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Bob, 300);
let xt2 = uxt(Charlie, 400);
let xt3 = uxt(Dave, 500);
api.set_priority(&xt0, 2);
api.set_priority(&xt1, 2);
api.set_priority(&xt2, 2);
api.set_priority(&xt3, 2);
let xt0_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt0.clone())).unwrap();
let xt1_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt1.clone())).unwrap();
let xt2_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt2.clone())).unwrap();
let xt3_watcher = block_on(pool.submit_and_watch(invalid_hash(), SOURCE, xt3.clone())).unwrap();
assert_pool_status!(header01.hash(), &pool, 2, 0);
assert_ready_iterator!(header01.hash(), pool, [xt2, xt3]);
assert_watcher_stream!(xt0_watcher, [TransactionStatus::Ready, TransactionStatus::Dropped]);
assert_watcher_stream!(xt1_watcher, [TransactionStatus::Ready, TransactionStatus::Dropped]);
assert_watcher_stream!(xt2_watcher, [TransactionStatus::Ready]);
assert_watcher_stream!(xt3_watcher, [TransactionStatus::Ready]);
}
#[test]
fn fatp_prios_submit_local_full_mempool_higher_prio_is_accepted() {
sp_tracing::try_init_simple();
let builder = TestPoolBuilder::new();
let (pool, api, _) = builder.with_mempool_count_limit(4).with_ready_count(2).build();
api.set_nonce(api.genesis_hash(), Bob.into(), 300);
api.set_nonce(api.genesis_hash(), Charlie.into(), 400);
api.set_nonce(api.genesis_hash(), Dave.into(), 500);
api.set_nonce(api.genesis_hash(), Eve.into(), 600);
api.set_nonce(api.genesis_hash(), Ferdie.into(), 700);
let header01 = api.push_block(1, vec![], true);
let event = new_best_block_event(&pool, None, header01.hash());
block_on(pool.maintain(event));
let xt0 = uxt(Alice, 200);
let xt1 = uxt(Bob, 300);
let xt2 = uxt(Charlie, 400);
let xt3 = uxt(Dave, 500);
let xt4 = uxt(Eve, 600);
let xt5 = uxt(Ferdie, 700);
api.set_priority(&xt0, 1);
api.set_priority(&xt1, 2);
api.set_priority(&xt2, 3);
api.set_priority(&xt3, 4);
api.set_priority(&xt4, 5);
api.set_priority(&xt5, 6);
pool.submit_local(invalid_hash(), xt0.clone()).unwrap();
pool.submit_local(invalid_hash(), xt1.clone()).unwrap();
assert_pool_status!(header01.hash(), &pool, 2, 0);
assert_eq!(block_on(pool.mempool_len()).0, 2);
let header02 = api.push_block_with_parent(header01.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header01.hash()), header02.hash())));
pool.submit_local(invalid_hash(), xt2.clone()).unwrap();
pool.submit_local(invalid_hash(), xt3.clone()).unwrap();
assert_pool_status!(header02.hash(), &pool, 2, 0);
assert_eq!(block_on(pool.mempool_len()).0, 4);
let header03 = api.push_block_with_parent(header02.hash(), vec![], true);
block_on(pool.maintain(new_best_block_event(&pool, Some(header02.hash()), header03.hash())));
pool.submit_local(invalid_hash(), xt4.clone()).unwrap();
pool.submit_local(invalid_hash(), xt5.clone()).unwrap();
assert_pool_status!(header03.hash(), &pool, 2, 0);
assert_eq!(block_on(pool.mempool_len()).0, 4);
assert_ready_iterator!(header01.hash(), pool, []);
assert_ready_iterator!(header02.hash(), pool, [xt3, xt2]);
assert_ready_iterator!(header03.hash(), pool, [xt5, xt4]);
}
@@ -0,0 +1,540 @@
// This file is part of Substrate.
// 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/>.
//! Testsuite of transaction pool integration tests.
pub mod zombienet;
use std::time::Duration;
use crate::zombienet::{
default_zn_scenario_builder, relaychain_pezkuwichain_local_network_spec as relay,
relaychain_pezkuwichain_local_network_spec::teyrchain_asset_hub_network_spec as para,
BlockSubscriptionType, NetworkSpawner,
};
use futures::future::join_all;
use tracing::info;
use txtesttool::{execution_log::ExecutionLog, scenario::ScenarioExecutor};
use zombienet::DEFAULT_SEND_FUTURE_AND_READY_TXS_TESTS_TIMEOUT_IN_SECS;
// Test which sends future and ready txs from many accounts
// to an unlimited pool of a teyrchain collator based on the asset-hub-pezkuwichain runtime.
#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn send_future_and_ready_from_many_accounts_to_teyrchain() {
let net = NetworkSpawner::from_toml_with_env_logger(para::HIGH_POOL_LIMIT_FATP)
.await
.unwrap();
// Wait for the teyrchain collator to start block production.
net.wait_for_block("charlie", BlockSubscriptionType::Best).await.unwrap();
// Create future & ready txs executors.
let ws = net.node_rpc_uri("charlie").unwrap();
let future_scenario_executor = default_zn_scenario_builder(&net)
.with_rpc_uri(ws.clone())
.with_start_id(0)
.with_last_id(99)
.with_nonce_from(Some(100))
.with_txs_count(100)
.with_executor_id("future-txs-executor".to_string())
.with_timeout_in_secs(DEFAULT_SEND_FUTURE_AND_READY_TXS_TESTS_TIMEOUT_IN_SECS)
.build()
.await;
let ready_scenario_executor = default_zn_scenario_builder(&net)
.with_rpc_uri(ws)
.with_start_id(0)
.with_last_id(99)
.with_nonce_from(Some(0))
.with_txs_count(100)
.with_executor_id("ready-txs-executor".to_string())
.with_timeout_in_secs(DEFAULT_SEND_FUTURE_AND_READY_TXS_TESTS_TIMEOUT_IN_SECS)
.build()
.await;
// Execute transactions and fetch the execution logs.
let (future_logs, ready_logs) = futures::future::join(
future_scenario_executor.execute(),
ready_scenario_executor.execute(),
)
.await;
let finalized_future =
future_logs.values().filter_map(|default_log| default_log.finalized()).count();
let finalized_ready =
ready_logs.values().filter_map(|default_log| default_log.finalized()).count();
assert_eq!(finalized_future, 10_000);
assert_eq!(finalized_ready, 10_000);
}
// Test which sends future and ready txs from many accounts
// to an unlimited pool of a relaychain node based on `pezkuwichain-local` runtime.
#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn send_future_and_ready_from_many_accounts_to_relaychain() {
let net = NetworkSpawner::from_toml_with_env_logger(relay::HIGH_POOL_LIMIT_FATP)
.await
.unwrap();
// Wait for the paracha validator to start block production & have its genesis block
// finalized.
net.wait_for_block("alice", BlockSubscriptionType::Best).await.unwrap();
// Create future & ready txs executors.
let ws = net.node_rpc_uri("alice").unwrap();
let future_scenario_executor = default_zn_scenario_builder(&net)
.with_rpc_uri(ws.clone())
.with_start_id(0)
.with_last_id(99)
.with_nonce_from(Some(100))
.with_txs_count(100)
.with_executor_id("future-txs-executor".to_string())
.with_timeout_in_secs(DEFAULT_SEND_FUTURE_AND_READY_TXS_TESTS_TIMEOUT_IN_SECS)
.build()
.await;
let ready_scenario_executor = default_zn_scenario_builder(&net)
.with_rpc_uri(ws)
.with_start_id(0)
.with_last_id(99)
.with_nonce_from(Some(0))
.with_txs_count(100)
.with_executor_id("ready-txs-executor".to_string())
.with_timeout_in_secs(DEFAULT_SEND_FUTURE_AND_READY_TXS_TESTS_TIMEOUT_IN_SECS)
.build()
.await;
// Execute transactions and fetch the execution logs.
// Execute transactions and fetch the execution logs.
let (future_logs, ready_logs) = futures::future::join(
future_scenario_executor.execute(),
ready_scenario_executor.execute(),
)
.await;
let finalized_future =
future_logs.values().filter_map(|default_log| default_log.finalized()).count();
let finalized_ready =
ready_logs.values().filter_map(|default_log| default_log.finalized()).count();
assert_eq!(finalized_future, 10_000);
assert_eq!(finalized_ready, 10_000);
}
// Send immortal and mortal txs. Some of the mortal txs are configured to get dropped
// while others to succeed. Mortal txs are future so not being able to become ready in time and
// included in blocks result in their dropping.
//
// Block length for pezkuwichain for user txs is 75% of maximum 5MB (per frame-system setup),
// so we get 3750KB. In the test scenario we aim for 5 txs per block roughly (not precesily)
// so to fill a block each user tx must have around 750kb.
#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn send_future_mortal_txs() {
let net = NetworkSpawner::from_toml_with_env_logger(relay::HIGH_POOL_LIMIT_FATP)
.await
.unwrap();
// Wait for the teyrchain collator to start block production.
net.wait_for_block("alice", BlockSubscriptionType::Finalized).await.unwrap();
// Create txs executors.
let ws = net.node_rpc_uri("alice").unwrap();
let ready_scenario_executor = default_zn_scenario_builder(&net)
.with_rpc_uri(ws.clone())
.with_start_id(0)
.with_nonce_from(Some(0))
.with_txs_count(50)
// Block length for pezkuwichain for user txs is 75% of maximum 5MB (per frame-system
// setup), so we get 3750KB. In the test scenario we aim for 5 txs per block roughly (not
// precesily) so to fill a block each user tx must have around 750kb. We aim for 5 txs per
// block because we send 50 ready txs which we want to distribute over 10 blocks, so
// mortal txs with lifetime lower than 10 should be declared invalid after the ready txs
// finalize, while mortal txs with bigger lifetime should be finalized.
.with_remark_recipe(750)
.with_executor_id("ready-txs-executor".to_string())
.build()
.await;
let mortal_scenario_invalid = default_zn_scenario_builder(&net)
.with_rpc_uri(ws.clone())
.with_start_id(0)
.with_nonce_from(Some(60))
.with_txs_count(10)
.with_executor_id("mortal-tx-executor-invalid".to_string())
.with_mortality(5)
.build()
.await;
let mortal_scenario_success = default_zn_scenario_builder(&net)
.with_rpc_uri(ws)
.with_start_id(0)
.with_nonce_from(Some(50))
.with_txs_count(10)
.with_executor_id("mortal-tx-executor-success".to_string())
.with_mortality(25)
.build()
.await;
// Execute transactions and fetch the execution logs.
let (mortal_invalid_logs, ready_logs, mortal_succes_logs) = tokio::join!(
mortal_scenario_invalid.execute(),
ready_scenario_executor.execute(),
mortal_scenario_success.execute(),
);
let mortal_invalid = mortal_invalid_logs
.values()
.filter(|default_log| default_log.get_invalid_reason().len() > 0)
.count();
let mortal_succesfull = mortal_succes_logs
.values()
.filter_map(|default_log| default_log.finalized())
.count();
let finalized_ready =
ready_logs.values().filter_map(|default_log| default_log.finalized()).count();
assert_eq!(mortal_invalid, 10);
assert_eq!(mortal_succesfull, 10);
assert_eq!(finalized_ready, 50);
}
// Send immortal and mortal txs. Some mortal txs have lower priority so they shouldn't get into
// blocks during their lifetime, and will be considered invalid, while other mortal txs have
// sufficient lifetime to be included in blocks, and are finalized successfully.
#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn send_lower_priority_mortal_txs() {
let net = NetworkSpawner::from_toml_with_env_logger(relay::HIGH_POOL_LIMIT_FATP)
.await
.unwrap();
// Wait for the teyrchain collator to start block production.
net.wait_for_block("alice", BlockSubscriptionType::Finalized).await.unwrap();
// Create txs executors.
let ws = net.node_rpc_uri("alice").unwrap();
let ready_scenario_executor = default_zn_scenario_builder(&net)
.with_rpc_uri(ws.clone())
.with_start_id(0)
.with_nonce_from(Some(0))
.with_txs_count(50)
.with_executor_id("ready-txs-executor".to_string())
// Block length for pezkuwichain for user txs is 75% of maximum 5MB (per frame-system
// setup), so we get 3750KB. In the test scenario we aim for 5 txs per block roughly (not
// precesily) so to fill a block each user tx must have around 750kb. We aim for 5 txs per
// block because we send 50 ready txs which we want to distribute over 10 blocks, so
// mortal txs with lifetime lower than 10 should be declared invalid after the ready txs
// finalize, while mortal txs with bigger lifetime should be finalized.
.with_remark_recipe(750)
.with_tip(150)
.build()
.await;
let mortal_scenario_invalid = default_zn_scenario_builder(&net)
.with_rpc_uri(ws.clone())
.with_start_id(1)
.with_nonce_from(Some(0))
.with_txs_count(10)
.with_executor_id("mortal-tx-executor-invalid".to_string())
.with_mortality(5)
// Make it very hard for these mortal txs to be included in blocks, by making them big
// enough to not let other txs be part of the same block as them, but also make sure they
// have the lowest priority so that they are not included in a single tx block over other
// txs. At some point they'll be starved and their lifetime will pass.
.with_remark_recipe(3500)
.with_tip(50)
.build()
.await;
let mortal_scenario_success = default_zn_scenario_builder(&net)
.with_rpc_uri(ws)
.with_start_id(2)
.with_nonce_from(Some(0))
.with_txs_count(10)
.with_executor_id("mortal-tx-executor-success".to_string())
.with_mortality(20)
// Same reasoning as for the ready immortal txs, we want these txs to be similarly big as
// the immortal txs, so at all times if it comes to pick a ready txs to include it in a
// block, an immortal tx should be picked instead (leaving these mortal txs to be picked
// only after, which is fine for these mortal txs).
.with_remark_recipe(750)
.with_tip(100)
.build()
.await;
// Execute transactions and fetch the execution logs.
let (mortal_invalid_logs, ready_logs, mortal_success_logs) = tokio::join!(
mortal_scenario_invalid.execute(),
ready_scenario_executor.execute(),
mortal_scenario_success.execute(),
);
let mortal_invalid = mortal_invalid_logs
.values()
.filter(|default_log| default_log.get_invalid_reason().len() > 0)
.count();
let mortal_succesfull = mortal_success_logs
.values()
.filter_map(|default_log| default_log.finalized())
.count();
let finalized_ready =
ready_logs.values().filter_map(|default_log| default_log.finalized()).count();
assert_eq!(mortal_invalid, 10);
assert_eq!(mortal_succesfull, 10);
assert_eq!(finalized_ready, 50);
}
// Test which sends 5m transactions to teyrchain. Long execution time expected.
#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn send_5m_from_many_accounts_to_teyrchain() {
let net = NetworkSpawner::from_toml_with_env_logger(para::HIGH_POOL_LIMIT_FATP)
.await
.unwrap();
// Wait for the teyrchain collator to start block production.
net.wait_for_block("charlie", BlockSubscriptionType::Best).await.unwrap();
// Create txs executor.
let ws = net.node_rpc_uri("charlie").unwrap();
let executor = default_zn_scenario_builder(&net)
.with_rpc_uri(ws)
.with_start_id(0)
.with_last_id(999)
.with_txs_count(5_000)
.with_executor_id("txs-executor".to_string())
.with_send_threshold(7500)
.build()
.await;
// Execute transactions and fetch the execution logs.
let execution_logs = executor.execute().await;
let finalized_txs = execution_logs.values().filter_map(|tx_log| tx_log.finalized()).count();
assert_eq!(finalized_txs, 5_000_000);
}
// Test which sends 5m transactions to relaychain. Long execution time expected.
#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn send_5m_from_many_accounts_to_relaychain() {
let net = NetworkSpawner::from_toml_with_env_logger(relay::HIGH_POOL_LIMIT_FATP)
.await
.unwrap();
// Wait for the teyrchain collator to start block production.
net.wait_for_block("alice", BlockSubscriptionType::Best).await.unwrap();
// Create txs executor.
let ws = net.node_rpc_uri("alice").unwrap();
let executor = default_zn_scenario_builder(&net)
.with_rpc_uri(ws.clone())
.with_start_id(0)
.with_last_id(999)
.with_txs_count(5000)
.with_executor_id("txs-executor".to_string())
.with_send_threshold(7500)
.build()
.await;
// Execute transactions and fetch the execution logs.
let execution_logs = executor.execute().await;
let finalized_txs = execution_logs.values().filter_map(|tx_log| tx_log.finalized()).count();
assert_eq!(finalized_txs, 5_000_000);
}
/// Internal test that allows to observe how transcactions are gossiped in the network. Requires
/// external tool to track transactions presence at nodes. Was used to evaluate some metrics of
/// existing transaction protocol.
#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn gossiping() {
let net = NetworkSpawner::from_toml_with_env_logger(relay::HIGH_POOL_LIMIT_FATP_TRACE)
.await
.unwrap();
// Wait for the teyrchain collator to start block production.
net.wait_for_block("a00", BlockSubscriptionType::Best).await.unwrap();
// Create the txs executor.
let ws = net.node_rpc_uri("a00").unwrap();
let executor = default_zn_scenario_builder(&net)
.with_rpc_uri(ws)
.with_start_id(0)
.with_last_id(999)
.with_executor_id("txs-executor".to_string())
.build()
.await;
// Execute transactions and fetch the execution logs.
let execution_logs = executor.execute().await;
let finalized_txs = execution_logs.values().filter_map(|tx_log| tx_log.finalized()).count();
assert_eq!(finalized_txs, 1000);
tracing::info!("BASEDIR: {:?}", net.base_dir_path());
}
/// Creates new transaction scenario executor and sends given batch of ready transactions to the
/// specified node. Single transaction is sent from single account.
async fn send_batch(
net: &NetworkSpawner,
node_name: &str,
from: u32,
to: u32,
prio: u32,
) -> ScenarioExecutor {
let ws = net.node_rpc_uri(node_name).unwrap();
info!(from, to, prio, "send_batch");
default_zn_scenario_builder(net)
.with_rpc_uri(ws)
.with_start_id(from)
.with_last_id(to)
.with_txs_count(1)
.with_tip(prio.into())
.with_executor_id(format!("txs-executor_{}_{}_{}", from, to, prio))
.with_send_threshold(usize::MAX)
.with_legacy_backend(true)
.build()
.await
}
/// Repeatedly sends batches of transactions to the specified node with priority provided by
/// closure.
///
/// This function loops indefinitely, adjusting the priority of the transaction batch each time
/// based on the provided function. Each batch is executed by an executor that times out after
/// period duration if not completed.
///
/// The progress of transactions is intentionally not monitored; the utility is intended for
/// transaction pool limits testing, where the accuracy of execution is challenging to monitor.
async fn batch_loop<F>(
net: &NetworkSpawner,
node_name: &str,
from: u32,
to: u32,
priority: F,
period: std::time::Duration,
) where
F: Fn(u32) -> u32,
{
let mut prio = 0;
loop {
prio = priority(prio);
let executor = send_batch(&net, node_name, from, to, prio).await;
let start = std::time::Instant::now();
let _results = tokio::time::timeout(period, executor.execute()).await;
let elapsed = start.elapsed();
if elapsed < period {
tokio::time::sleep(period - elapsed).await;
}
}
}
/// Tests the transaction pool limits by continuously sending transaction batches to a teyrchain
/// network node. This test checks the pool's behavior under high load by simulating multiple
/// senders with increasing priorities.
#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn test_limits_increasing_prio_teyrchain() {
let net = NetworkSpawner::from_toml_with_env_logger(para::LOW_POOL_LIMIT_FATP)
.await
.unwrap();
net.wait_for_block("charlie", BlockSubscriptionType::Best).await.unwrap();
let mut executors = vec![];
let senders_count = 25;
let sender_batch = 2000;
for i in 0..senders_count {
let from = 0 + i * sender_batch;
let to = from + sender_batch - 1;
executors.push(batch_loop(
&net,
"charlie",
from,
to,
|prio| prio + 1,
Duration::from_secs(60),
));
}
let _results = join_all(executors).await;
}
/// Tests the transaction pool limits by continuously sending transaction batches to a relaychain
/// network node. This test checks the pool's behavior under high load by simulating multiple
/// senders with increasing priorities.
#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn test_limits_increasing_prio_relaychain() {
let net = NetworkSpawner::from_toml_with_env_logger(relay::LOW_POOL_LIMIT_FATP)
.await
.unwrap();
net.wait_for_block("alice", BlockSubscriptionType::Best).await.unwrap();
let mut executors = vec![];
//this looks like current limit of what we can handle. A bit choky but almost no empty blocks.
let senders_count = 50;
let sender_batch = 2000;
for i in 0..senders_count {
let from = 0 + i * sender_batch;
let to = from + sender_batch - 1;
executors.push(batch_loop(
&net,
"alice",
from,
to,
|prio| prio + 1,
Duration::from_secs(15),
));
}
let _results = join_all(executors).await;
}
/// Tests the transaction pool limits by continuously sending transaction batches to a relaychain
/// network node. This test checks the pool's behavior under high load by simulating multiple
/// senders with increasing priorities.
#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn test_limits_same_prio_relaychain() {
let net = NetworkSpawner::from_toml_with_env_logger(relay::LOW_POOL_LIMIT_FATP)
.await
.unwrap();
net.wait_for_block("alice", BlockSubscriptionType::Best).await.unwrap();
let mut executors = vec![];
let senders_count = 50;
let sender_batch = 2000;
for i in 0..senders_count {
let from = 0 + i * sender_batch;
let to = from + sender_batch - 1;
executors.push(batch_loop(&net, "alice", from, to, |prio| prio, Duration::from_secs(15)));
}
let _results = join_all(executors).await;
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,250 @@
// This file is part of Substrate.
// 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 zombienet spawner for integration tests for a transaction pool. Holds shared logic used
//! across integration tests for transaction pool.
use anyhow::anyhow;
use std::time::SystemTime;
use tracing_subscriber::EnvFilter;
use txtesttool::scenario::{ChainType, ScenarioBuilder};
use zombienet_sdk::{
subxt::SubstrateConfig, GlobalSettingsBuilder, LocalFileSystem, Network, NetworkConfig,
NetworkConfigBuilder, NetworkConfigExt, WithRelaychain,
};
/// Gathers TOML files paths for relaychains and for teyrchains' (that use pezkuwichain-local based
/// relaychains) zombienet network specs for testing in relation to fork aware transaction pool.
pub mod relaychain_pezkuwichain_local_network_spec {
pub const HIGH_POOL_LIMIT_FATP: &'static str =
"tests/zombienet/network-specs/pezkuwichain-local-high-pool-limit-fatp.toml";
pub const LOW_POOL_LIMIT_FATP: &'static str =
"tests/zombienet/network-specs/pezkuwichain-local-low-pool-limit-fatp.toml";
pub const HIGH_POOL_LIMIT_FATP_TRACE: &'static str =
"tests/zombienet/network-specs/pezkuwichain-local-gossiping.toml";
/// Network specs used for fork-aware tx pool testing of teyrchains.
pub mod teyrchain_asset_hub_network_spec {
pub const LOW_POOL_LIMIT_FATP: &'static str =
"tests/zombienet/network-specs/asset-hub-low-pool-limit-fatp.toml";
pub const HIGH_POOL_LIMIT_FATP: &'static str =
"tests/zombienet/network-specs/asset-hub-high-pool-limit-fatp.toml";
}
}
mod yap_test;
/// Default time that we expect to need for a full run of current tests that send future and ready
/// txs to teyrchain or relaychain networks.
pub const DEFAULT_SEND_FUTURE_AND_READY_TXS_TESTS_TIMEOUT_IN_SECS: u64 = 1500;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Network initialization failure: {0}")]
NetworkInit(anyhow::Error),
#[error("Node couldn't be found as part of the network: {0}")]
NodeNotFound(anyhow::Error),
#[error("Failed to get node online client")]
FailedToGetOnlineClinet,
#[error("Failed to get node blocks stream")]
FailedToGetBlocksStream,
}
/// Result of work related to network spawning.
pub type Result<T> = std::result::Result<T, Error>;
/// Environment variable defining the location of zombienet network base dir.
const TXPOOL_TEST_DIR_ENV: &str = "TXPOOL_TEST_DIR";
/// Type for block subscription modes.
pub enum BlockSubscriptionType {
Finalized,
Best,
}
/// Provides logic to spawn a network based on a Zombienet toml file.
pub struct NetworkSpawner {
network: Network<LocalFileSystem>,
}
impl NetworkSpawner {
/// Initialize the network spawner using given `builder` closure.
pub async fn with_closure<F>(builder: F) -> Result<NetworkSpawner>
where
F: FnOnce() -> NetworkConfigBuilder<WithRelaychain>,
{
let _ = env_logger::try_init_from_env(
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
);
let config_builder = builder();
let net_config = config_builder
.with_global_settings(|global_settings| match NetworkSpawner::base_dir_from_env() {
Some(val) => global_settings.with_base_dir(val),
_ => global_settings,
})
.build()
.map_err(|errs| {
let msg = errs.into_iter().map(|e| e.to_string()).collect::<Vec<_>>().join(", ");
Error::NetworkInit(anyhow!(msg))
})?;
Ok(NetworkSpawner {
network: net_config
.spawn_native()
.await
.map_err(|err| Error::NetworkInit(anyhow!(err.to_string())))?,
})
}
/// Generates a directory path from an environment variable and the current timestamp.
/// The format is "<TENV>/test_YMD_HMS"
pub fn base_dir_from_env() -> Option<String> {
std::env::var(TXPOOL_TEST_DIR_ENV)
.map(|pool_test_dir| {
let datetime: chrono::DateTime<chrono::Local> = SystemTime::now().into();
let formatted_date = datetime.format("%Y%m%d_%H%M%S");
format!("{}/test_{}", pool_test_dir, formatted_date)
})
.ok()
}
/// Initialize the network spawner based on a Zombienet toml file
pub async fn from_toml_with_env_logger(toml_path: &'static str) -> Result<NetworkSpawner> {
// Initialize the subscriber with a default log level of INFO if RUST_LOG is not set
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
// Set up the subscriber with the formatter and the environment filter
tracing_subscriber::fmt()
.with_env_filter(env_filter) // Use the env filter
.init();
let net_config = if let Some(base_dir) = Self::base_dir_from_env() {
let settings = GlobalSettingsBuilder::new().with_base_dir(base_dir).build().unwrap();
NetworkConfig::load_from_toml_with_settings(toml_path, &settings)
.map_err(Error::NetworkInit)?
} else {
tracing::info!("'{TXPOOL_TEST_DIR_ENV}' env not set, proceeding with defaults.");
NetworkConfig::load_from_toml(toml_path).map_err(Error::NetworkInit)?
};
Ok(NetworkSpawner {
network: net_config
.spawn_native()
.await
.map_err(|err| Error::NetworkInit(anyhow!(err.to_string())))?,
})
}
/// Returns the spawned network.
pub fn network(&self) -> &Network<LocalFileSystem> {
&self.network
}
/// Waits for blocks production/import to kick-off on given node.
///
/// It subscribes to best/finalized blocks on the given node to determine whether
/// the blocks were considered as best/finalized.
pub async fn wait_for_block(
&self,
node_name: &str,
subscription_type: BlockSubscriptionType,
) -> Result<()> {
let node = self
.network
.get_node(node_name)
.map_err(|_| Error::NodeNotFound(anyhow!("{node_name}")))?;
let client = node
.wait_client::<SubstrateConfig>()
.await
.map_err(|_| Error::FailedToGetOnlineClinet)?;
let mut stream = match subscription_type {
BlockSubscriptionType::Best => client
.blocks()
.subscribe_finalized()
.await
.map_err(|_| Error::FailedToGetBlocksStream)?,
BlockSubscriptionType::Finalized => client
.blocks()
.subscribe_best()
.await
.map_err(|_| Error::FailedToGetBlocksStream)?,
};
// It should take at most two iterations to return with the best block, if any.
for _ in 0..=1 {
let Some(block) = stream.next().await else {
continue;
};
if let Some(block) = block.ok().filter(|block| block.number() == 1) {
tracing::info!("[{node_name}] found first block: {:#?}", block.hash());
break;
}
tracing::info!("[{node_name}] waiting for first block");
}
Ok(())
}
/// Get the network filesystem base dir path.
pub fn base_dir_path(&self) -> Option<&str> {
self.network.base_dir()
}
/// Get a certain node rpc uri.
pub fn node_rpc_uri(&self, node_name: &str) -> Result<String> {
self.network
.get_node(node_name)
.and_then(|node| Ok(node.ws_uri().to_string()))
.map_err(|_| Error::NodeNotFound(anyhow!("{node_name}")))
}
}
/// Shared params usually set in same way for most of the scenarios.
pub struct ScenarioBuilderSharedParams {
watched_txs: bool,
does_block_monitoring: bool,
send_threshold: usize,
chain_type: ChainType,
}
impl Default for ScenarioBuilderSharedParams {
fn default() -> Self {
Self {
watched_txs: true,
does_block_monitoring: false,
send_threshold: 20000,
chain_type: ChainType::Sub,
}
}
}
/// Creates a [`txtesttool::scenario::ScenarioBuilder`] with a set of default parameters defined
/// with [`ScenarioBuilderSharedParams::default`].
pub fn default_zn_scenario_builder(net_spawner: &NetworkSpawner) -> ScenarioBuilder {
let shared_params = ScenarioBuilderSharedParams::default();
ScenarioBuilder::new()
.with_watched_txs(shared_params.watched_txs)
.with_send_threshold(shared_params.send_threshold)
.with_block_monitoring(shared_params.does_block_monitoring)
.with_chain_type(shared_params.chain_type)
.with_base_dir_path(net_spawner.base_dir_path().unwrap().to_string())
.with_timeout_in_secs(21600) //6 hours
}
@@ -0,0 +1,62 @@
[settings]
timeout = 1500
[relaychain]
default_image = "pezkuwichain/pezkuwi:latest"
default_command = "pezkuwi"
chain = "pezkuwichain-local"
[[relaychain.nodes]]
name = "alice"
rpc_port = 9944
validator = true
[[relaychain.nodes]]
name = "bob"
rpc_port = 9945
validator = true
[[teyrchains]]
id = 2000
chain = "asset-hub-pezkuwichain-local"
default_command = "pezkuwi-teyrchain"
default_image = "pezkuwichain/pezkuwi-teyrchain:latest"
cumulus_based = true
default_args = [
"--force-authoring",
"--pool-kbytes 2048000",
"--pool-limit 500000",
"--pool-type=fork-aware",
"--rpc-max-connections 15000",
"--rpc-max-response-size 150",
"--rpc-max-subscriptions-per-connection=128000",
"--state-pruning=1024",
"-laura::cumulus=info",
"-lbasic-authorship=info",
"-lpeerset=info",
"-lsub-libp2p=info",
"-lsync=info",
"-ltxpool=debug",
]
[teyrchains.genesis.runtimeGenesis.patch.balances]
devAccounts = [1000, 1000000000000000000, "//Sender//{}"]
[[teyrchains.collators]]
name = "charlie"
validator = false
rpc_port = 9933
[[teyrchains.collators]]
name = "dave"
validator = true
rpc_port = 9934
[[teyrchains.collators]]
name = "eve"
validator = true
rpc_port = 9935
[[teyrchains.collators]]
name = "ferdie"
validator = true
rpc_port = 9936
@@ -0,0 +1,63 @@
[settings]
timeout = 1500
[relaychain]
default_image = "pezkuwichain/pezkuwi:latest"
default_command = "pezkuwi"
chain = "pezkuwichain-local"
[[relaychain.nodes]]
name = "alice"
rpc_port = 9944
validator = true
[[relaychain.nodes]]
name = "bob"
validator = true
[[teyrchains]]
id = 2000
cumulus_based = true
chain = "asset-hub-pezkuwichain-local"
default_image = "pezkuwichain/pezkuwi-teyrchain:latest"
default_command = "pezkuwi-teyrchain"
default_args = [
"--force-authoring",
"--pool-kbytes 2048000",
"--pool-limit 10000",
"--pool-type=fork-aware",
"--rpc-max-connections 100000",
"--rpc-max-response-size 150",
"--rpc-max-subscriptions-per-connection=128000",
"--state-pruning=1024",
"-laura::cumulus=info",
"-lbasic-authorship=info",
"-lpeerset=info",
"-lsub-libp2p=info",
"-lsync=info",
"-ltxpool=debug",
"-ltxpoolstat=debug",
]
[teyrchains.genesis.runtimeGenesis.patch.balances]
devAccounts = [80000, 1000000000000000000, "//Sender//{}"]
# run charlie as teyrchain collator
[[teyrchains.collators]]
name = "charlie"
validator = false
rpc_port = 9933
[[teyrchains.collators]]
name = "dave"
validator = true
rpc_port = 9934
[[teyrchains.collators]]
name = "eve"
validator = true
rpc_port = 9935
[[teyrchains.collators]]
name = "ferdie"
validator = true
rpc_port = 9936
@@ -0,0 +1,108 @@
[settings]
timeout = 1500
[relaychain]
default_image = "pezkuwichain/pezkuwi:latest"
default_command = "pezkuwi"
chain = "pezkuwichain-local"
default_args = [
# "--network-backend litep2p",
"--pool-kbytes 2048000",
"--pool-limit 500000",
"--pool-type=fork-aware",
"--rpc-max-connections 15000",
"--rpc-max-response-size 150",
"--rpc-max-subscriptions-per-connection=128000",
"--state-pruning=1024",
"-ltxpool=trace",
"-lsync=trace",
"--out-peers=3",
"--in-peers=3",
]
[relaychain.genesis.runtimeGenesis.patch.balances]
devAccounts = [1000, 1000000000000000000, "//Sender//{}"]
[[relaychain.nodes]]
name = "a00"
rpc_port = 9944
validator = true
[[relaychain.nodes]]
name = "b00"
validator = true
[[relaychain.nodes]]
name = "b01"
validator = true
[[relaychain.nodes]]
name = "b02"
validator = true
[[relaychain.nodes]]
name = "b03"
validator = true
[[relaychain.nodes]]
name = "b04"
validator = true
[[relaychain.nodes]]
name = "b05"
validator = true
[[relaychain.nodes]]
name = "b06"
validator = true
[[relaychain.nodes]]
name = "b07"
validator = true
[[relaychain.nodes]]
name = "b08"
validator = true
[[relaychain.nodes]]
name = "b09"
validator = true
[[relaychain.nodes]]
name = "b10"
validator = true
[[relaychain.nodes]]
name = "b11"
validator = true
[[relaychain.nodes]]
name = "b12"
validator = true
[[relaychain.nodes]]
name = "b13"
validator = true
[[relaychain.nodes]]
name = "b14"
validator = true
[[relaychain.nodes]]
name = "b15"
validator = true
[[relaychain.nodes]]
name = "b16"
validator = true
[[relaychain.nodes]]
name = "b17"
validator = true
[[relaychain.nodes]]
name = "b18"
validator = true
[[relaychain.nodes]]
name = "b19"
validator = true
@@ -0,0 +1,30 @@
[settings]
timeout = 1500
[relaychain]
default_image = "pezkuwichain/pezkuwi:latest"
default_command = "pezkuwi"
chain = "pezkuwichain-local"
default_args = [
"--pool-kbytes 2048000",
"--pool-limit 500000",
"--pool-type=fork-aware",
"--rpc-max-connections 15000",
"--rpc-max-response-size 1500",
"--rpc-max-subscriptions-per-connection=128000",
"--state-pruning=1024",
"-lsync=info",
"-ltxpool=debug",
]
[relaychain.genesis.runtimeGenesis.patch.balances]
devAccounts = [1000, 1000000000000000000, "//Sender//{}"]
[[relaychain.nodes]]
name = "alice"
rpc_port = 9944
validator = true
[[relaychain.nodes]]
name = "bob"
rpc_port = 9945
validator = true
@@ -0,0 +1,38 @@
[settings]
timeout = 1500
[relaychain]
default_image = "pezkuwichain/pezkuwi:latest"
default_command = "pezkuwi"
chain = "pezkuwichain-local"
default_args = [
"--pool-kbytes 2048000",
"--pool-limit 100000",
"--pool-type=fork-aware",
"--rpc-max-connections 15000",
"--rpc-max-response-size 150",
"--rpc-max-subscriptions-per-connection=128000",
"--state-pruning=1024",
"-lsync=info",
"-ltxpool=debug",
"-ltxpoolstat=debug",
]
[relaychain.genesis.runtimeGenesis.patch.balances]
devAccounts = [100000, 1000000000000000000, "//Sender//{}"]
[[relaychain.nodes]]
# command = "/home/miszka/parity/14-txpool-forks/pezkuwi-sdk-master-02/target/release-tokio-console/pezkuwi"
name = "alice"
rpc_port = 9944
validator = false
[[relaychain.nodes]]
name = "bob"
rpc_port = 9945
validator = true
[[relaychain.nodes]]
name = "charlie"
rpc_port = 9946
validator = true
@@ -0,0 +1,145 @@
// This file is part of Substrate.
// 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/>.
// Test inspired (copied) from:
// https://github.com/pezkuwichain/pezkuwi-sdk/blob/85b71daf7aac59da4d2186b45d589c7c619f0981/polkadot/zombienet-sdk-tests/tests/elastic_scaling/slot_based_3cores.rs#L21
// and patched as in:
// https://github.com/paritytech/polkadot-sdk/pull/7220#issuecomment-2808830472
use crate::zombienet::{BlockSubscriptionType, NetworkSpawner, ScenarioBuilderSharedParams};
use cumulus_zombienet_sdk_helpers::create_assign_core_call;
use serde_json::json;
use txtesttool::{execution_log::ExecutionLog, scenario::ScenarioBuilder};
use zombienet_sdk::{
subxt::{OnlineClient, PolkadotConfig},
subxt_signer::sr25519::dev,
NetworkConfigBuilder,
};
#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn slot_based_3cores_test() -> Result<(), anyhow::Error> {
let spawner = NetworkSpawner::with_closure(|| {
let images = zombienet_sdk::environment::get_images_from_env();
let names = ["alice", "bob", "charlie"];
NetworkConfigBuilder::new()
.with_relaychain(|r| {
let r = r
.with_chain("pezkuwichain-local")
.with_default_command("pezkuwi")
.with_default_image(images.pezkuwi.as_str())
.with_default_args(vec![("-lteyrchain=debug").into()])
.with_genesis_overrides(json!({
"configuration": {
"config": {
"scheduler_params": {
// Num cores is 2, because 1 extra will be added automatically when registering the para.
"num_cores": 2,
"max_validators_per_core": 1
}
}
}
}))
.with_default_resources(|resources| {
resources.with_request_cpu(4).with_request_memory("4G")
})
// Have to set a `with_node` outside of the loop below, so that `r` has the
// right type.
.with_node(|node| node.with_name(names[0]));
(1..3).fold(r, |acc, i| acc.with_node(|node| node.with_name(names[i])))
})
.with_teyrchain(|p| {
// Para 2200 uses the new RFC103-enabled collator which sends the UMP signal
// commitment for selecting the core index
p.with_id(2200)
.with_default_command("pezkuwi-teyrchain")
.with_default_image(images.cumulus.as_str())
.with_chain("yap-pezkuwichain-local-2200")
.with_genesis_overrides(json!({
"balances": {
"devAccounts": [
100000, 1000000000000000000u64, "//Sender//{}"
]
}
}))
.with_default_args(vec![
"--authoring=slot-based".into(),
"--rpc-max-subscriptions-per-connection=256000".into(),
"--rpc-max-connections=128000".into(),
"--rpc-max-response-size=150".into(),
"--pool-limit=2500000".into(),
"--pool-kbytes=4048000".into(),
"--pool-type=fork-aware".into(),
("-lteyrchain=debug,aura=debug,txpool=debug,txpoolstat=debug").into(),
])
.with_collator(|n| n.with_name("dave").with_rpc_port(9944))
})
})
.await
.unwrap();
let relay_node = spawner.network().get_node("alice")?;
let relay_client: OnlineClient<PolkadotConfig> = relay_node.wait_client().await?;
let alice = dev::alice();
let assign_cores_call = create_assign_core_call(&[(0, 2200), (1, 2200)]);
// Assign two extra cores to each teyrchain.
relay_client
.tx()
.sign_and_submit_then_watch_default(&assign_cores_call, &alice)
.await?
.wait_for_finalized_success()
.await?;
tracing::info!("2 more cores assigned to the teyrchain");
// Wait for the teyrchain collator to start block production.
spawner.wait_for_block("dave", BlockSubscriptionType::Best).await.unwrap();
// Create txs executor.
let ws = spawner.node_rpc_uri("dave").unwrap();
let executor = {
let shared_params = ScenarioBuilderSharedParams::default();
ScenarioBuilder::new()
.with_watched_txs(shared_params.watched_txs)
.with_send_threshold(shared_params.send_threshold)
.with_block_monitoring(shared_params.does_block_monitoring)
.with_chain_type(shared_params.chain_type)
.with_base_dir_path(spawner.base_dir_path().unwrap().to_string())
.with_timeout_in_secs(21600) //6 hours
.with_legacy_backend(true)
}
.with_rpc_uri(ws)
.with_start_id(0)
.with_last_id(99999)
.with_txs_count(150)
.with_executor_id("txs-executor".to_string())
.with_send_threshold(25000)
.build()
.await;
// Execute transactions and fetch the execution logs.
let execution_logs = executor.execute().await;
let finalized_txs = execution_logs.values().filter_map(|tx_log| tx_log.finalized()).count();
assert_eq!(finalized_txs, 15_000_000);
Ok(())
}