Skip to content

Commit 86d7135

Browse files
committed
[p2p] only attempt 1p1c when both txns provided by the same peer
Now that we track all announcers of an orphan, it's not helpful to consider an orphan provided by a peer that didn't send us this parent. It can only hurt our chances of finding the right orphan when there are multiple candidates. Adapt the 2 tests in p2p_opportunistic_1p1c.py that looked at 1p1c packages from different peers. Instead of checking that the right peer is punished, we now check that the package is not submitted. We can't use the functional test to see that the package was not considered because the behavior is indistinguishable (except for the logs).
1 parent f7658d9 commit 86d7135

File tree

6 files changed

+14
-101
lines changed

6 files changed

+14
-101
lines changed

src/node/txdownloadman_impl.cpp

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,11 @@ std::optional<PackageToValidate> TxDownloadManagerImpl::Find1P1CPackage(const CT
300300

301301
Assume(RecentRejectsReconsiderableFilter().contains(parent_wtxid.ToUint256()));
302302

303-
// Prefer children from this peer. This helps prevent censorship attempts in which an attacker
303+
// Only consider children from this peer. This helps prevent censorship attempts in which an attacker
304304
// sends lots of fake children for the parent, and we (unluckily) keep selecting the fake
305-
// children instead of the real one provided by the honest peer.
305+
// children instead of the real one provided by the honest peer. Since we track all announcers
306+
// of an orphan, this does not exclude parent + orphan pairs that we happened to request from
307+
// different peers.
306308
const auto cpfp_candidates_same_peer{m_orphanage.GetChildrenFromSamePeer(ptx, nodeid)};
307309

308310
// These children should be sorted from newest to oldest. In the (probably uncommon) case
@@ -315,34 +317,6 @@ std::optional<PackageToValidate> TxDownloadManagerImpl::Find1P1CPackage(const CT
315317
return PackageToValidate{ptx, child, nodeid, nodeid};
316318
}
317319
}
318-
319-
// If no suitable candidate from the same peer is found, also try children that were provided by
320-
// a different peer. This is useful because sometimes multiple peers announce both transactions
321-
// to us, and we happen to download them from different peers (we wouldn't have known that these
322-
// 2 transactions are related). We still want to find 1p1c packages then.
323-
//
324-
// If we start tracking all announcers of orphans, we can restrict this logic to parent + child
325-
// pairs in which both were provided by the same peer, i.e. delete this step.
326-
const auto cpfp_candidates_different_peer{m_orphanage.GetChildrenFromDifferentPeer(ptx, nodeid)};
327-
328-
// Find the first 1p1c that hasn't already been rejected. We randomize the order to not
329-
// create a bias that attackers can use to delay package acceptance.
330-
//
331-
// Create a random permutation of the indices.
332-
std::vector<size_t> tx_indices(cpfp_candidates_different_peer.size());
333-
std::iota(tx_indices.begin(), tx_indices.end(), 0);
334-
std::shuffle(tx_indices.begin(), tx_indices.end(), m_opts.m_rng);
335-
336-
for (const auto index : tx_indices) {
337-
// If we already tried a package and failed for any reason, the combined hash was
338-
// cached in m_lazy_recent_rejects_reconsiderable.
339-
const auto [child_tx, child_sender] = cpfp_candidates_different_peer.at(index);
340-
Package maybe_cpfp_package{ptx, child_tx};
341-
if (!RecentRejectsReconsiderableFilter().contains(GetPackageHash(maybe_cpfp_package)) &&
342-
!RecentRejectsFilter().contains(child_tx->GetHash().ToUint256())) {
343-
return PackageToValidate{ptx, child_tx, nodeid, child_sender};
344-
}
345-
}
346320
return std::nullopt;
347321
}
348322

src/test/fuzz/txorphan.cpp

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,6 @@ FUZZ_TARGET(txorphan, .init = initialize_orphanage)
8888
return input.prevout.hash == ptx_potential_parent->GetHash();
8989
}));
9090
}
91-
for (const auto& [child, peer] : orphanage.GetChildrenFromDifferentPeer(ptx_potential_parent, peer_id)) {
92-
assert(std::any_of(child->vin.cbegin(), child->vin.cend(), [&](const auto& input) {
93-
return input.prevout.hash == ptx_potential_parent->GetHash();
94-
}));
95-
assert(peer != peer_id);
96-
}
9791
}
9892

9993
// trigger orphanage functions

src/test/orphanage_tests.cpp

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,6 @@ static bool EqualTxns(const std::set<CTransactionRef>& set_txns, const std::vect
9393
}
9494
return true;
9595
}
96-
static bool EqualTxns(const std::set<CTransactionRef>& set_txns,
97-
const std::vector<std::pair<CTransactionRef, NodeId>>& vec_txns)
98-
{
99-
if (vec_txns.size() != set_txns.size()) return false;
100-
for (const auto& [tx, nodeid] : vec_txns) {
101-
if (!set_txns.contains(tx)) return false;
102-
}
103-
return true;
104-
}
10596

10697
BOOST_AUTO_TEST_CASE(DoS_mapOrphans)
10798
{
@@ -312,18 +303,13 @@ BOOST_AUTO_TEST_CASE(get_children)
312303
BOOST_CHECK(EqualTxns(expected_parent1_children, orphanage.GetChildrenFromSamePeer(parent1, node1)));
313304
BOOST_CHECK(EqualTxns(expected_parent2_children, orphanage.GetChildrenFromSamePeer(parent2, node1)));
314305

315-
BOOST_CHECK(EqualTxns(expected_parent1_children, orphanage.GetChildrenFromDifferentPeer(parent1, node2)));
316-
BOOST_CHECK(EqualTxns(expected_parent2_children, orphanage.GetChildrenFromDifferentPeer(parent2, node2)));
317-
318306
// The peer must match
319307
BOOST_CHECK(orphanage.GetChildrenFromSamePeer(parent1, node2).empty());
320308
BOOST_CHECK(orphanage.GetChildrenFromSamePeer(parent2, node2).empty());
321309

322310
// There shouldn't be any children of this tx in the orphanage
323311
BOOST_CHECK(orphanage.GetChildrenFromSamePeer(child_p1n0_p2n0, node1).empty());
324312
BOOST_CHECK(orphanage.GetChildrenFromSamePeer(child_p1n0_p2n0, node2).empty());
325-
BOOST_CHECK(orphanage.GetChildrenFromDifferentPeer(child_p1n0_p2n0, node1).empty());
326-
BOOST_CHECK(orphanage.GetChildrenFromDifferentPeer(child_p1n0_p2n0, node2).empty());
327313
}
328314

329315
// Orphans provided by node1 and node2
@@ -346,31 +332,27 @@ BOOST_AUTO_TEST_CASE(get_children)
346332
std::set<CTransactionRef> expected_parent1_node1{child_p1n0};
347333

348334
BOOST_CHECK(EqualTxns(expected_parent1_node1, orphanage.GetChildrenFromSamePeer(parent1, node1)));
349-
BOOST_CHECK(EqualTxns(expected_parent1_node1, orphanage.GetChildrenFromDifferentPeer(parent1, node2)));
350335
}
351336

352337
// Children of parent2 from node1:
353338
{
354339
std::set<CTransactionRef> expected_parent2_node1{child_p2n1};
355340

356341
BOOST_CHECK(EqualTxns(expected_parent2_node1, orphanage.GetChildrenFromSamePeer(parent2, node1)));
357-
BOOST_CHECK(EqualTxns(expected_parent2_node1, orphanage.GetChildrenFromDifferentPeer(parent2, node2)));
358342
}
359343

360344
// Children of parent1 from node2:
361345
{
362346
std::set<CTransactionRef> expected_parent1_node2{child_p1n0_p1n1, child_p1n0_p2n0};
363347

364348
BOOST_CHECK(EqualTxns(expected_parent1_node2, orphanage.GetChildrenFromSamePeer(parent1, node2)));
365-
BOOST_CHECK(EqualTxns(expected_parent1_node2, orphanage.GetChildrenFromDifferentPeer(parent1, node1)));
366349
}
367350

368351
// Children of parent2 from node2:
369352
{
370353
std::set<CTransactionRef> expected_parent2_node2{child_p1n0_p2n0};
371354

372355
BOOST_CHECK(EqualTxns(expected_parent2_node2, orphanage.GetChildrenFromSamePeer(parent2, node2)));
373-
BOOST_CHECK(EqualTxns(expected_parent2_node2, orphanage.GetChildrenFromDifferentPeer(parent2, node1)));
374356
}
375357
}
376358
}

src/txorphanage.cpp

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -287,38 +287,6 @@ std::vector<CTransactionRef> TxOrphanage::GetChildrenFromSamePeer(const CTransac
287287
return children_found;
288288
}
289289

290-
std::vector<std::pair<CTransactionRef, NodeId>> TxOrphanage::GetChildrenFromDifferentPeer(const CTransactionRef& parent, NodeId nodeid) const
291-
{
292-
// First construct vector of iterators to ensure we do not return duplicates of the same tx.
293-
std::vector<OrphanMap::iterator> iters;
294-
295-
// For each output, get all entries spending this prevout, filtering for ones not from the specified peer.
296-
for (unsigned int i = 0; i < parent->vout.size(); i++) {
297-
const auto it_by_prev = m_outpoint_to_orphan_it.find(COutPoint(parent->GetHash(), i));
298-
if (it_by_prev != m_outpoint_to_orphan_it.end()) {
299-
for (const auto& elem : it_by_prev->second) {
300-
if (!elem->second.announcers.contains(nodeid)) {
301-
iters.emplace_back(elem);
302-
}
303-
}
304-
}
305-
}
306-
307-
// Erase duplicates
308-
std::sort(iters.begin(), iters.end(), IteratorComparator());
309-
iters.erase(std::unique(iters.begin(), iters.end()), iters.end());
310-
311-
// Convert iterators to pair<CTransactionRef, NodeId>
312-
std::vector<std::pair<CTransactionRef, NodeId>> children_found;
313-
children_found.reserve(iters.size());
314-
for (const auto& child_iter : iters) {
315-
// Use first peer in announcers list
316-
auto peer = *(child_iter->second.announcers.begin());
317-
children_found.emplace_back(child_iter->second.tx, peer);
318-
}
319-
return children_found;
320-
}
321-
322290
std::vector<TxOrphanage::OrphanTxBase> TxOrphanage::GetOrphanTransactions() const
323291
{
324292
std::vector<OrphanTxBase> ret;

src/txorphanage.h

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,6 @@ class TxOrphanage {
7171
* recent to least recent. */
7272
std::vector<CTransactionRef> GetChildrenFromSamePeer(const CTransactionRef& parent, NodeId nodeid) const;
7373

74-
/** Get all children that spend from this tx but were not received from nodeid. Also return
75-
* which peer provided each tx. */
76-
std::vector<std::pair<CTransactionRef, NodeId>> GetChildrenFromDifferentPeer(const CTransactionRef& parent, NodeId nodeid) const;
77-
7874
/** Return how many entries exist in the orphange */
7975
size_t Size() const
8076
{

test/functional/p2p_opportunistic_1p1c.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ def test_low_and_high_child(self, wallet):
215215

216216
@cleanup
217217
def test_orphan_consensus_failure(self):
218-
self.log.info("Check opportunistic 1p1c logic with consensus-invalid orphan causes disconnect of the correct peer")
218+
self.log.info("Check opportunistic 1p1c logic requires parent and child to be from the same peer")
219219
node = self.nodes[0]
220220
low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet)
221221
coin = low_fee_parent["new_utxo"]
@@ -239,15 +239,17 @@ def test_orphan_consensus_failure(self):
239239
parent_txid_int = int(low_fee_parent["txid"], 16)
240240
bad_orphan_sender.wait_for_getdata([parent_txid_int])
241241

242-
# 3. A different peer relays the parent. Parent+Child are evaluated as a package and rejected.
243-
parent_sender.send_message(msg_tx(low_fee_parent["tx"]))
242+
# 3. A different peer relays the parent. Package is not evaluated because the transactions
243+
# were not sent from the same peer.
244+
parent_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
244245

245246
# 4. Transactions should not be in mempool.
246247
node_mempool = node.getrawmempool()
247248
assert low_fee_parent["txid"] not in node_mempool
248249
assert tx_orphan_bad_wit.rehash() not in node_mempool
249250

250-
# 5. Peer that sent a consensus-invalid transaction should be disconnected.
251+
# 5. Have the other peer send the tx too, so that tx_orphan_bad_wit package is attempted.
252+
bad_orphan_sender.send_message(msg_tx(low_fee_parent["tx"]))
251253
bad_orphan_sender.wait_for_disconnect()
252254

253255
# The peer that didn't provide the orphan should not be disconnected.
@@ -279,20 +281,17 @@ def test_parent_consensus_failure(self):
279281
package_sender.wait_for_getdata([parent_txid_int])
280282

281283
# 3. A different node relays the parent. The parent is first evaluated by itself and
282-
# rejected for being too low feerate. Then it is evaluated as a package and, after passing
283-
# feerate checks, rejected for having a bad signature (consensus error).
284-
fake_parent_sender.send_message(msg_tx(tx_parent_bad_wit))
284+
# rejected for being too low feerate. It is not evaluated as a package because the child was
285+
# sent from a different peer, so we don't find out that the child is consensus-invalid.
286+
fake_parent_sender.send_and_ping(msg_tx(tx_parent_bad_wit))
285287

286288
# 4. Transactions should not be in mempool.
287289
node_mempool = node.getrawmempool()
288290
assert tx_parent_bad_wit.rehash() not in node_mempool
289291
assert high_fee_child["txid"] not in node_mempool
290292

291-
# 5. Peer sent a consensus-invalid transaction.
292-
fake_parent_sender.wait_for_disconnect()
293-
294293
self.log.info("Check that fake parent does not cause orphan to be deleted and real package can still be submitted")
295-
# 6. Child-sending should not have been punished and the orphan should remain in orphanage.
294+
# 5. Child-sending should not have been punished and the orphan should remain in orphanage.
296295
# It can send the "real" parent transaction, and the package is accepted.
297296
parent_wtxid_int = int(low_fee_parent["tx"].getwtxid(), 16)
298297
package_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))

0 commit comments

Comments
 (0)