Skip to content

Commit e12fafd

Browse files
committed
[validation] de-duplicate package transactions already in mempool
As node operators are free to set their mempool policies however they please, it's possible for package transaction(s) to already be in the mempool. We definitely don't want to reject the entire package in that case (as that could be a censorship vector). We should still return the successful result to the caller, so add another result type to MempoolAcceptResult.
1 parent 8310d94 commit e12fafd

File tree

3 files changed

+61
-3
lines changed

3 files changed

+61
-3
lines changed

src/rpc/rawtransaction.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,8 @@ static RPCHelpMan testmempoolaccept()
974974
continue;
975975
}
976976
const auto& tx_result = it->second;
977+
// Package testmempoolaccept doesn't allow transactions to already be in the mempool.
978+
CHECK_NONFATAL(tx_result.m_result_type != MempoolAcceptResult::ResultType::MEMPOOL_ENTRY);
977979
if (tx_result.m_result_type == MempoolAcceptResult::ResultType::VALID) {
978980
const CAmount fee = tx_result.m_base_fees.value();
979981
// Check that fee does not exceed maximum fee

src/validation.cpp

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,10 @@ bool MemPoolAccept::PackageMempoolChecks(const std::vector<CTransactionRef>& txn
916916
AssertLockHeld(cs_main);
917917
AssertLockHeld(m_pool.cs);
918918

919+
// CheckPackageLimits expects the package transactions to not already be in the mempool.
920+
assert(std::all_of(txns.cbegin(), txns.cend(), [this](const auto& tx)
921+
{ return !m_pool.exists(GenTxid::Txid(tx->GetHash()));}));
922+
919923
std::string err_string;
920924
if (!m_pool.CheckPackageLimits(txns, m_limit_ancestors, m_limit_ancestor_size, m_limit_descendants,
921925
m_limit_descendant_size, err_string)) {
@@ -1238,7 +1242,49 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptPackage(const Package& package,
12381242
m_view.SetBackend(m_dummy);
12391243

12401244
LOCK(m_pool.cs);
1241-
return AcceptMultipleTransactions(package, args);
1245+
std::map<const uint256, const MempoolAcceptResult> results;
1246+
// As node operators are free to set their mempool policies however they please, it's possible
1247+
// for package transaction(s) to already be in the mempool, and we don't want to reject the
1248+
// entire package in that case (as that could be a censorship vector). Filter the transactions
1249+
// that are already in mempool and add their information to results, since we already have them.
1250+
std::vector<CTransactionRef> txns_new;
1251+
for (const auto& tx : package) {
1252+
const auto& wtxid = tx->GetWitnessHash();
1253+
const auto& txid = tx->GetHash();
1254+
// There are 3 possibilities: already in mempool, same-txid-diff-wtxid already in mempool,
1255+
// or not in mempool. An already confirmed tx is treated as one not in mempool, because all
1256+
// we know is that the inputs aren't available.
1257+
if (m_pool.exists(GenTxid::Wtxid(wtxid))) {
1258+
// Exact transaction already exists in the mempool.
1259+
auto iter = m_pool.GetIter(wtxid);
1260+
assert(iter != std::nullopt);
1261+
results.emplace(wtxid, MempoolAcceptResult::MempoolTx(iter.value()->GetTxSize(), iter.value()->GetFee()));
1262+
} else if (m_pool.exists(GenTxid::Txid(txid))) {
1263+
// Transaction with the same non-witness data but different witness (same txid,
1264+
// different wtxid) already exists in the mempool.
1265+
//
1266+
// We don't allow replacement transactions right now, so just swap the package
1267+
// transaction for the mempool one. Note that we are ignoring the validity of the
1268+
// package transaction passed in.
1269+
// TODO: allow witness replacement in packages.
1270+
auto iter = m_pool.GetIter(wtxid);
1271+
assert(iter != std::nullopt);
1272+
results.emplace(txid, MempoolAcceptResult::MempoolTx(iter.value()->GetTxSize(), iter.value()->GetFee()));
1273+
} else {
1274+
// Transaction does not already exist in the mempool.
1275+
txns_new.push_back(tx);
1276+
}
1277+
}
1278+
1279+
// Nothing to do if the entire package has already been submitted.
1280+
if (txns_new.empty()) return PackageMempoolAcceptResult(package_state, std::move(results));
1281+
// Validate the (deduplicated) transactions as a package.
1282+
auto submission_result = AcceptMultipleTransactions(txns_new, args);
1283+
// Include already-in-mempool transaction results in the final result.
1284+
for (const auto& [wtxid, mempoolaccept_res] : results) {
1285+
submission_result.m_tx_results.emplace(wtxid, mempoolaccept_res);
1286+
}
1287+
return submission_result;
12421288
}
12431289

12441290
} // anon namespace

src/validation.h

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,17 +161,19 @@ struct MempoolAcceptResult {
161161
enum class ResultType {
162162
VALID, //!> Fully validated, valid.
163163
INVALID, //!> Invalid.
164+
MEMPOOL_ENTRY, //!> Valid, transaction was already in the mempool.
164165
};
165166
const ResultType m_result_type;
166167
const TxValidationState m_state;
167168

168-
// The following fields are only present when m_result_type = ResultType::VALID
169+
// The following fields are only present when m_result_type = ResultType::VALID or MEMPOOL_ENTRY
169170
/** Mempool transactions replaced by the tx per BIP 125 rules. */
170171
const std::optional<std::list<CTransactionRef>> m_replaced_transactions;
171172
/** Virtual size as used by the mempool, calculated using serialized size and sigops. */
172173
const std::optional<int64_t> m_vsize;
173174
/** Raw base fees in satoshis. */
174175
const std::optional<CAmount> m_base_fees;
176+
175177
static MempoolAcceptResult Failure(TxValidationState state) {
176178
return MempoolAcceptResult(state);
177179
}
@@ -180,6 +182,10 @@ struct MempoolAcceptResult {
180182
return MempoolAcceptResult(std::move(replaced_txns), vsize, fees);
181183
}
182184

185+
static MempoolAcceptResult MempoolTx(int64_t vsize, CAmount fees) {
186+
return MempoolAcceptResult(vsize, fees);
187+
}
188+
183189
// Private constructors. Use static methods MempoolAcceptResult::Success, etc. to construct.
184190
private:
185191
/** Constructor for failure case */
@@ -192,6 +198,10 @@ struct MempoolAcceptResult {
192198
explicit MempoolAcceptResult(std::list<CTransactionRef>&& replaced_txns, int64_t vsize, CAmount fees)
193199
: m_result_type(ResultType::VALID),
194200
m_replaced_transactions(std::move(replaced_txns)), m_vsize{vsize}, m_base_fees(fees) {}
201+
202+
/** Constructor for already-in-mempool case. It wouldn't replace any transactions. */
203+
explicit MempoolAcceptResult(int64_t vsize, CAmount fees)
204+
: m_result_type(ResultType::MEMPOOL_ENTRY), m_vsize{vsize}, m_base_fees(fees) {}
195205
};
196206

197207
/**
@@ -201,7 +211,7 @@ struct PackageMempoolAcceptResult
201211
{
202212
const PackageValidationState m_state;
203213
/**
204-
* Map from wtxid to finished MempoolAcceptResults. The client is responsible
214+
* Map from (w)txid to finished MempoolAcceptResults. The client is responsible
205215
* for keeping track of the transaction objects themselves. If a result is not
206216
* present, it means validation was unfinished for that transaction. If there
207217
* was a package-wide error (see result in m_state), m_tx_results will be empty.

0 commit comments

Comments
 (0)