mirror of
https://gitlab.com/pezkuwichain/pezkuwi-sdk.git
synced 2026-11-06 02:41:09 +00:00
feat: initialize Kurdistan SDK - independent fork of Polkadot SDK
This commit is contained in:
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",
|
||||
]
|
||||
@@ -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
+764
@@ -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
|
||||
}
|
||||
+62
@@ -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
|
||||
+63
@@ -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
|
||||
+108
@@ -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
|
||||
+30
@@ -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
|
||||
+38
@@ -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(())
|
||||
}
|
||||
Reference in new issue
Block a user