Single transactions
AcceptToMemoryPool()
(ATMP) is where the checks on single transactions occur before they enter the mempool.
You can see the calls to the various *Checks()
functions in the call graph, and the order in which they are run.
Let’s take a look inside AcceptToMemoryPool()
's inner function AcceptSingleTransaction()
which handles running the checks:
MempoolAcceptResult MemPoolAccept::AcceptSingleTransaction(const CTransactionRef& ptx, ATMPArgs& args)
{
AssertLockHeld(cs_main);
LOCK(m_pool.cs); // mempool "read lock" (held through GetMainSignals().TransactionAddedToMempool())
Workspace ws(ptx);
if (!PreChecks(args, ws)) return MempoolAcceptResult::Failure(ws.m_state);
if (m_rbf && !ReplacementChecks(ws)) return MempoolAcceptResult::Failure(ws.m_state);
// Perform the inexpensive checks first and avoid hashing and signature verification unless
// those checks pass, to mitigate CPU exhaustion denial-of-service attacks.
if (!PolicyScriptChecks(args, ws)) return MempoolAcceptResult::Failure(ws.m_state);
if (!ConsensusScriptChecks(args, ws)) return MempoolAcceptResult::Failure(ws.m_state);
// Tx was accepted, but not added
if (args.m_test_accept) {
return MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions), ws.m_vsize, ws.m_base_fees);
}
if (!Finalize(args, ws)) return MempoolAcceptResult::Failure(ws.m_state);
GetMainSignals().TransactionAddedToMempool(ptx, m_pool.GetAndIncrementSequence());
return MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions), ws.m_vsize, ws.m_base_fees);
}
We purposefully run checks in this order so that the least computationally-expensive checks are run first. This means that we can hopefully fail early and minimise CPU cycles used on invalid transactions. |
If an attacker could force us to perform many expensive computations simply by sending us many invalid transactions then it would be inexpensive to bring our node to a halt. |
Once AcceptSingleTransaction
has acquired the cs_main
and m_pool.cs
locks it initializes a Workspace
struct — a storage area for (validation status) state which can be shared by the different validation checks. Caching this state avoids performing the same computations multiple times and is important for performance. It will pass this workspace, along with the struct of ATMPArgs
it received as argument, to the checks.
Click to see the code comments on why we hold two locks before performing consensus checks on transactions
/**
* This mutex needs to be locked when accessing `mapTx` or other members
* that are guarded by it.
*
* @par Consistency guarantees
*
* By design, it is guaranteed that:
*
* 1. Locking both `cs_main` and `mempool.cs` will give a view of mempool
* that is consistent with current chain tip (`::ChainActive()` and
* `CoinsTip()`) and is fully populated. Fully populated means that if the
* current active chain is missing transactions that were present in a
* previously active chain, all the missing transactions will have been
* re-added to the mempool and should be present if they meet size and
* consistency constraints.
*
* 2. Locking `mempool.cs` without `cs_main` will give a view of a mempool
* consistent with some chain that was active since `cs_main` was last
* locked, and that is fully populated as described above. It is ok for
* code that only needs to query or remove transactions from the mempool
* to lock just `mempool.cs` without `cs_main`.
*
* To provide these guarantees, it is necessary to lock both `cs_main` and
* `mempool.cs` whenever adding transactions to the mempool and whenever
* changing the chain tip. It's necessary to keep both mutexes locked until
* the mempool is consistent with the new chain tip and fully populated.
*/
mutable RecursiveMutex cs;
The Workspace
is initialized with a pointer to the transaction (as a CTransactionRef
) and holds some additional information related to intermediate state.
We can look at the ATMPArgs
struct to see what other information our mempool wants to know about in addition to transaction information.
If all the checks pass and this was not a test_accept
submission then we will MemPoolAccept::Finalize
the transaction, adding it to the mempool, before trimming the mempool size and updating any affected RBF transactions as required.