Skip to content

Commit

Permalink
merge bitcoin#22084: package testmempoolaccept followups
Browse files Browse the repository at this point in the history
inapplicable:
- 5cac95c (we don't have RBF)
  • Loading branch information
kwvg authored and PastaPastaPasta committed Feb 3, 2024
1 parent 792b430 commit a7ebe53
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 73 deletions.
1 change: 1 addition & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ libbitcoin_server_a_SOURCES = \
node/ui_interface.cpp \
noui.cpp \
policy/fees.cpp \
policy/packages.cpp \
policy/policy.cpp \
policy/settings.cpp \
pow.cpp \
Expand Down
62 changes: 62 additions & 0 deletions src/policy/packages.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) 2021 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <consensus/validation.h>
#include <policy/packages.h>
#include <primitives/transaction.h>
#include <uint256.h>
#include <util/hasher.h>

#include <numeric>
#include <unordered_set>

bool CheckPackage(const Package& txns, PackageValidationState& state)
{
const unsigned int package_count = txns.size();

if (package_count > MAX_PACKAGE_COUNT) {
return state.Invalid(PackageValidationResult::PCKG_POLICY, "package-too-many-transactions");
}

const int64_t total_size = std::accumulate(txns.cbegin(), txns.cend(), 0,
[](int64_t sum, const auto& tx) { return sum + GetVirtualTransactionSize(*tx); });
// If the package only contains 1 tx, it's better to report the policy violation on individual tx size.
if (package_count > 1 && total_size > MAX_PACKAGE_SIZE * 1000) {
return state.Invalid(PackageValidationResult::PCKG_POLICY, "package-too-large");
}

// Require the package to be sorted in order of dependency, i.e. parents appear before children.
// An unsorted package will fail anyway on missing-inputs, but it's better to quit earlier and
// fail on something less ambiguous (missing-inputs could also be an orphan or trying to
// spend nonexistent coins).
std::unordered_set<uint256, SaltedTxidHasher> later_txids;
std::transform(txns.cbegin(), txns.cend(), std::inserter(later_txids, later_txids.end()),
[](const auto& tx) { return tx->GetHash(); });
for (const auto& tx : txns) {
for (const auto& input : tx->vin) {
if (later_txids.find(input.prevout.hash) != later_txids.end()) {
// The parent is a subsequent transaction in the package.
return state.Invalid(PackageValidationResult::PCKG_POLICY, "package-not-sorted");
}
}
later_txids.erase(tx->GetHash());
}

// Don't allow any conflicting transactions, i.e. spending the same inputs, in a package.
std::unordered_set<COutPoint, SaltedOutpointHasher> inputs_seen;
for (const auto& tx : txns) {
for (const auto& input : tx->vin) {
if (inputs_seen.find(input.prevout) != inputs_seen.end()) {
// This input is also present in another tx in the package.
return state.Invalid(PackageValidationResult::PCKG_POLICY, "conflict-in-package");
}
}
// Batch-add all the inputs for a tx at a time. If we added them 1 at a time, we could
// catch duplicate inputs within a single tx. This is a more severe, consensus error,
// and we want to report that from CheckTransaction instead.
std::transform(tx->vin.cbegin(), tx->vin.cend(), std::inserter(inputs_seen, inputs_seen.end()),
[](const auto& input) { return input.prevout; });
}
return true;
}
10 changes: 10 additions & 0 deletions src/policy/packages.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#define BITCOIN_POLICY_PACKAGES_H

#include <consensus/validation.h>
#include <policy/policy.h>
#include <primitives/transaction.h>

#include <vector>
Expand All @@ -14,6 +15,7 @@
static constexpr uint32_t MAX_PACKAGE_COUNT{25};
/** Default maximum total virtual size of transactions in a package in KvB. */
static constexpr uint32_t MAX_PACKAGE_SIZE{101};
static_assert(MAX_PACKAGE_SIZE * 1000 >= MAX_STANDARD_TX_SIZE);

/** A "reason" why a package was invalid. It may be that one or more of the included
* transactions is invalid or the package itself violates our rules.
Expand All @@ -31,4 +33,12 @@ using Package = std::vector<CTransactionRef>;

class PackageValidationState : public ValidationState<PackageValidationResult> {};

/** Context-free package policy checks:
* 1. The number of transactions cannot exceed MAX_PACKAGE_COUNT.
* 2. The total virtual size cannot exceed MAX_PACKAGE_SIZE.
* 3. If any dependencies exist between transactions, parents must appear before children.
* 4. Transactions cannot conflict, i.e., spend the same inputs.
*/
bool CheckPackage(const Package& txns, PackageValidationState& state);

#endif // BITCOIN_POLICY_PACKAGES_H
10 changes: 5 additions & 5 deletions src/rpc/rawtransaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1159,7 +1159,7 @@ static UniValue testmempoolaccept(const JSONRPCRequest& request)
"\nReturns result of mempool acceptance tests indicating if raw transaction (serialized, hex-encoded) would be accepted by mempool.\n"
"\nIf multiple transactions are passed in, parents must come before children and package policies apply: the transactions cannot conflict with any mempool transactions or each other.\n"
"\nIf one transaction fails, other transactions may not be fully validated (the 'allowed' key will be blank).\n"
"\nThe maximum number of transactions allowed is 25 (MAX_PACKAGE_COUNT)\n"
"\nThe maximum number of transactions allowed is " + ToString(MAX_PACKAGE_COUNT) + ".\n"
"\nThis checks if transactions violate the consensus or policy rules.\n"
"\nSee sendrawtransaction call.\n",
{
Expand All @@ -1174,7 +1174,7 @@ static UniValue testmempoolaccept(const JSONRPCRequest& request)
RPCResult{
RPCResult::Type::ARR, "", "The result of the mempool acceptance test for each raw transaction in the input array.\n"
"Returns results for each transaction in the same order they were passed in.\n"
"It is possible for transactions to not be fully validated ('allowed' unset) if an earlier transaction failed.\n",
"It is possible for transactions to not be fully validated ('allowed' unset) if another transaction failed.\n",
{
{RPCResult::Type::OBJ, "", "",
{
Expand Down Expand Up @@ -1207,7 +1207,6 @@ static UniValue testmempoolaccept(const JSONRPCRequest& request)
UniValue::VARR,
UniValueType(), // VNUM or VSTR, checked inside AmountFromValue()
});

const UniValue raw_transactions = request.params[0].get_array();
if (raw_transactions.size() < 1 || raw_transactions.size() > MAX_PACKAGE_COUNT) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
Expand All @@ -1219,6 +1218,7 @@ static UniValue testmempoolaccept(const JSONRPCRequest& request)
CFeeRate(AmountFromValue(request.params[1]));

std::vector<CTransactionRef> txns;
txns.reserve(raw_transactions.size());
for (const auto& rawtx : raw_transactions.getValues()) {
CMutableTransaction mtx;
if (!DecodeHexTx(mtx, rawtx.get_str())) {
Expand All @@ -1239,8 +1239,8 @@ static UniValue testmempoolaccept(const JSONRPCRequest& request)
}();

UniValue rpc_result(UniValue::VARR);
// We will check transaction fees we iterate through txns in order. If any transaction fee
// exceeds maxfeerate, we will keave the rest of the validation results blank, because it
// We will check transaction fees while we iterate through txns in order. If any transaction fee
// exceeds maxfeerate, we will leave the rest of the validation results blank, because it
// doesn't make sense to return a validation result for a transaction if its ancestor(s) would
// not be submitted.
bool exit_early{false};
Expand Down
4 changes: 2 additions & 2 deletions src/test/miner_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ struct MinerTestingSetup : public TestingSetup {
void TestPackageSelection(const CChainParams& chainparams, const CScript& scriptPubKey, const std::vector<CTransactionRef>& txFirst) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, m_node.mempool->cs);
bool TestSequenceLocks(const CTransaction& tx, int flags) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, m_node.mempool->cs)
{
CCoinsViewMemPool viewMempool(&m_node.chainman->ActiveChainstate().CoinsTip(), *m_node.mempool);
return CheckSequenceLocks(m_node.chainman->ActiveChain().Tip(), viewMempool, tx, flags);
CCoinsViewMemPool view_mempool(&m_node.chainman->ActiveChainstate().CoinsTip(), *m_node.mempool);
return CheckSequenceLocks(m_node.chainman->ActiveChain().Tip(), view_mempool, tx, flags);
}
BlockAssembler AssemblerForTest(const CChainParams& params);
};
Expand Down
4 changes: 2 additions & 2 deletions src/txmempool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -728,9 +728,9 @@ void CTxMemPool::removeForReorg(CChainState& active_chainstate, int flags) EXCLU
LockPoints lp = it->GetLockPoints();
assert(std::addressof(::ChainstateActive()) == std::addressof(active_chainstate));
bool validLP = TestLockPointValidity(active_chainstate.m_chain, &lp);
CCoinsViewMemPool viewMempool(&active_chainstate.CoinsTip(), *this);
CCoinsViewMemPool view_mempool(&active_chainstate.CoinsTip(), *this);
if (!CheckFinalTx(active_chainstate.m_chain.Tip(), tx, flags)
|| !CheckSequenceLocks(active_chainstate.m_chain.Tip(), viewMempool, tx, flags, &lp, validLP)) {
|| !CheckSequenceLocks(active_chainstate.m_chain.Tip(), view_mempool, tx, flags, &lp, validLP)) {
// Note if CheckSequenceLocks fails the LockPoints may still be invalid
// So it's critical that we remove the tx and not depend on the LockPoints.
txToRemove.insert(it);
Expand Down
3 changes: 2 additions & 1 deletion src/txmempool.h
Original file line number Diff line number Diff line change
Expand Up @@ -859,7 +859,8 @@ class CCoinsViewMemPool : public CCoinsViewBacked
public:
CCoinsViewMemPool(CCoinsView* baseIn, const CTxMemPool& mempoolIn);
bool GetCoin(const COutPoint &outpoint, Coin &coin) const override;
/** Add the coins created by this transaction. */
/** Add the coins created by this transaction. These coins are only temporarily stored in
* m_temp_added and cannot be flushed to the back end. Only used for package validation. */
void PackageAddTransaction(const CTransactionRef& tx);
};

Expand Down
63 changes: 6 additions & 57 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ class MemPoolAccept
/**
* 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, otherwise a TX_MISSING_INPUTS error will be returned.
* dependencies exist.
*/
PackageMempoolAcceptResult AcceptMultipleTransactions(const std::vector<CTransactionRef>& txns, ATMPArgs& args) EXCLUSIVE_LOCKS_REQUIRED(cs_main);

Expand Down Expand Up @@ -982,65 +982,15 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
{
AssertLockHeld(cs_main);

// These context-free package limits can be done before taking the mempool lock.
PackageValidationState package_state;
const unsigned int package_count = txns.size();
if (!CheckPackage(txns, package_state)) return PackageMempoolAcceptResult(package_state, {});

// These context-free package limits can be checked before taking the mempool lock.
if (package_count > MAX_PACKAGE_COUNT) {
package_state.Invalid(PackageValidationResult::PCKG_POLICY, "package-too-many-transactions");
return PackageMempoolAcceptResult(package_state, {});
}

const int64_t total_size = std::accumulate(txns.cbegin(), txns.cend(), 0,
[](int64_t sum, const auto& tx) { return sum + GetVirtualTransactionSize(*tx); });
// If the package only contains 1 tx, it's better to report the policy violation on individual tx size.
if (package_count > 1 && total_size > MAX_PACKAGE_SIZE * 1000) {
package_state.Invalid(PackageValidationResult::PCKG_POLICY, "package-too-large");
return PackageMempoolAcceptResult(package_state, {});
}

// Construct workspaces and check package policies.
std::vector<Workspace> workspaces{};
workspaces.reserve(package_count);
{
std::unordered_set<uint256, SaltedTxidHasher> later_txids;
std::transform(txns.cbegin(), txns.cend(), std::inserter(later_txids, later_txids.end()),
[](const auto& tx) { return tx->GetHash(); });
// Require the package to be sorted in order of dependency, i.e. parents appear before children.
// An unsorted package will fail anyway on missing-inputs, but it's better to quit earlier and
// fail on something less ambiguous (missing-inputs could also be an orphan or trying to
// spend nonexistent coins).
for (const auto& tx : txns) {
for (const auto& input : tx->vin) {
if (later_txids.find(input.prevout.hash) != later_txids.end()) {
// The parent is a subsequent transaction in the package.
package_state.Invalid(PackageValidationResult::PCKG_POLICY, "package-not-sorted");
return PackageMempoolAcceptResult(package_state, {});
}
}
later_txids.erase(tx->GetHash());
workspaces.emplace_back(Workspace(tx));
}
}
workspaces.reserve(txns.size());
std::transform(txns.cbegin(), txns.cend(), std::back_inserter(workspaces),
[](const auto& tx) { return Workspace(tx); });
std::map<const uint256, const MempoolAcceptResult> results;
{
// Don't allow any conflicting transactions, i.e. spending the same inputs, in a package.
std::unordered_set<COutPoint, SaltedOutpointHasher> inputs_seen;
for (const auto& tx : txns) {
for (const auto& input : tx->vin) {
if (inputs_seen.find(input.prevout) != inputs_seen.end()) {
// This input is also present in another tx in the package.
package_state.Invalid(PackageValidationResult::PCKG_POLICY, "conflict-in-package");
return PackageMempoolAcceptResult(package_state, {});
}
}
// Batch-add all the inputs for a tx at a time. If we added them 1 at a time, we could
// catch duplicate inputs within a single tx. This is a more severe, consensus error,
// and we want to report that from CheckTransaction instead.
std::transform(tx->vin.cbegin(), tx->vin.cend(), std::inserter(inputs_seen, inputs_seen.end()),
[](const auto& input) { return input.prevout; });
}
}

LOCK(m_pool.cs);

Expand Down Expand Up @@ -1127,7 +1077,6 @@ PackageMempoolAcceptResult ProcessNewPackage(CChainState& active_chainstate, CTx
const PackageMempoolAcceptResult result = MemPoolAccept(pool, active_chainstate).AcceptMultipleTransactions(package, args);

// Uncache coins pertaining to transactions that were not submitted to the mempool.
// Ensure the cache is still within its size limits.
for (const COutPoint& hashTx : coins_to_uncache) {
active_chainstate.CoinsTip().Uncache(hashTx);
}
Expand Down
18 changes: 12 additions & 6 deletions src/validation.h
Original file line number Diff line number Diff line change
Expand Up @@ -291,10 +291,12 @@ MempoolAcceptResult AcceptToMemoryPool(CChainState& active_chainstate, CTxMemPoo
bool bypass_limits, bool test_accept=false) EXCLUSIVE_LOCKS_REQUIRED(cs_main);

/**
* Atomically test acceptance of a package. If the package only contains one tx, package rules still apply.
* Atomically test acceptance of a package. If the package only contains one tx, package rules still
* apply. The transaction(s) cannot spend the same inputs as any transaction in the mempool.
* @param[in] txns Group of transactions which may be independent or contain
* parent-child dependencies. The transactions must not conflict, i.e.
* must not spend the same inputs. Parents must appear before children.
* parent-child dependencies. The transactions must not conflict
* with each other, i.e., must not spend the same inputs. If any
* dependencies exist, parents must appear before children.
* @returns a PackageMempoolAcceptResult which includes a MempoolAcceptResult for each transaction.
* If a transaction fails, validation will exit early and some results may be missing.
*/
Expand Down Expand Up @@ -329,9 +331,13 @@ bool TestLockPointValidity(CChain& active_chain, const LockPoints* lp) EXCLUSIVE
* Check if transaction will be BIP68 final in the next block to be created on top of tip.
* @param[in] tip Chain tip to check tx sequence locks against. For example,
* the tip of the current active chain.
* @param[in] coins_view Any CCoinsView that provides access to the relevant coins
* for checking sequence locks. Any CCoinsView can be passed in;
* it is assumed to be consistent with the tip.
* @param[in] coins_view Any CCoinsView that provides access to the relevant coins for
* checking sequence locks. For example, it can be a CCoinsViewCache
* that isn't connected to anything but contains all the relevant
* coins, or a CCoinsViewMemPool that is connected to the
* mempool and chainstate UTXO set. In the latter case, the caller is
* responsible for holding the appropriate locks to ensure that
* calls to GetCoin() return correct coins.
* Simulates calling SequenceLocks() with data from the tip passed in.
* Optionally stores in LockPoints the resulting height and time calculated and the hash
* of the block needed for calculation or skips the calculation and uses the LockPoints
Expand Down

0 comments on commit a7ebe53

Please sign in to comment.