Skip to content

Commit 87c5c52

Browse files
committed
[p2p] opportunistically accept 1-parent-1-child packages
1 parent 6c51e1d commit 87c5c52

File tree

1 file changed

+196
-2
lines changed

1 file changed

+196
-2
lines changed

src/net_processing.cpp

Lines changed: 196 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,45 @@ class PeerManagerImpl final : public PeerManager
596596
void ProcessValidTx(NodeId nodeid, const CTransactionRef& tx, const std::list<CTransactionRef>& replaced_transactions)
597597
EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, g_msgproc_mutex, cs_main);
598598

599+
/** Handle the results of package validation: calls ProcessValidTx and ProcessInvalidTx for
600+
* individual transactions, and caches rejection for the package as a group.
601+
* @param[in] senders Must contain the nodeids of the peers that provided each transaction
602+
* in package, in the same order.
603+
* */
604+
void ProcessPackageResult(const Package& package, const PackageMempoolAcceptResult& package_result, const std::vector<NodeId>& senders)
605+
EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, g_msgproc_mutex, cs_main);
606+
607+
/** A package to validate */
608+
struct PackageToValidate {
609+
const Package m_txns;
610+
const std::vector<NodeId> m_senders;
611+
/** Construct a 1-parent-1-child package. */
612+
explicit PackageToValidate(const CTransactionRef& parent,
613+
const CTransactionRef& child,
614+
NodeId parent_sender,
615+
NodeId child_sender) :
616+
m_txns{parent, child},
617+
m_senders {parent_sender, child_sender}
618+
{}
619+
620+
std::string ToString() const {
621+
Assume(m_txns.size() == 2);
622+
return strprintf("parent %s (wtxid=%s, sender=%d) + child %s (wtxid=%s, sender=%d)",
623+
m_txns.front()->GetHash().ToString(),
624+
m_txns.front()->GetWitnessHash().ToString(),
625+
m_senders.front(),
626+
m_txns.back()->GetHash().ToString(),
627+
m_txns.back()->GetWitnessHash().ToString(),
628+
m_senders.back());
629+
}
630+
};
631+
632+
/** Look for a child of this transaction in the orphanage to form a 1-parent-1-child package,
633+
* skipping any combinations that have already been tried. Return the resulting package along with
634+
* the senders of its respective transactions, or std::nullopt if no package is found. */
635+
std::optional<PackageToValidate> Find1P1CPackage(const CTransactionRef& ptx, NodeId nodeid)
636+
EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, g_msgproc_mutex, cs_main);
637+
599638
/**
600639
* Reconsider orphan transactions after a parent has been accepted to the mempool.
601640
*
@@ -3198,6 +3237,117 @@ void PeerManagerImpl::ProcessValidTx(NodeId nodeid, const CTransactionRef& tx, c
31983237
}
31993238
}
32003239

3240+
void PeerManagerImpl::ProcessPackageResult(const Package& package, const PackageMempoolAcceptResult& package_result, const std::vector<NodeId>& senders)
3241+
{
3242+
AssertLockNotHeld(m_peer_mutex);
3243+
AssertLockHeld(g_msgproc_mutex);
3244+
AssertLockHeld(cs_main);
3245+
3246+
if (package_result.m_state.IsInvalid()) {
3247+
m_recent_rejects_reconsiderable.insert(GetPackageHash(package));
3248+
}
3249+
// We currently only expect to process 1-parent-1-child packages. Remove if this changes.
3250+
if (!Assume(package.size() == 2)) return;
3251+
3252+
// No package results to look through for PCKG_POLICY or PCKG_MEMPOOL_ERROR
3253+
if (package_result.m_state.GetResult() == PackageValidationResult::PCKG_POLICY ||
3254+
package_result.m_state.GetResult() == PackageValidationResult::PCKG_MEMPOOL_ERROR) return;
3255+
3256+
// Iterate backwards to erase in-package descendants from the orphanage before they become
3257+
// relevant in AddChildrenToWorkSet.
3258+
auto package_iter = package.rbegin();
3259+
auto senders_iter = senders.rbegin();
3260+
while (package_iter != package.rend()) {
3261+
const auto& tx = *package_iter;
3262+
const NodeId nodeid = *senders_iter;
3263+
const auto it_result{package_result.m_tx_results.find(tx->GetWitnessHash())};
3264+
if (Assume(it_result != package_result.m_tx_results.end())) {
3265+
const auto& tx_result = it_result->second;
3266+
switch (tx_result.m_result_type) {
3267+
case MempoolAcceptResult::ResultType::VALID:
3268+
{
3269+
Assume(tx_result.m_replaced_transactions.has_value());
3270+
std::list<CTransactionRef> empty_replacement_list;
3271+
ProcessValidTx(nodeid, tx, tx_result.m_replaced_transactions.value_or(empty_replacement_list));
3272+
break;
3273+
}
3274+
case MempoolAcceptResult::ResultType::INVALID:
3275+
case MempoolAcceptResult::ResultType::DIFFERENT_WITNESS:
3276+
{
3277+
// Don't add to vExtraTxnForCompact, as these transactions should have already been
3278+
// added there when added to the orphanage or rejected for TX_RECONSIDERABLE.
3279+
// This should be updated if package submission is ever used for transactions
3280+
// that haven't already been validated before.
3281+
ProcessInvalidTx(nodeid, tx, tx_result.m_state, /*maybe_add_extra_compact_tx=*/false);
3282+
break;
3283+
}
3284+
case MempoolAcceptResult::ResultType::MEMPOOL_ENTRY:
3285+
{
3286+
// AlreadyHaveTx() should be catching transactions that are already in mempool.
3287+
Assume(false);
3288+
break;
3289+
}
3290+
}
3291+
}
3292+
package_iter++;
3293+
senders_iter++;
3294+
}
3295+
}
3296+
3297+
std::optional<PeerManagerImpl::PackageToValidate> PeerManagerImpl::Find1P1CPackage(const CTransactionRef& ptx, NodeId nodeid)
3298+
{
3299+
AssertLockNotHeld(m_peer_mutex);
3300+
AssertLockHeld(g_msgproc_mutex);
3301+
AssertLockHeld(cs_main);
3302+
3303+
const auto& parent_wtxid{ptx->GetWitnessHash()};
3304+
3305+
Assume(m_recent_rejects_reconsiderable.contains(parent_wtxid.ToUint256()));
3306+
3307+
// Prefer children from this peer. This helps prevent censorship attempts in which an attacker
3308+
// sends lots of fake children for the parent, and we (unluckily) keep selecting the fake
3309+
// children instead of the real one provided by the honest peer.
3310+
const auto cpfp_candidates_same_peer{m_orphanage.GetChildrenFromSamePeer(ptx, nodeid)};
3311+
3312+
// These children should be sorted from newest to oldest. In the (probably uncommon) case
3313+
// of children that replace each other, this helps us accept the highest feerate (probably the
3314+
// most recent) one efficiently.
3315+
for (const auto& child : cpfp_candidates_same_peer) {
3316+
Package maybe_cpfp_package{ptx, child};
3317+
if (!m_recent_rejects_reconsiderable.contains(GetPackageHash(maybe_cpfp_package))) {
3318+
return PeerManagerImpl::PackageToValidate{ptx, child, nodeid, nodeid};
3319+
}
3320+
}
3321+
3322+
// If no suitable candidate from the same peer is found, also try children that were provided by
3323+
// a different peer. This is useful because sometimes multiple peers announce both transactions
3324+
// to us, and we happen to download them from different peers (we wouldn't have known that these
3325+
// 2 transactions are related). We still want to find 1p1c packages then.
3326+
//
3327+
// If we start tracking all announcers of orphans, we can restrict this logic to parent + child
3328+
// pairs in which both were provided by the same peer, i.e. delete this step.
3329+
const auto cpfp_candidates_different_peer{m_orphanage.GetChildrenFromDifferentPeer(ptx, nodeid)};
3330+
3331+
// Find the first 1p1c that hasn't already been rejected. We randomize the order to not
3332+
// create a bias that attackers can use to delay package acceptance.
3333+
//
3334+
// Create a random permutation of the indices.
3335+
std::vector<size_t> tx_indices(cpfp_candidates_different_peer.size());
3336+
std::iota(tx_indices.begin(), tx_indices.end(), 0);
3337+
Shuffle(tx_indices.begin(), tx_indices.end(), m_rng);
3338+
3339+
for (const auto index : tx_indices) {
3340+
// If we already tried a package and failed for any reason, the combined hash was
3341+
// cached in m_recent_rejects_reconsiderable.
3342+
const auto [child_tx, child_sender] = cpfp_candidates_different_peer.at(index);
3343+
Package maybe_cpfp_package{ptx, child_tx};
3344+
if (!m_recent_rejects_reconsiderable.contains(GetPackageHash(maybe_cpfp_package))) {
3345+
return PeerManagerImpl::PackageToValidate{ptx, child_tx, nodeid, child_sender};
3346+
}
3347+
}
3348+
return std::nullopt;
3349+
}
3350+
32013351
bool PeerManagerImpl::ProcessOrphanTx(Peer& peer)
32023352
{
32033353
AssertLockHeld(g_msgproc_mutex);
@@ -4377,6 +4527,20 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
43774527
RelayTransaction(tx.GetHash(), tx.GetWitnessHash());
43784528
}
43794529
}
4530+
4531+
if (m_recent_rejects_reconsiderable.contains(wtxid)) {
4532+
// When a transaction is already in m_recent_rejects_reconsiderable, we shouldn't submit
4533+
// it by itself again. However, look for a matching child in the orphanage, as it is
4534+
// possible that they succeed as a package.
4535+
LogPrint(BCLog::TXPACKAGES, "found tx %s (wtxid=%s) in reconsiderable rejects, looking for child in orphanage\n",
4536+
txid.ToString(), wtxid.ToString());
4537+
if (auto package_to_validate{Find1P1CPackage(ptx, pfrom.GetId())}) {
4538+
const auto package_result{ProcessNewPackage(m_chainman.ActiveChainstate(), m_mempool, package_to_validate->m_txns, /*test_accept=*/false, /*client_maxfeerate=*/std::nullopt)};
4539+
LogDebug(BCLog::TXPACKAGES, "package evaluation for %s: %s\n", package_to_validate->ToString(),
4540+
package_result.m_state.IsValid() ? "package accepted" : "package rejected");
4541+
ProcessPackageResult(package_to_validate->m_txns, package_result, package_to_validate->m_senders);
4542+
}
4543+
}
43804544
// If a tx is detected by m_recent_rejects it is ignored. Because we haven't
43814545
// submitted the tx to our mempool, we won't have computed a DoS
43824546
// score for it or determined exactly why we consider it invalid.
@@ -4418,10 +4582,23 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
44184582
}
44194583
std::sort(unique_parents.begin(), unique_parents.end());
44204584
unique_parents.erase(std::unique(unique_parents.begin(), unique_parents.end()), unique_parents.end());
4585+
4586+
// Distinguish between parents in m_recent_rejects and m_recent_rejects_reconsiderable.
4587+
// We can tolerate having up to 1 parent in m_recent_rejects_reconsiderable since we
4588+
// submit 1p1c packages. However, fail immediately if any are in m_recent_rejects.
4589+
std::optional<uint256> rejected_parent_reconsiderable;
44214590
for (const uint256& parent_txid : unique_parents) {
44224591
if (m_recent_rejects.contains(parent_txid)) {
44234592
fRejectedParents = true;
44244593
break;
4594+
} else if (m_recent_rejects_reconsiderable.contains(parent_txid) && !m_mempool.exists(GenTxid::Txid(parent_txid))) {
4595+
// More than 1 parent in m_recent_rejects_reconsiderable: 1p1c will not be
4596+
// sufficient to accept this package, so just give up here.
4597+
if (rejected_parent_reconsiderable.has_value()) {
4598+
fRejectedParents = true;
4599+
break;
4600+
}
4601+
rejected_parent_reconsiderable = parent_txid;
44254602
}
44264603
}
44274604
if (!fRejectedParents) {
@@ -4435,7 +4612,9 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
44354612
// protocol for getting all unconfirmed parents.
44364613
const auto gtxid{GenTxid::Txid(parent_txid)};
44374614
AddKnownTx(*peer, parent_txid);
4438-
if (!AlreadyHaveTx(gtxid, /*include_reconsiderable=*/true)) AddTxAnnouncement(pfrom, gtxid, current_time);
4615+
// Exclude m_recent_rejects_reconsiderable: the missing parent may have been
4616+
// previously rejected for being too low feerate. This orphan might CPFP it.
4617+
if (!AlreadyHaveTx(gtxid, /*include_reconsiderable=*/false)) AddTxAnnouncement(pfrom, gtxid, current_time);
44394618
}
44404619

44414620
if (m_orphanage.AddTx(ptx, pfrom.GetId())) {
@@ -4467,6 +4646,19 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
44674646
if (state.IsInvalid()) {
44684647
ProcessInvalidTx(pfrom.GetId(), ptx, state, /*maybe_add_extra_compact_tx=*/true);
44694648
}
4649+
// When a transaction fails for TX_RECONSIDERABLE, look for a matching child in the
4650+
// orphanage, as it is possible that they succeed as a package.
4651+
if (state.GetResult() == TxValidationResult::TX_RECONSIDERABLE) {
4652+
LogPrint(BCLog::TXPACKAGES, "tx %s (wtxid=%s) failed but reconsiderable, looking for child in orphanage\n",
4653+
txid.ToString(), wtxid.ToString());
4654+
if (auto package_to_validate{Find1P1CPackage(ptx, pfrom.GetId())}) {
4655+
const auto package_result{ProcessNewPackage(m_chainman.ActiveChainstate(), m_mempool, package_to_validate->m_txns, /*test_accept=*/false, /*client_maxfeerate=*/std::nullopt)};
4656+
LogDebug(BCLog::TXPACKAGES, "package evaluation for %s: %s\n", package_to_validate->ToString(),
4657+
package_result.m_state.IsValid() ? "package accepted" : "package rejected");
4658+
ProcessPackageResult(package_to_validate->m_txns, package_result, package_to_validate->m_senders);
4659+
}
4660+
}
4661+
44704662
return;
44714663
}
44724664

@@ -6076,7 +6268,9 @@ bool PeerManagerImpl::SendMessages(CNode* pto)
60766268
entry.second.GetHash().ToString(), entry.first);
60776269
}
60786270
for (const GenTxid& gtxid : requestable) {
6079-
if (!AlreadyHaveTx(gtxid, /*include_reconsiderable=*/true)) {
6271+
// Exclude m_recent_rejects_reconsiderable: we may be requesting a missing parent
6272+
// that was previously rejected for being too low feerate.
6273+
if (!AlreadyHaveTx(gtxid, /*include_reconsiderable=*/false)) {
60806274
LogPrint(BCLog::NET, "Requesting %s %s peer=%d\n", gtxid.IsWtxid() ? "wtx" : "tx",
60816275
gtxid.GetHash().ToString(), pto->GetId());
60826276
vGetData.emplace_back(gtxid.IsWtxid() ? MSG_WTX : (MSG_TX | GetFetchFlags(*peer)), gtxid.GetHash());

0 commit comments

Comments
 (0)