Skip to content

Commit 78aee0f

Browse files
committed
Merge bitcoin/bitcoin#26569: p2p: Ensure transaction announcements are only queued for fully connected peers
8f2dac5 [test] Add p2p_tx_privacy.py (dergoegge) ce63fca [net processing] Assume that TxRelay::m_tx_inventory_to_send is empty pre-verack (dergoegge) 845e3a3 [net processing] Ensure transaction announcements are only queued for fully connected peers (dergoegge) Pull request description: `TxRelay::m_next_inv_send_time` is initialized to 0, which means that any txids in `TxRelay::m_tx_inventory_to_send` will be announced on the first call to `PeerManagerImpl::SendMessages` for a fully connected peer (i.e. it completed the version handshake). Prior to #21160, `TxRelay::m_tx_inventory_to_send` was guaranteed to be empty on the first `SendMessages` call, as transaction announcements were only queued for fully connected peers. #21160 replaced a `CConnman::ForEachNode` call with a loop over `PeerManagerImpl::m_peer_map`, in which the txid for a transaction to be relayed is added to `TxRelay::m_tx_inventory_to_send` for all peers. Even for those peers that have not completed the version handshake. Prior to the PR this was not the case as `ForEachNode` has a "fully connected check" before calling a function for each node. ACKs for top commit: MarcoFalke: ACK 8f2dac5 🔝 jnewbery: utACK 8f2dac5 Tree-SHA512: e9eaccf7e00633ee0806fff1068b0e413a69a5e389d96c9659f68079915a6381ad5040c61f716cfcde77931d1b563b1049da97a232a95c6cd8355bd3d13404b9
2 parents 9e59d21 + 8f2dac5 commit 78aee0f

File tree

3 files changed

+102
-2
lines changed

3 files changed

+102
-2
lines changed

src/net_processing.cpp

+23-2
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ struct Peer {
295295
std::atomic<std::chrono::seconds> m_last_mempool_req{0s};
296296
/** The next time after which we will send an `inv` message containing
297297
* transaction announcements to this peer. */
298-
std::chrono::microseconds m_next_inv_send_time GUARDED_BY(NetEventsInterface::g_msgproc_mutex){0};
298+
std::chrono::microseconds m_next_inv_send_time GUARDED_BY(m_tx_inventory_mutex){0};
299299

300300
/** Minimum fee rate with which to filter transaction announcements to this node. See BIP133. */
301301
std::atomic<CAmount> m_fee_filter_received{0};
@@ -2017,8 +2017,15 @@ void PeerManagerImpl::RelayTransaction(const uint256& txid, const uint256& wtxid
20172017
auto tx_relay = peer.GetTxRelay();
20182018
if (!tx_relay) continue;
20192019

2020-
const uint256& hash{peer.m_wtxid_relay ? wtxid : txid};
20212020
LOCK(tx_relay->m_tx_inventory_mutex);
2021+
// Only queue transactions for announcement once the version handshake
2022+
// is completed. The time of arrival for these transactions is
2023+
// otherwise at risk of leaking to a spy, if the spy is able to
2024+
// distinguish transactions received during the handshake from the rest
2025+
// in the announcement.
2026+
if (tx_relay->m_next_inv_send_time == 0s) continue;
2027+
2028+
const uint256& hash{peer.m_wtxid_relay ? wtxid : txid};
20222029
if (!tx_relay->m_tx_inventory_known_filter.contains(hash)) {
20232030
tx_relay->m_tx_inventory_to_send.insert(hash);
20242031
}
@@ -3424,6 +3431,20 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
34243431
}
34253432
}
34263433

3434+
if (auto tx_relay = peer->GetTxRelay()) {
3435+
// `TxRelay::m_tx_inventory_to_send` must be empty before the
3436+
// version handshake is completed as
3437+
// `TxRelay::m_next_inv_send_time` is first initialised in
3438+
// `SendMessages` after the verack is received. Any transactions
3439+
// received during the version handshake would otherwise
3440+
// immediately be advertised without random delay, potentially
3441+
// leaking the time of arrival to a spy.
3442+
Assume(WITH_LOCK(
3443+
tx_relay->m_tx_inventory_mutex,
3444+
return tx_relay->m_tx_inventory_to_send.empty() &&
3445+
tx_relay->m_next_inv_send_time == 0s));
3446+
}
3447+
34273448
pfrom.fSuccessfullyConnected = true;
34283449
return;
34293450
}

test/functional/p2p_tx_privacy.py

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2022 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""
6+
Test that transaction announcements are only queued for peers that have
7+
successfully completed the version handshake.
8+
9+
Topology:
10+
11+
tx_originator ----> node[0] <---- spy
12+
13+
We test that a transaction sent by tx_originator is only relayed to spy
14+
if it was received after spy's version handshake completed.
15+
16+
1. Fully connect tx_originator
17+
2. Connect spy (no version handshake)
18+
3. tx_originator sends tx1
19+
4. spy completes the version handshake
20+
5. tx_originator sends tx2
21+
6. We check that only tx2 is announced on the spy interface
22+
"""
23+
from test_framework.messages import (
24+
msg_wtxidrelay,
25+
msg_verack,
26+
msg_tx,
27+
CInv,
28+
MSG_WTX,
29+
)
30+
from test_framework.p2p import (
31+
P2PInterface,
32+
)
33+
from test_framework.test_framework import BitcoinTestFramework
34+
from test_framework.wallet import MiniWallet
35+
36+
class P2PTxSpy(P2PInterface):
37+
def __init__(self):
38+
super().__init__()
39+
self.all_invs = []
40+
41+
def on_version(self, message):
42+
self.send_message(msg_wtxidrelay())
43+
44+
def on_inv(self, message):
45+
self.all_invs += message.inv
46+
47+
def wait_for_inv_match(self, expected_inv):
48+
self.wait_until(lambda: len(self.all_invs) == 1 and self.all_invs[0] == expected_inv)
49+
50+
class TxPrivacyTest(BitcoinTestFramework):
51+
def set_test_params(self):
52+
self.num_nodes = 1
53+
54+
def run_test(self):
55+
self.wallet = MiniWallet(self.nodes[0])
56+
self.wallet.rescan_utxos()
57+
58+
tx_originator = self.nodes[0].add_p2p_connection(P2PInterface())
59+
spy = self.nodes[0].add_p2p_connection(P2PTxSpy(), wait_for_verack=False)
60+
spy.wait_for_verack()
61+
62+
# tx_originator sends tx1
63+
tx1 = self.wallet.create_self_transfer()["tx"]
64+
tx_originator.send_and_ping(msg_tx(tx1))
65+
66+
# Spy sends the verack
67+
spy.send_and_ping(msg_verack())
68+
69+
# tx_originator sends tx2
70+
tx2 = self.wallet.create_self_transfer()["tx"]
71+
tx_originator.send_and_ping(msg_tx(tx2))
72+
73+
# Spy should only get an inv for the second transaction as the first
74+
# one was received pre-verack with the spy
75+
spy.wait_for_inv_match(CInv(MSG_WTX, tx2.calc_sha256(True)))
76+
77+
if __name__ == '__main__':
78+
TxPrivacyTest().main()

test/functional/test_runner.py

+1
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@
320320
'rpc_deriveaddresses.py',
321321
'rpc_deriveaddresses.py --usecli',
322322
'p2p_ping.py',
323+
'p2p_tx_privacy.py',
323324
'rpc_scanblocks.py',
324325
'p2p_sendtxrcncl.py',
325326
'rpc_scantxoutset.py',

0 commit comments

Comments
 (0)