Skip to content

Commit fab3658

Browse files
sdaftuarMarcoFalke
authored andcommitted
[qa] Test that getdata requests work as expected
We should eventually request a transaction from all peers that announce it (assuming we never receive it). We should prefer requesting from outbound peers over inbound peers. Enforce the max tx requests in flight, and the eventual expiry of those requests. Test author: Suhas Daftuar <[email protected]> Adjusted by: MarcoFalke
1 parent fa883ab commit fab3658

File tree

2 files changed

+176
-0
lines changed

2 files changed

+176
-0
lines changed

test/functional/p2p_tx_download.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2019 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 transaction download behavior
7+
"""
8+
9+
from test_framework.messages import (
10+
CInv,
11+
CTransaction,
12+
FromHex,
13+
MSG_TX,
14+
MSG_TYPE_MASK,
15+
msg_inv,
16+
msg_notfound,
17+
)
18+
from test_framework.mininode import (
19+
P2PInterface,
20+
mininode_lock,
21+
)
22+
from test_framework.test_framework import BitcoinTestFramework
23+
from test_framework.util import (
24+
assert_equal,
25+
wait_until,
26+
)
27+
from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE
28+
29+
import time
30+
31+
32+
class TestP2PConn(P2PInterface):
33+
def __init__(self):
34+
super().__init__()
35+
self.tx_getdata_count = 0
36+
37+
def on_getdata(self, message):
38+
for i in message.inv:
39+
if i.type & MSG_TYPE_MASK == MSG_TX:
40+
self.tx_getdata_count += 1
41+
42+
43+
# Constants from net_processing
44+
GETDATA_TX_INTERVAL = 60 # seconds
45+
MAX_GETDATA_RANDOM_DELAY = 2 # seconds
46+
INBOUND_PEER_TX_DELAY = 2 # seconds
47+
MAX_GETDATA_IN_FLIGHT = 100
48+
TX_EXPIRY_INTERVAL = GETDATA_TX_INTERVAL * 10
49+
50+
# Python test constants
51+
NUM_INBOUND = 10
52+
MAX_GETDATA_INBOUND_WAIT = GETDATA_TX_INTERVAL + MAX_GETDATA_RANDOM_DELAY + INBOUND_PEER_TX_DELAY
53+
54+
55+
class TxDownloadTest(BitcoinTestFramework):
56+
def set_test_params(self):
57+
self.setup_clean_chain = False
58+
self.num_nodes = 2
59+
60+
def test_tx_requests(self):
61+
self.log.info("Test that we request transactions from all our peers, eventually")
62+
63+
txid = 0xdeadbeef
64+
65+
self.log.info("Announce the txid from each incoming peer to node 0")
66+
msg = msg_inv([CInv(t=1, h=txid)])
67+
for p in self.nodes[0].p2ps:
68+
p.send_message(msg)
69+
p.sync_with_ping()
70+
71+
outstanding_peer_index = [i for i in range(len(self.nodes[0].p2ps))]
72+
73+
def getdata_found(peer_index):
74+
p = self.nodes[0].p2ps[peer_index]
75+
with mininode_lock:
76+
return p.last_message.get("getdata") and p.last_message["getdata"].inv[-1].hash == txid
77+
78+
node_0_mocktime = int(time.time())
79+
while outstanding_peer_index:
80+
node_0_mocktime += MAX_GETDATA_INBOUND_WAIT
81+
self.nodes[0].setmocktime(node_0_mocktime)
82+
wait_until(lambda: any(getdata_found(i) for i in outstanding_peer_index))
83+
for i in outstanding_peer_index:
84+
if getdata_found(i):
85+
outstanding_peer_index.remove(i)
86+
87+
self.nodes[0].setmocktime(0)
88+
self.log.info("All outstanding peers received a getdata")
89+
90+
def test_inv_block(self):
91+
self.log.info("Generate a transaction on node 0")
92+
tx = self.nodes[0].createrawtransaction(
93+
inputs=[{ # coinbase
94+
"txid": self.nodes[0].getblock(self.nodes[0].getblockhash(1))['tx'][0],
95+
"vout": 0
96+
}],
97+
outputs={ADDRESS_BCRT1_UNSPENDABLE: 50 - 0.00025},
98+
)
99+
tx = self.nodes[0].signrawtransactionwithkey(
100+
hexstring=tx,
101+
privkeys=[self.nodes[0].get_deterministic_priv_key().key],
102+
)['hex']
103+
ctx = FromHex(CTransaction(), tx)
104+
txid = int(ctx.rehash(), 16)
105+
106+
self.log.info(
107+
"Announce the transaction to all nodes from all {} incoming peers, but never send it".format(NUM_INBOUND))
108+
msg = msg_inv([CInv(t=1, h=txid)])
109+
for p in self.peers:
110+
p.send_message(msg)
111+
p.sync_with_ping()
112+
113+
self.log.info("Put the tx in node 0's mempool")
114+
self.nodes[0].sendrawtransaction(tx)
115+
116+
# Since node 1 is connected outbound to an honest peer (node 0), it
117+
# should get the tx within a timeout. (Assuming that node 0
118+
# announced the tx within the timeout)
119+
# The timeout is the sum of
120+
# * the worst case until the tx is first requested from an inbound
121+
# peer, plus
122+
# * the first time it is re-requested from the outbound peer, plus
123+
# * 2 seconds to avoid races
124+
timeout = 2 + (MAX_GETDATA_RANDOM_DELAY + INBOUND_PEER_TX_DELAY) + (
125+
GETDATA_TX_INTERVAL + MAX_GETDATA_RANDOM_DELAY)
126+
self.log.info("Tx should be received at node 1 after {} seconds".format(timeout))
127+
self.sync_mempools(timeout=timeout)
128+
129+
def test_in_flight_max(self):
130+
self.log.info("Test that we don't request more than {} transactions from any peer, every {} minutes".format(
131+
MAX_GETDATA_IN_FLIGHT, TX_EXPIRY_INTERVAL / 60))
132+
txids = [i for i in range(MAX_GETDATA_IN_FLIGHT + 2)]
133+
134+
p = self.nodes[0].p2ps[0]
135+
136+
with mininode_lock:
137+
p.tx_getdata_count = 0
138+
139+
p.send_message(msg_inv([CInv(t=1, h=i) for i in txids]))
140+
wait_until(lambda: p.tx_getdata_count >= MAX_GETDATA_IN_FLIGHT, lock=mininode_lock)
141+
with mininode_lock:
142+
assert_equal(p.tx_getdata_count, MAX_GETDATA_IN_FLIGHT)
143+
144+
self.log.info("Now check that if we send a NOTFOUND for a transaction, we'll get one more request")
145+
p.send_message(msg_notfound(vec=[CInv(t=1, h=txids[0])]))
146+
wait_until(lambda: p.tx_getdata_count >= MAX_GETDATA_IN_FLIGHT + 1, timeout=10, lock=mininode_lock)
147+
with mininode_lock:
148+
assert_equal(p.tx_getdata_count, MAX_GETDATA_IN_FLIGHT + 1)
149+
150+
WAIT_TIME = TX_EXPIRY_INTERVAL // 2 + TX_EXPIRY_INTERVAL
151+
self.log.info("if we wait about {} minutes, we should eventually get more requests".format(WAIT_TIME / 60))
152+
self.nodes[0].setmocktime(int(time.time() + WAIT_TIME))
153+
wait_until(lambda: p.tx_getdata_count == MAX_GETDATA_IN_FLIGHT + 2)
154+
self.nodes[0].setmocktime(0)
155+
156+
def run_test(self):
157+
# Setup the p2p connections
158+
self.peers = []
159+
for node in self.nodes:
160+
for i in range(NUM_INBOUND):
161+
self.peers.append(node.add_p2p_connection(TestP2PConn()))
162+
163+
self.log.info("Nodes are setup with {} incoming connections each".format(NUM_INBOUND))
164+
165+
# Test the in-flight max first, because we want no transactions in
166+
# flight ahead of this test.
167+
self.test_in_flight_max()
168+
169+
self.test_inv_block()
170+
171+
self.test_tx_requests()
172+
173+
174+
if __name__ == '__main__':
175+
TxDownloadTest().main()

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
'wallet_labels.py',
9292
'p2p_segwit.py',
9393
'p2p_timeouts.py',
94+
'p2p_tx_download.py',
9495
'wallet_dump.py',
9596
'wallet_listtransactions.py',
9697
# vv Tests less than 60s vv

0 commit comments

Comments
 (0)