From be3ff151a1f9665720cdf70d072b098a2f9726a9 Mon Sep 17 00:00:00 2001 From: glozow Date: Tue, 20 Jul 2021 11:07:25 +0100 Subject: [PATCH] [validation] full package accept + mempool submission --- src/validation.cpp | 111 +++++++++++++++++++++++++++++++++++++++++---- src/validation.h | 4 +- 2 files changed, 106 insertions(+), 9 deletions(-) diff --git a/src/validation.cpp b/src/validation.cpp index 08d37aec0c5..692c18983fe 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -451,6 +451,11 @@ public: * any transaction spending the same inputs as a transaction in the mempool is considered * a conflict. */ const bool m_allow_bip125_replacement; + /** When true, the mempool will not be trimmed when individual transactions are submitted in + * Finalize(). Instead, limits should be enforced at the end to ensure the package is not + * partially submitted. + */ + const bool m_package_submission; /** Parameters for single transaction mempool validation. */ static ATMPArgs SingleAccept(const CChainParams& chainparams, int64_t accept_time, @@ -462,6 +467,7 @@ public: /* m_coins_to_uncache */ coins_to_uncache, /* m_test_accept */ test_accept, /* m_allow_bip125_replacement */ true, + /* m_package_submission */ false, }; } @@ -474,6 +480,7 @@ public: /* m_coins_to_uncache */ coins_to_uncache, /* m_test_accept */ true, /* m_allow_bip125_replacement */ false, + /* m_package_submission */ false, // not submitting to mempool }; } @@ -486,6 +493,7 @@ public: /* m_coins_to_uncache */ coins_to_uncache, /* m_test_accept */ false, /* m_allow_bip125_replacement */ false, + /* m_package_submission */ true, }; } // No default ctor to avoid exposing details to clients and allowing the possibility of @@ -497,9 +505,9 @@ public: MempoolAcceptResult AcceptSingleTransaction(const CTransactionRef& ptx, ATMPArgs& args) EXCLUSIVE_LOCKS_REQUIRED(cs_main); /** - * Multiple transaction acceptance. Transactions may or may not be interdependent, - * but must not conflict with each other. Parents must come before children if any - * dependencies exist. + * Multiple transaction acceptance. Transactions may or may not be interdependent, but must not + * conflict with each other, and the transactions cannot already be in the mempool. Parents must + * come before children if any dependencies exist. */ PackageMempoolAcceptResult AcceptMultipleTransactions(const std::vector& txns, ATMPArgs& args) EXCLUSIVE_LOCKS_REQUIRED(cs_main); @@ -581,6 +589,14 @@ private: // limiting is performed, false otherwise. bool Finalize(const ATMPArgs& args, Workspace& ws) EXCLUSIVE_LOCKS_REQUIRED(cs_main, m_pool.cs); + // Submit all transactions to the mempool and call ConsensusScriptChecks to add to the script + // cache - should only be called after successful validation of all transactions in the package. + // The package may end up partially-submitted after size limitting; returns true if all + // transactions are successfully added to the mempool, false otherwise. + bool FinalizePackage(const ATMPArgs& args, std::vector& workspaces, PackageValidationState& package_state, + std::map& results) + EXCLUSIVE_LOCKS_REQUIRED(cs_main, m_pool.cs); + // Compare a package's feerate against minimum allowed. bool CheckFeeRate(size_t package_size, CAmount package_fee, TxValidationState& state) EXCLUSIVE_LOCKS_REQUIRED(cs_main, m_pool.cs) { @@ -992,13 +1008,18 @@ bool MemPoolAccept::Finalize(const ATMPArgs& args, Workspace& ws) // - it's not being re-added during a reorg which bypasses typical mempool fee limits // - the node is not behind // - the transaction is not dependent on any other transactions in the mempool - bool validForFeeEstimation = !bypass_limits && IsCurrentForFeeEstimation(m_active_chainstate) && m_pool.HasNoInputsOf(tx); + // - it's not part of a package. Since package relay is not currently supported, this + // transaction has not necessarily been accepted to miners' mempools. + bool validForFeeEstimation = !bypass_limits && !args.m_package_submission && IsCurrentForFeeEstimation(m_active_chainstate) && m_pool.HasNoInputsOf(tx); // Store transaction in memory m_pool.addUnchecked(*entry, ws.m_ancestors, validForFeeEstimation); // trim mempool and check if tx was trimmed - if (!bypass_limits) { + // If we are validating a package, don't trim here because we could evict a previous transaction + // in the package. LimitMempoolSize() should be called at the very end to make sure the mempool + // is still within limits and package submission happens atomically. + if (!args.m_package_submission && !bypass_limits) { LimitMempoolSize(m_pool, m_active_chainstate.CoinsTip(), gArgs.GetIntArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000, std::chrono::hours{gArgs.GetIntArg("-mempoolexpiry", DEFAULT_MEMPOOL_EXPIRY)}); if (!m_pool.exists(GenTxid::Txid(hash))) return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "mempool full"); @@ -1006,6 +1027,69 @@ bool MemPoolAccept::Finalize(const ATMPArgs& args, Workspace& ws) return true; } +bool MemPoolAccept::FinalizePackage(const ATMPArgs& args, std::vector& workspaces, + PackageValidationState& package_state, + std::map& results) +{ + AssertLockHeld(cs_main); + AssertLockHeld(m_pool.cs); + bool all_submitted = true; + // ConsensusScriptChecks adds to the script cache and is therefore consensus-critical; + // CheckInputsFromMempoolAndCache asserts that transactions only spend coins available from the + // mempool or UTXO set. Submit each transaction to the mempool immediately after calling + // ConsensusScriptChecks to make the outputs available for subsequent transactions. + for (Workspace& ws : workspaces) { + if (!ConsensusScriptChecks(args, ws)) { + results.emplace(ws.m_ptx->GetWitnessHash(), MempoolAcceptResult::Failure(ws.m_state)); + // Since PolicyScriptChecks() passed, this should never fail. + all_submitted = Assume(false); + } + + // Re-calculate mempool ancestors to call addUnchecked(). They may have changed since the + // last calculation done in PreChecks, since package ancestors have already been submitted. + std::string err_string; + if(!m_pool.CalculateMemPoolAncestors(*ws.m_entry, ws.m_ancestors, m_limit_ancestors, + m_limit_ancestor_size, m_limit_descendants, + m_limit_descendant_size, err_string)) { + results.emplace(ws.m_ptx->GetWitnessHash(), MempoolAcceptResult::Failure(ws.m_state)); + // Since PreChecks() and PackageMempoolChecks() both enforce limits, this should never fail. + all_submitted = Assume(false); + } + // If we call LimitMempoolSize() for each individual Finalize(), the mempool will not take + // the transaction's descendant feerate into account because it hasn't seen them yet. Also, + // we risk evicting a transaction that a subsequent package transaction depends on. Instead, + // allow the mempool to temporarily bypass limits, the maximum package size) while + // submitting transactions individually and then trim at the very end. + if (!Finalize(args, ws)) { + results.emplace(ws.m_ptx->GetWitnessHash(), MempoolAcceptResult::Failure(ws.m_state)); + // Since LimitMempoolSize() won't be called, this should never fail. + all_submitted = Assume(false); + } + } + + // It may or may not be the case that all the transactions made it into the mempool. Regardless, + // make sure we haven't exceeded max mempool size. + LimitMempoolSize(m_pool, m_active_chainstate.CoinsTip(), + gArgs.GetIntArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000, + std::chrono::hours{gArgs.GetIntArg("-mempoolexpiry", DEFAULT_MEMPOOL_EXPIRY)}); + if (!all_submitted) return false; + + // Find the wtxids of the transactions that made it into the mempool. Allow partial submission, + // but don't report success unless they all made it into the mempool. + for (Workspace& ws : workspaces) { + if (m_pool.exists(GenTxid::Wtxid(ws.m_ptx->GetWitnessHash()))) { + results.emplace(ws.m_ptx->GetWitnessHash(), + MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions), ws.m_vsize, ws.m_base_fees)); + GetMainSignals().TransactionAddedToMempool(ws.m_ptx, m_pool.GetAndIncrementSequence()); + } else { + all_submitted = false; + ws.m_state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "mempool full"); + results.emplace(ws.m_ptx->GetWitnessHash(), MempoolAcceptResult::Failure(ws.m_state)); + } + } + return all_submitted; +} + MempoolAcceptResult MemPoolAccept::AcceptSingleTransaction(const CTransactionRef& ptx, ATMPArgs& args) { AssertLockHeld(cs_main); @@ -1091,6 +1175,13 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std:: } } + if (args.m_test_accept) return PackageMempoolAcceptResult(package_state, std::move(results)); + + if (!FinalizePackage(args, workspaces, package_state, results)) { + package_state.Invalid(PackageValidationResult::PCKG_TX, "submission failed"); + return PackageMempoolAcceptResult(package_state, std::move(results)); + } + return PackageMempoolAcceptResult(package_state, std::move(results)); } @@ -1187,7 +1278,6 @@ PackageMempoolAcceptResult ProcessNewPackage(CChainState& active_chainstate, CTx const Package& package, bool test_accept) { AssertLockHeld(cs_main); - assert(test_accept); // Only allow package accept dry-runs (testmempoolaccept RPC). assert(!package.empty()); assert(std::all_of(package.cbegin(), package.cend(), [](const auto& tx){return tx != nullptr;})); @@ -1205,9 +1295,14 @@ PackageMempoolAcceptResult ProcessNewPackage(CChainState& active_chainstate, CTx }(); // Uncache coins pertaining to transactions that were not submitted to the mempool. - for (const COutPoint& hashTx : coins_to_uncache) { - active_chainstate.CoinsTip().Uncache(hashTx); + // Ensure the coins cache is still within limits. + if (test_accept || result.m_state.IsInvalid()) { + for (const COutPoint& hashTx : coins_to_uncache) { + active_chainstate.CoinsTip().Uncache(hashTx); + } } + BlockValidationState state_dummy; + active_chainstate.FlushStateToDisk(state_dummy, FlushStateMode::PERIODIC); return result; } diff --git a/src/validation.h b/src/validation.h index 2621d3e7f13..4578a5e02ec 100644 --- a/src/validation.h +++ b/src/validation.h @@ -218,8 +218,10 @@ MempoolAcceptResult AcceptToMemoryPool(CChainState& active_chainstate, CTxMemPoo /** * Validate (and maybe submit) a package to the mempool. See doc/policy/packages.md for full details * on package validation rules. +* @param[in] test_accept When true, run validation checks but don't submit to mempool. * @returns a PackageMempoolAcceptResult which includes a MempoolAcceptResult for each transaction. -* If a transaction fails, validation will exit early and some results may be missing. +* If a transaction fails, validation will exit early and some results may be missing. It is also +* possible for the package to be partially submitted. */ PackageMempoolAcceptResult ProcessNewPackage(CChainState& active_chainstate, CTxMemPool& pool, const Package& txns, bool test_accept)