There is no reason we should process conflicting advertisements for transactions, as they cannot be both accepted into our mempool.
Avoid processing these and doing spurious work.
Should be no change in observable behavior.
There is no reason we should process conflicting advertisements for transactions, as they cannot be both accepted into our mempool.
Avoid processing these and doing spurious work.
Should be no change in observable behavior.
<!--e57a25ab6845829454e8d69fc972939a-->
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
<!--006a51241073e994b41acfe9ec718e94-->
For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35878.
<!--021abf342d371248e50ceaed478a90ca-->
See the guideline and AI policy for information on the review process.
If your review is incorrectly listed, please copy-paste <code><!--meta-tag:bot-skip--></code> into the comment that the bot should ignore.
<!--174a7506f384e20aa4161008e828411d-->
Reviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.
<!--5faf32d7da4f0f540f40219e4f7537a3-->
Concept ACK
Makes sense. Concept ACK.
Concept ACK
Deduplicating identical tx announcements makes sense.
ACK b11d8fc5f761274c1e4aa28b1cf8c40321074c21
Code Review ACK b11d8fc5f761274c1e4aa28b1cf8c40321074c21
4338 | @@ -4337,6 +4339,7 @@ void PeerManagerImpl::ProcessMessage(Peer& peer, CNode& pfrom, const std::string 4339 | } 4340 | 4341 | const bool reject_tx_invs{RejectIncomingTxs(pfrom)}; 4342 | + std::unordered_set<uint256, SaltedUint256Hasher> seen_tx_hashes;
SaltedUint256Hasher is stateful now and it's recreated for most inv messages this way - including ones without a transaction announcement.
Could we keep one salt in PeerManagerImpl and copy it into each short-lived set?
(I considered the new SipHash-1-3-UJ here, but CInv::hash is peer-controlled and does not satisfy its jumbo-input assumptions, so I think we should keep SipHash-2-4)
<details><summary>reuse tx INV dedup salt</summary>
diff --git a/src/net_processing.cpp b/src/net_processing.cpp
index cbb248b7e5..e94dac73fb 100644
--- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -844,6 +844,9 @@ private:
FastRandomContext m_rng GUARDED_BY(NetEventsInterface::g_msgproc_mutex);
+ const SaltedUint256Hasher m_txhash_hasher;
FeeFilterRounder m_fee_filter_rounder GUARDED_BY(NetEventsInterface::g_msgproc_mutex);
const CChainParams& m_chainparams;
@@ -4339,7 +4342,7 @@ void PeerManagerImpl::ProcessMessage(Peer& peer, CNode& pfrom, const std::string
}
const bool reject_tx_invs{RejectIncomingTxs(pfrom)};
- std::unordered_set<uint256, SaltedUint256Hasher> seen_tx_hashes;
+ std::unordered_set<uint256, SaltedUint256Hasher> seen_tx_hashes(0, m_txhash_hasher);
LOCK2(cs_main, m_tx_download_mutex);
</details>
considered this before, but this is pushing me over the edge: I'll make one hasher and reuse it for both filter
4377 | @@ -4375,6 +4378,7 @@ void PeerManagerImpl::ProcessMessage(Peer& peer, CNode& pfrom, const std::string 4378 | pfrom.fDisconnect = true; 4379 | return; 4380 | } 4381 | + if (!seen_tx_hashes.insert(inv.hash).second) continue;
Shouldn't we deduplicate txids and wtxids separately to make sure one doesn't prohibit the other?
BIP144 says MSG_WITNESS_TX is only for use in getdata, but IsGenTxMsg() includes it, so this inv path currently processes it.
Is it intentional for a preceding MSG_WITNESS_TX to suppress a following MSG_WTX with the same hash before AddTxAnnouncement() sees it? Concretely, this is what I mean:
<details><summary>MSG_WITNESS_TX carries a txid, while MSG_WTX carries a wtxid</summary>
diff --git a/test/functional/p2p_tx_download.py b/test/functional/p2p_tx_download.py
--- a/test/functional/p2p_tx_download.py (revision c0355a8b183de8461f29814e992e677fb813417a)
+++ b/test/functional/p2p_tx_download.py (revision aec4caf0d946e97149009d6280277c159fdc8cb5)
@@ -16,6 +16,7 @@
CInv,
MSG_TX,
MSG_TYPE_MASK,
+ MSG_WITNESS_TX,
MSG_WTX,
msg_inv,
msg_notfound,
@@ -364,6 +365,16 @@
assert_equal(log.count(inv_a_log), 1)
assert_equal(log.count(inv_b_log), 0)
+ # MSG_WITNESS_TX carries a txid, while MSG_WTX carries a wtxid.
+ peer = node.add_p2p_connection(TestP2PConn(wtxidrelay=True))
+ hash_a = 0x778899
+ log = send_invs_and_read_log(peer, [
+ CInv(t=MSG_WITNESS_TX, h=hash_a),
+ CInv(t=MSG_WTX, h=hash_a),
+ ])
+ assert_equal(log.count(f"got inv: witness-tx {hash_a:064x}"), 1)
+ assert_equal(log.count(f"got inv: wtx {hash_a:064x}"), 1) # TODO fails currently
+
def test_spurious_notfound(self):
self.log.info('Check that spurious notfound is ignored')
self.nodes[0].p2ps[0].send_without_ping(msg_notfound(vec=[CInv(MSG_TX, 1)]))
</details>
If so, we could probably fix it by having separate deduplication sets.
good catch, even if it's a protocol violation I don't want to affect behavior. Added a second filter as you suggest
I think a followup that does:
} else if (inv.IsGenTxMsg()) {
if (reject_tx_invs) { ...; return; }
if (peer.m_wtxid_relay) {
if (!inv.IsMsgWtx()) continue;
} else {
if (!inv.IsMsgTx()) continue;
}
...
would make sense. (ie, move the ignore-wrong-invs into the GenTxMsg block, disconnect first, and ignore MSG_WITNESS_TX requests) I think not dropping MSG_WITNESS_TX's was just an oversight, ref: #18044 (review)
Shouldn't we deduplicate txids and wtxids separately to make sure one doesn't prohibit the other?
I don't think this makes sense -- if you have txA with txid X and txB with wtxid X, then either txA=txB and they don't have any witness data and deduping is fine, or you've found a hash collision and bitcoin's broken anyway. Alternatively, if your peer is lying to you advertising tx inv's where it doesn't have the corresponding tx and hoping to trick you into not requesting something that you actually want, they could equally well just ignore your request if you had actually made it, just as they'll have to ignore your request for the tx that doesn't actually exist.
The relevant context seems to be the orphanage, where "the orphanage can store multiple transactions with the same txid", see https://github.com/bitcoin/bitcoin/blob/1278a5970d5ada0979052a5bad899e896b8ab40b/src/node/txdownloadman_impl.cpp#L129-L135
It's a bit complicated for me, especially since BIP 144's restriction does not seem to be enforced here. If we ignored MSG_WITNESS_TX INVs, the separate sets would probably become unnecessary. Based on the reproducer and the AI explanations, though, it seems to me that either the separation or ignoring those INVs is currently needed for this change.
<details><summary>AI summary of the problem</summary>
The same witnessless transaction is enough for the two announcements to behave differently.
The txid form reaches
AlreadyHaveTx(), which finds the witnessless orphan and returns. The wtxid form instead enters the orphan-resolution branch, which treats the peer as if it had announced every missing parent, even if those parent INVs were never sent.With a shared set, the preceding
MSG_WITNESS_TXsuppresses that second side effect, so the node loses this peer as a source for the orphan’s missing parents.The peer could ignore
getdata, but that applies to every transaction announcement and does not make these processing paths equivalent.If the follow-up ignores
MSG_WITNESS_TXINVs, the separate sets would indeed become unnecessary.
</details>
@ajtowns for my part, I wanted to keep the (lack of) behavior changes simple to follow. I had thought that there was a "domain separation" due to the existing filtering of wtid/txid invs from above, but this wasn't strictly true.
Trying to keep this as re-viewable as possible for now. Agreed on followup!
@instagibbs Yeah, that made so much sense to me it triggered my "doesn't need to be said" filter, oops.
I considered that and had the same reaction as AJ. If you get a hit for wtxid == txid, this is a legacy transaction you've already seen so you should drop it.
I wasn't very familiar with this part of the codebase (let me know if my assessment is wrong), but it seems to me the txids and wtxids should be deduped separately.
There is no reason we should process conflicting
advertisements for transactions, as they cannot be both
accepted into our mempool.
Avoid processing these and doing spurious work.
ACK 1278a5970d5ada0979052a5bad899e896b8ab40b
Don't think the separation between txids/wtxids is needed, but don't think it's harmful either. Not reseeding the hasher over time or between different peers also seems fine.
4380 | @@ -4375,6 +4381,9 @@ void PeerManagerImpl::ProcessMessage(Peer& peer, CNode& pfrom, const std::string 4381 | pfrom.fDisconnect = true; 4382 | return; 4383 | } 4384 | + // MSG_WITNESS_TX is treated as a txid, despite only being specified for getdata. 4385 | + auto& seen_hashes{inv.IsMsgWtx() ? seen_wtxids : seen_txids}; 4386 | + if (!seen_hashes.insert(inv.hash).second) continue; 4387 | const GenTxid gtxid = ToGenTxid(inv);
nit: alternatively, to avoid duplicating inv.IsMsgWtx() in ToGenTxid, we could reuse gtxid:
const GenTxid gtxid{ToGenTxid(inv)};
auto& seen_hashes{gtxid.IsWtxid() ? seen_wtxids : seen_txids};
if (!seen_hashes.insert(gtxid.ToUint256()).second) continue;
ACK 1278a5970d5ada0979052a5bad899e896b8ab40b
I don't think it's harmful, but it bugs me that we now unnecessarily duplicate our deduplication sets. Would reviewers here be interested in reviewing a small follow-up that restores this to a single set?
I don't think it's harmful, but it bugs me that we now unnecessarily duplicate our deduplication sets. Would reviewers here be interested in reviewing a small follow-up that restores this to a single set?
Sure