Followup to #31397.
Addressing (in order): #31397 (review) #31397 (review) #31397 (review) #31397 (review) #31397 (review) #31397 (review) #31397 (review) #31658#pullrequestreview-2551617694 #31397 (review)
Followup to #31397.
Addressing (in order): #31397 (review) #31397 (review) #31397 (review) #31397 (review) #31397 (review) #31397 (review) #31397 (review) #31658#pullrequestreview-2551617694 #31397 (review)
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/31666.
See the guideline for information on the review process.
Type | Reviewers |
---|---|
ACK | instagibbs, marcofleon, mzumsande, dergoegge |
If your review is incorrectly listed, please react with π to this comment and the bot will ignore it on the next update.
No conflicts as of last run.
193@@ -194,6 +194,12 @@ class TxDownloadManagerImpl {
194 /** Helper for getting deduplicated vector of Txids in vin. */
195 std::vector<Txid> GetUniqueParents(const CTransaction& tx);
196
197+ /** If a transaction is an orphan resolution candidate, treat the unique_parents as announced by this peer; add them
195@@ -196,7 +196,7 @@ class TxRequestTracker {
196 size_t Size() const;
197
198 /** For some tx return all peers with non-COMPLETED announcements for its txid or wtxid. The resulting vector may contain duplicate NodeIds. */
199- std::vector<NodeId> GetCandidatePeers(const CTransactionRef& tx) const;
200+ void GetCandidatePeers(const uint256& txhash, std::vector<NodeId>& result_peers) const;
Nice improvements in txdownloadman_impl
, definitely easier to read.
code review ACK 47f269b3375288c53a02bdb894de83cfa39c81c4
294@@ -295,6 +295,12 @@ class Tester
295 tracked += m_announcements[txhash][peer].m_state != State::NOTHING;
296 inflight += m_announcements[txhash][peer].m_state == State::REQUESTED;
297 candidates += m_announcements[txhash][peer].m_state == State::CANDIDATE;
298+
299+ std::vector<NodeId> candidate_peers;
300+ m_tracker.GetCandidatePeers(TXHASHES[txhash], candidate_peers);
301+ for (const auto& peer : candidate_peers) {
In commit “[fuzz] GetCandidatePeers”
This tests that all values returned by GetCandidatePeers
are in the set of expected results, but not that nothing is missing from the GetCandidatePeers
result (so if GetCandidatePeers()
never modified candidate_peers
, no issue would be detected).
candidate_peers
matches m_announcements[txhash]
.
171@@ -172,6 +172,22 @@ void TxDownloadManagerImpl::DisconnectedPeer(NodeId nodeid)
172
173 }
174
175+bool TxDownloadManagerImpl::AddOrphanParentAnnouncements(const std::vector<Txid>& unique_parents, const Wtxid& wtxid, NodeId nodeid, std::chrono::microseconds now)
176+{
177+ if (auto delay{OrphanResolutionCandidate(nodeid, wtxid, unique_parents.size())}) {
178+ const auto& info = m_peer_info.at(nodeid).m_connection_info;
179+
180+ // Treat finding orphan resolution candidate as equivalent to the peer announcing all missing parents
In commit “[refactor] helper function for adding orphan parents to txrequest”.
Β΅nit: period at the end of a sentence?
170- LogDebug(BCLog::TXPACKAGES, "added %s (wtxid=%s) to peer %d workset\n",
171- tx.GetHash().ToString(), tx.GetWitnessHash().ToString(), announcer);
172- }
173+
174+ // Select a random peer to assign orphan processing, reducing wasted work if the orphan is still missing
175+ // inputs. However, we don't want to create an issue in which the assigned peer can purposefully stop us
In commit “[p2p] assign just 1 random announcer in AddChildrenToWorkSet”
How is this prevented? Just by virtue of picking a random one, so it’s unlikely to be an attacker?
The attack scenario here would be:
without this PR’s change, we may be doing the re-evaluation work up to num_inbound times erroneously?
Also, making sure, are there any ways 1P1C are affected by the choice either way?
without this PR’s change, we may be doing the re-evaluation work up to num_inbound times erroneously?
yes - I suggested this because it seemed a bit wasteful in the honest case, but it is probably no big deal, because the actual full validation will only be done once and checking inputs repeatedly shouldn’t be too much additional work.
On the other hand, I think that this attack would be really hard to pull off timing-wise (you would need to guess when other peers do orphan resolution, and even if you get that right you need to disconnect exactly at the right moment) so I’m not sure if it is practical.
I wonder if a good “mitigation” (if we’re worried) is @mzumsande other suggestion, which is to process 1 thing off the peer’s workset when it disconnects? Although if trying to “attack” this way, the solution then is to just have 2 in the workset. I’d propose the same trick then, which is randomly popping from workset. That outta be good enough? I prefer something like this to potentially re-validating repeatedly.
However I think it’s very realistic to add outbound/inbound information to txorphanage (we likely want it for other stuff) so I think I shall do that.
Also, making sure, are there any ways 1P1C are affected by the choice either way?
No because 1p1c doesn’t happen through the workset stuff. We pick an orphan child and immediately process it.
I wonder if a good “mitigation” (if we’re worried) is @mzumsande other suggestion, which is to process 1 thing off the peer’s workset when it disconnects? Although if trying to “attack” this way, the solution then is to just have 2 in the workset.
Another idea that would deal with that is to assign it just to the peer who provided the parent here (no randomness), and during disconnection we don’t do any validation but pass all oustanding workset entries on to another random announcer, if any exist.
without this PR’s change, we may be doing the re-evaluation work up to num_inbound times erroneously?
yes - I suggested this because it seemed a bit wasteful in the honest case, but it is probably no big deal, because the actual full validation will only be done once and checking inputs repeatedly shouldn’t be too much additional work.
Hm. What do you mean by “the actual full validation will only be done once” btw?
Hm. What do you mean by “the actual full validation will only be done once” btw?
I meant that aborting due to missing inputs should be fast compared to full validation (signature checks), which will only be done once, because after the tx is in the mempool all subsequent checks from other peer’s worksets will abort quickly.
I meant that aborting due to missing inputs should be fast compared to full validation (signature checks), which will only be done once,
Ah ok same page π was wondering if you were thinking of some other kind of caching
409@@ -401,17 +410,8 @@ node::RejectedTxTodo TxDownloadManagerImpl::MempoolRejectedTx(const CTransaction
410 add_extra_compact_tx &= !m_orphanage.HaveTx(wtxid);
411
412 auto add_orphan_reso_candidate = [&](const CTransactionRef& orphan_tx, const std::vector<Txid>& unique_parents, NodeId nodeid, std::chrono::microseconds now) {
413- const auto& wtxid = orphan_tx->GetWitnessHash();
414- if (auto delay{OrphanResolutionCandidate(nodeid, wtxid, unique_parents.size())}) {
415- const auto& info = m_peer_info.at(nodeid).m_connection_info;
416+ if (AddOrphanParentAnnouncements(unique_parents, orphan_tx->GetWitnessHash(), nodeid, now)) {
I find the flow of logic a bit convoluted:
The check whether a peer is a orphan resolution candidate happens in OrphanResolutionCandidate
, which also calculates the delay and is called from AddOrphanParentAnnouncements
.
I would find the folllowing kind of of flow easier to understand (no need for std::optional
, functions do exactly what their name suggests they do):
0if (IsOrphanCandidate()) { // only checks if peer is a candidate
1 AddOrphanParentAnnouncements(); // delay is calculated here or in a submethod
2 m_orphanage.AddTx() / m_orphanage.AddAnnouncer();
3}
OrphanResolutionCandidate
instead. We don’t need 2 small helper functions where 1 always calls the other. I think this is even clearer, but let me know if you think I should still split it into IsOrphanCandidate()
and AddOrphanParentAnnouncements()
?
Deduplicate the logic of adding the parents as announcements to
txrequest. The function can return a bool (indicating whether we're
attempting orphan resolution) instead of the delay.
The txrequest fuzzer uses uint256s, not transactions, so it's best if
GetCandidatePeers takes that as an input.
Otherwise, it is not meaningful to test whether the announcement is
ignored, because *all* announcements of this type are ignored.
reACK 7426afbe62414fa575f91b4f8d3ea63bcc653e8b
I am -0 on “[p2p] assign just 1 random announcer in AddChildrenToWorkSet” though. That and similar changes discussed in #31666 (review) seem to complicate analysis for unclear performance gains?
reACK 7426afbe62414fa575f91b4f8d3ea63bcc653e8b
Changes since I last reviewed:
AddOrphanParentAnnoucements
and OrphanResolutionCandidate
. Checked that the logic ends up being the same with the updated version.txrequest
fuzz target more robust. I ran it for a bit to be sure.I’m also neutral on the AddChildrenToWorkSet
decision for now. Maybe it’s something that might be included/changed with a future orphan handling PR?