Skip to content

Commit a711269

Browse files
committed
Treat SPK conflicts the same as RBF-optin TxIn conflicts (except never DoS ban)
1 parent 873972f commit a711269

File tree

4 files changed

+54
-24
lines changed

4 files changed

+54
-24
lines changed

src/policy/rbf.cpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,17 @@ std::optional<std::string> HasNoNewUnconfirmed(const CTransaction& tx,
119119
}
120120

121121
std::optional<std::string> EntriesAndTxidsDisjoint(const CTxMemPool::setEntries& ancestors,
122-
const std::set<Txid>& direct_conflicts,
123-
const uint256& txid)
122+
const std::map<Txid, bool>& direct_conflicts,
123+
const uint256& txid, bool* const out_violates_policy)
124124
{
125125
for (CTxMemPool::txiter ancestorIt : ancestors) {
126126
const Txid& hashAncestor = ancestorIt->GetTx().GetHash();
127-
if (direct_conflicts.count(hashAncestor)) {
127+
const auto& conflictit = direct_conflicts.find(hashAncestor);
128+
if (conflictit != direct_conflicts.end()) {
129+
if (!conflictit->second /* mere SPK conflict, NOT invalid */) {
130+
if (out_violates_policy) *out_violates_policy = true;
131+
continue;
132+
}
128133
return strprintf("%s spends conflicting transaction %s",
129134
txid.ToString(),
130135
hashAncestor.ToString());

src/policy/rbf.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,12 @@ std::optional<std::string> HasNoNewUnconfirmed(const CTransaction& tx, const CTx
8888
* @param[in] direct_conflicts Set of txids corresponding to the mempool conflicts
8989
* (candidates to be replaced).
9090
* @param[in] txid Transaction ID, included in the error message if violation occurs.
91-
* @returns error message if the sets intersect, std::nullopt if they are disjoint.
91+
* @param[out] out_violates_policy Assigned to true if there are any policy-only conflicts.
92+
* @returns error message if the sets intersect (consensus-only conflicts), std::nullopt if they are disjoint or only intersect on policy matters.
9293
*/
9394
std::optional<std::string> EntriesAndTxidsDisjoint(const CTxMemPool::setEntries& ancestors,
94-
const std::set<Txid>& direct_conflicts,
95-
const uint256& txid);
95+
const std::map<Txid, bool>& direct_conflicts,
96+
const uint256& txid, bool* out_violates_policy);
9697

9798
/** Check that the feerate of the replacement transaction(s) is higher than the feerate of each
9899
* of the transactions in iters_conflicting.

src/test/rbf_tests.cpp

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -216,14 +216,22 @@ BOOST_FIXTURE_TEST_CASE(rbf_helper_functions, TestChain100Setup)
216216
BOOST_CHECK(PaysMoreThanConflicts(set_34_cpfp, CFeeRate(entry4_high->GetModifiedFee(), entry4_high->GetTxSize()), unused_txid).has_value());
217217

218218
// Tests for EntriesAndTxidsDisjoint
219-
BOOST_CHECK(EntriesAndTxidsDisjoint(empty_set, {tx1->GetHash()}, unused_txid) == std::nullopt);
220-
BOOST_CHECK(EntriesAndTxidsDisjoint(set_12_normal, {tx3->GetHash()}, unused_txid) == std::nullopt);
221-
BOOST_CHECK(EntriesAndTxidsDisjoint({entry2_normal}, {tx2->GetHash()}, unused_txid).has_value());
222-
BOOST_CHECK(EntriesAndTxidsDisjoint(set_12_normal, {tx1->GetHash()}, unused_txid).has_value());
223-
BOOST_CHECK(EntriesAndTxidsDisjoint(set_12_normal, {tx2->GetHash()}, unused_txid).has_value());
219+
bool violates_policy{false};
220+
BOOST_CHECK(EntriesAndTxidsDisjoint(empty_set, {{tx1->GetHash(), true}}, unused_txid, &violates_policy) == std::nullopt);
221+
BOOST_CHECK(!violates_policy);
222+
BOOST_CHECK(EntriesAndTxidsDisjoint(set_12_normal, {{tx3->GetHash(), true}}, unused_txid, &violates_policy) == std::nullopt);
223+
BOOST_CHECK(!violates_policy);
224+
BOOST_CHECK(EntriesAndTxidsDisjoint({entry2_normal}, {{tx2->GetHash(), true}}, unused_txid, &violates_policy).has_value());
225+
BOOST_CHECK(!violates_policy);
226+
BOOST_CHECK(EntriesAndTxidsDisjoint(set_12_normal, {{tx1->GetHash(), true}}, unused_txid, &violates_policy).has_value());
227+
BOOST_CHECK(!violates_policy);
228+
BOOST_CHECK(EntriesAndTxidsDisjoint(set_12_normal, {{tx2->GetHash(), true}}, unused_txid, &violates_policy).has_value());
229+
BOOST_CHECK(!violates_policy);
224230
// EntriesAndTxidsDisjoint does not calculate descendants of iters_conflicting; it uses whatever
225231
// the caller passed in. As such, no error is returned even though entry2_normal is a descendant of tx1.
226-
BOOST_CHECK(EntriesAndTxidsDisjoint({entry2_normal}, {tx1->GetHash()}, unused_txid) == std::nullopt);
232+
BOOST_CHECK(EntriesAndTxidsDisjoint({entry2_normal}, {{tx1->GetHash(), true}}, unused_txid, &violates_policy) == std::nullopt);
233+
BOOST_CHECK(!violates_policy);
234+
// TODO: Add tests for policy-only conflicts
227235

228236
// Tests for PaysForRBF
229237
const CFeeRate incremental_relay_feerate{DEFAULT_INCREMENTAL_RELAY_FEE};

src/validation.cpp

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,8 @@ class MemPoolAccept
630630
explicit Workspace(const CTransactionRef& ptx) : m_ptx(ptx), m_hash(ptx->GetHash()) {}
631631
/** Txids of mempool transactions that this transaction directly conflicts with or may
632632
* replace via sibling eviction. */
633-
std::set<Txid> m_conflicts;
633+
/** .second=true is a consensus conflict, and .second=false is a policy conflict. */
634+
std::map<Txid, bool> m_conflicts_incl_policy;
634635
/** Iterators to mempool entries that this transaction directly conflicts with or may
635636
* replace via sibling eviction. */
636637
CTxMemPool::setEntries m_iters_conflicting;
@@ -852,7 +853,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
852853
// Transaction conflicts with a mempool tx, but we're not allowing replacements.
853854
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "bip125-replacement-disallowed");
854855
}
855-
if (!ws.m_conflicts.count(ptxConflicting->GetHash()))
856+
if (!ws.m_conflicts_incl_policy.count(ptxConflicting->GetHash()))
856857
{
857858
// Transactions that don't explicitly signal replaceability are
858859
// *not* replaceable with the current logic, even if one of their
@@ -878,16 +879,22 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
878879
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "txn-mempool-conflict");
879880
}
880881

881-
ws.m_conflicts.insert(ptxConflicting->GetHash());
882+
ws.m_conflicts_incl_policy.emplace(ptxConflicting->GetHash(), true);
882883
}
883884
}
884885
}
885886

886887
if (spk_reuse_mode != SRM_ALLOW) {
887888
for (const CTxOut& txout : tx.vout) {
888889
uint160 hashSPK = ScriptHashkey(txout.scriptPubKey);
889-
if (m_pool.mapUsedSPK.find(hashSPK) != m_pool.mapUsedSPK.end()) {
890-
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "txn-mempool-spk-reused");
890+
const auto& SPKUsedIn = m_pool.mapUsedSPK.find(hashSPK);
891+
if (SPKUsedIn != m_pool.mapUsedSPK.end()) {
892+
if (SPKUsedIn->second.first) {
893+
ws.m_conflicts_incl_policy.emplace(SPKUsedIn->second.first->GetHash(), false);
894+
}
895+
if (SPKUsedIn->second.second) {
896+
ws.m_conflicts_incl_policy.emplace(SPKUsedIn->second.second->GetHash(), false);
897+
}
891898
}
892899
if (mapSPK.find(hashSPK) != mapSPK.end()) {
893900
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "txn-spk-reused-twinoutputs");
@@ -962,7 +969,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
962969
const auto& SPKit = m_pool.mapUsedSPK.find(hashSPK);
963970
if (SPKit != m_pool.mapUsedSPK.end()) {
964971
if (SPKit->second.second /* Spent */) {
965-
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "txn-mempool-spk-reused-spend");
972+
ws.m_conflicts_incl_policy.emplace(SPKit->second.second->GetHash(), false);
966973
}
967974
}
968975
mapSPK[hashSPK] = MemPool_SPK_State(mapSPK[hashSPK] | MSS_SPENT);
@@ -1024,14 +1031,18 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
10241031
// feerate later.
10251032
if (!args.m_package_feerates && !CheckFeeRate(ws.m_vsize, ws.m_modified_fees, state, args.m_ignore_rejects)) return false;
10261033

1027-
ws.m_iters_conflicting = m_pool.GetIterSet(ws.m_conflicts);
1034+
std::set<Txid> conflicts_as_a_set;
1035+
std::transform(ws.m_conflicts_incl_policy.begin(), ws.m_conflicts_incl_policy.end(),
1036+
std::inserter(conflicts_as_a_set, conflicts_as_a_set.end()),
1037+
[](const std::pair<Txid, bool>& pair){ return pair.first; });
1038+
ws.m_iters_conflicting = m_pool.GetIterSet(conflicts_as_a_set);
10281039

10291040
// Note that these modifications are only applicable to single transaction scenarios;
10301041
// carve-outs are disabled for multi-transaction evaluations.
10311042
CTxMemPool::Limits maybe_rbf_limits = m_pool.m_opts.limits;
10321043

10331044
// Calculate in-mempool ancestors, up to a limit.
1034-
if (ws.m_conflicts.size() == 1 && args.m_allow_carveouts) {
1045+
if (ws.m_conflicts_incl_policy.size() == 1 && args.m_allow_carveouts) {
10351046
// In general, when we receive an RBF transaction with mempool conflicts, we want to know whether we
10361047
// would meet the chain limits after the conflicts have been removed. However, there isn't a practical
10371048
// way to do this short of calculating the ancestor and descendant sets with an overlay cache of
@@ -1115,15 +1126,16 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
11151126
// check using the full ancestor set here because it's more convenient to use what we have
11161127
// already calculated.
11171128
if (m_pool.m_opts.truc_policy == TRUCPolicy::Enforce) {
1118-
if (const auto err{SingleTRUCChecks(ws.m_ptx, "truc-", reason, ignore_rejects, ws.m_ancestors, ws.m_conflicts, ws.m_vsize)}) {
1129+
if (const auto err{SingleTRUCChecks(ws.m_ptx, "truc-", reason, ignore_rejects, ws.m_ancestors, conflicts_as_a_set, ws.m_vsize)}) {
11191130
// Single transaction contexts only.
11201131
if (args.m_allow_sibling_eviction && err->second != nullptr) {
11211132
// We should only be considering where replacement is considered valid as well.
11221133
Assume(args.m_allow_replacement);
11231134

11241135
// Potential sibling eviction. Add the sibling to our list of mempool conflicts to be
11251136
// included in RBF checks.
1126-
ws.m_conflicts.insert(err->second->GetHash());
1137+
ws.m_conflicts_incl_policy.emplace(err->second->GetHash(), false);
1138+
conflicts_as_a_set.insert(err->second->GetHash());
11271139
// Adding the sibling to m_iters_conflicting here means that it doesn't count towards
11281140
// RBF Carve Out above. This is correct, since removing to-be-replaced transactions from
11291141
// the descendant count is done separately in SingleTRUCChecks for TRUC transactions.
@@ -1142,14 +1154,18 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
11421154
// that we have the set of all ancestors we can detect this
11431155
// pathological case by making sure ws.m_conflicts and ws.m_ancestors don't
11441156
// intersect.
1145-
if (const auto err_string{EntriesAndTxidsDisjoint(ws.m_ancestors, ws.m_conflicts, hash)}) {
1157+
bool has_policy_conflict{false};
1158+
if (const auto err_string{EntriesAndTxidsDisjoint(ws.m_ancestors, ws.m_conflicts_incl_policy, hash, &has_policy_conflict)}) {
11461159
// We classify this as a consensus error because a transaction depending on something it
11471160
// conflicts with would be inconsistent.
11481161
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-spends-conflicting-tx", *err_string);
11491162
}
1163+
if (has_policy_conflict) {
1164+
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "txn-spk-reused-chained");
1165+
}
11501166

11511167
// We want to detect conflicts in any tx in a package to trigger package RBF logic
1152-
m_subpackage.m_rbf |= !ws.m_conflicts.empty();
1168+
m_subpackage.m_rbf |= !ws.m_conflicts_incl_policy.empty();
11531169
return true;
11541170
}
11551171

0 commit comments

Comments
 (0)