Files
pezkuwi-subxt/substrate/client/transaction-pool
David 29c0c6a4a8 jsonrpsee integration (#8783)
* Add tokio

* No need to map CallError to CallError

* jsonrpsee proc macros (#9673)

* port error types to `JsonRpseeError`

* migrate chain module to proc macro api

* make it compile with proc macros

* update branch

* update branch

* update to jsonrpsee master

* port system rpc

* port state rpc

* port childstate & offchain

* frame system rpc

* frame transaction payment

* bring back CORS hack to work with polkadot UI

* port babe rpc

* port manual seal rpc

* port frame mmr rpc

* port frame contracts rpc

* port finality grandpa rpc

* port sync state rpc

* resolve a few TODO + no jsonrpc deps

* Update bin/node/rpc-client/src/main.rs

* Update bin/node/rpc-client/src/main.rs

* Update bin/node/rpc-client/src/main.rs

* Update bin/node/rpc-client/src/main.rs

* Port over system_ rpc tests

* Make it compile

* Use prost 0.8

* Use prost 0.8

* Make it compile

* Ignore more failing tests

* Comment out WIP tests

* fix nit in frame system api

* Update lockfile

* No more juggling tokio versions

* No more wait_for_stop ?

* Remove browser-testing

* Arguments must be arrays

* Use same argument names

* Resolve todo: no wait_for_stop for WS server
Add todo: is parse_rpc_result used?
Cleanup imports

* fmt

* log

* One test passes

* update jsonrpsee

* update jsonrpsee

* cleanup rpc-servers crate

* jsonrpsee: add host and origin filtering (#9787)

* add access control in the jsonrpsee servers

* use master

* fix nits

* rpc runtime_version safe

* fix nits

* fix grumbles

* remove unused files

* resolve some todos

* jsonrpsee more cleanup (#9803)

* more cleanup

* resolve TODOs

* fix some unwraps

* remove type hints

* update jsonrpsee

* downgrade zeroize

* pin jsonrpsee rev

* remove unwrap nit

* Comment out more tests that aren't ported

* Comment out more tests

* Fix tests after merge

* Subscription test

* Invalid nonce test

* Pending exts

* WIP removeExtrinsic test

* Test remove_extrinsic

* Make state test: should_return_storage work

* Uncomment/fix the other non-subscription related state tests

* test: author_insertKey

* test: author_rotateKeys

* Get rest of state tests passing

* asyncify a little more

* Add todo to note #msg change

* Crashing test for has_session_keys

* Fix error conversion to avoid stack overflows
Port author_hasSessionKeys test
fmt

* test author_hasKey

* Add two missing tests
Add a check on the return type
Add todos for James's concerns

* RPC tests for state, author and system (#9859)

* Fix test runner

* Impl Default for SubscriptionTaskExecutor

* Keep the minimul amount of code needed to compile tests

* Re-instate `RpcSession` (for now)

* cleanup

* Port over RPC tests

* Add tokio

* No need to map CallError to CallError

* Port over system_ rpc tests

* Make it compile

* Use prost 0.8

* Use prost 0.8

* Make it compile

* Ignore more failing tests

* Comment out WIP tests

* Update lockfile

* No more juggling tokio versions

* No more wait_for_stop ?

* Remove browser-testing

* Arguments must be arrays

* Use same argument names

* Resolve todo: no wait_for_stop for WS server
Add todo: is parse_rpc_result used?
Cleanup imports

* fmt

* log

* One test passes

* Comment out more tests that aren't ported

* Comment out more tests

* Fix tests after merge

* Subscription test

* Invalid nonce test

* Pending exts

* WIP removeExtrinsic test

* Test remove_extrinsic

* Make state test: should_return_storage work

* Uncomment/fix the other non-subscription related state tests

* test: author_insertKey

* test: author_rotateKeys

* Get rest of state tests passing

* asyncify a little more

* Add todo to note #msg change

* Crashing test for has_session_keys

* Fix error conversion to avoid stack overflows
Port author_hasSessionKeys test
fmt

* test author_hasKey

* Add two missing tests
Add a check on the return type
Add todos for James's concerns

* offchain rpc tests

* Address todos

* fmt

Co-authored-by: James Wilson <james@jsdw.me>

* fix drop in state test

* update jsonrpsee

* fix ignored system test

* fix chain tests

* remove some boiler plate

* Port BEEFY RPC (#9883)

* Merge master

* Port beefy RPC (ty @niklas!)

* trivial changes left over from merge

* Remove unused code

* Update jsonrpsee

* fix build

* make tests compile again

* beefy update jsonrpsee

* fix: respect rpc methods policy

* update cargo.lock

* update jsonrpsee

* update jsonrpsee

* downgrade error logs

* update jsonrpsee

* Fix typo

* remove unused file

* Better name

* Port Babe RPC tests

* Put docs back

* Resolve todo

* Port tests for System RPCs

* Resolve todo

* fix build

* Updated jsonrpsee to current master

* fix: port finality grandpa rpc tests

* Move .into() outside of the match

* more review grumbles

* jsonrpsee: add `rpc handlers` back (#10245)

* add back RpcHandlers

* cargo fmt

* fix docs

* fix grumble: remove needless alloc

* resolve TODO

* fmt

* Fix typo

* grumble: Use constants based on BASE_ERROR

* grumble: DRY whitelisted listening addresses
grumble: s/JSONRPC/JSON-RPC/

* cleanup

* grumbles: Making readers aware of the possibility of gaps

* review grumbles

* grumbles

* remove notes from niklasad1

* Update `jsonrpsee`

* fix: jsonrpsee features

* jsonrpsee: fallback to random port in case the specified port failed (#10304)

* jsonrpsee: fallback to random port

* better comment

* Update client/rpc-servers/src/lib.rs

Co-authored-by: Maciej Hirsz <1096222+maciejhirsz@users.noreply.github.com>

* Update client/rpc-servers/src/lib.rs

Co-authored-by: Maciej Hirsz <1096222+maciejhirsz@users.noreply.github.com>

* address grumbles

* cargo fmt

* addrs already slice

Co-authored-by: Maciej Hirsz <1096222+maciejhirsz@users.noreply.github.com>

* Update jsonrpsee to 092081a0a2b8904c6ebd2cd99e16c7bc13ffc3ae

* lockfile

* update jsonrpsee

* fix warning

* Don't fetch jsonrpsee from crates

* make tests compile again

* fix rpc tests

* remove unused deps

* update tokio

* fix rpc tests again

* fix: test runner

`HttpServerBuilder::builder` fails unless it's called within tokio runtime

* cargo fmt

* grumbles: fix subscription aliases

* make clippy happy

* update remaining subscriptions alias

* cleanup

* cleanup

* fix chain subscription: less boiler plate (#10285)

* fix chain subscription: less boiler plate

* fix bad merge

* cargo fmt

* Switch to jsonrpsee 0.5

* fix build

* add missing features

* fix nit: remove needless Box::pin

* Integrate jsonrpsee metrics (#10395)

* draft metrics impl

* Use latest api

* Add missing file

* Http server metrics

* cleanup

* bump jsonrpsee

* Remove `ServerMetrics` and use a single middleware for both connection counting (aka sessions) and call metrics.

* fix build

* remove needless Arc::clone

* Update to jsonrpsee 0.6

* lolz

* fix metrics

* Revert "lolz"

This reverts commit eed6c6a56e78d8e307b4950f4c52a1c3a2322ba1.

* fix: in-memory rpc support subscriptions

* commit Cargo.lock

* Update tests to 0.7

* fix TODOs

* ws server: generate subscriptionIDs as Strings

Some libraries seems to expect the subscription IDs to be Strings, let's not break
this in this PR.

* Increase timeout

* Port over tests

* cleanup

* Using error codes from the spec

* fix clippy

* cargo fmt

* update jsonrpsee

* fix nits

* fix: rpc_query

* enable custom subid gen through spawn_tasks

* remove unsed deps

* unify tokio deps

* Revert "enable custom subid gen through spawn_tasks"

This reverts commit 5c5eb70328fe39d154fdb55c56e637b4548cf470.

* fix bad merge of `test-utils`

* fix more nits

* downgrade wasm-instrument to 0.1.0

* [jsonrpsee]: enable custom RPC subscription ID generatation (#10731)

* enable custom subid gen through spawn_tasks

* fix nits

* Update client/service/src/builder.rs

Co-authored-by: David <dvdplm@gmail.com>

* add Poc; needs jsonrpsee pr

* update jsonrpsee

* add re-exports

* add docs

Co-authored-by: David <dvdplm@gmail.com>

* cargo fmt

* fmt

* port RPC-API dev

* Remove unused file

* fix nit: remove async trait

* fix doc links

* fix merge nit: remove jsonrpc deps

* kill namespace on rpc apis

* companion for jsonrpsee v0.10 (#11158)

* companion for jsonrpsee v0.10

* update versions v0.10.0

* add some fixes

* spelling

* fix spaces

Co-authored-by: Niklas Adolfsson <niklasadolfsson1@gmail.com>

* send error before subs are closed

* fix unsubscribe method names: chain

* fix tests

* jsonrpc server: print binded local address

* grumbles: kill SubscriptionTaskExecutor

* Update client/sync-state-rpc/src/lib.rs

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Update client/rpc/src/chain/chain_full.rs

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Update client/rpc/src/chain/chain_full.rs

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* sync-state-rpc: kill anyhow

* no more anyhow

* remove todo

* jsonrpsee:  fix bad params in subscriptions. (#11251)

* update jsonrpsee

* fix error responses

* revert error codes

* dont do weird stuff in drop impl

* rpc servers: remove needless clone

* Remove silly constants

* chore: update jsonrpsee v0.12

* commit Cargo.lock

* deps: downgrade git2

* feat: CLI flag max subscriptions per connection

* metrics: use old logging format

* fix: read WS address from substrate output (#11379)

Co-authored-by: Niklas Adolfsson <niklasadolfsson1@gmail.com>
Co-authored-by: James Wilson <james@jsdw.me>
Co-authored-by: Maciej Hirsz <hello@maciej.codes>
Co-authored-by: Maciej Hirsz <1096222+maciejhirsz@users.noreply.github.com>
Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>
2022-05-10 08:52:19 +00:00
..
2022-05-10 08:52:19 +00:00
2022-04-30 21:28:27 +00:00
2022-04-03 21:44:29 +02:00
2022-05-04 13:38:54 +00:00
2022-01-18 19:05:12 +00:00

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 the runtime are encapsulated in the TransactionValidity type.

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 TransactionTags (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 runned 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.
  2. 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.
  3. 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 everyting 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.

  2. 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.

  3. Transactions with exactly the same bytes are most likely going to give the same validity results. We can essentially treat them as identical.

  4. Watch out for re-organisations and re-importing transactions from retracted blocks.

  5. 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.

  6. 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.

  7. 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.

  8. 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).

  2. 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.

  3. We periodically validate all transactions in the pool in batches.

  4. To minimize runtime calls, we introduce the batch-verify call. Note it should reset the state (overlay) after every verification.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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 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).
  2. Handling block import notifications and doing pruning + re-importing of transactions from retracted blocks.
  3. 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).