Merge bitcoin/bitcoin#33048: test: reduce runtime of p2p_opportunistic_1p1c.py

eb65f57f31 [test] setmocktime instead of waiting in 1p1c tests (glozow)
70772dd469 [test] cut the number of transactions involved in 1p1c DoS tests (glozow)

Pull request description:

  It was brought to my attention that the runtime of this test is Too Damn High. The test is slow due to the many `wait_for_getdata`s with delays (inbound peer + txid request) and the large volume of messages sent in the dos-related tests. This PR cuts the runtime by about 60% by reducing the number of messages/transactions and using `setmocktime` instead of waiting.

  On my machine, master:
  ```
  84.51s user 1.55s system 57% cpu 2:28.53 total
  ```
  After first commit (about 1min faster):
  ```
  28.29s user 0.88s system 35% cpu 1:22.84 total
  ```
  After second commit (about 30sec faster):
  ```
  28.17s user 0.87s system 59% cpu 49.082 total
  ```

  Reviewers should verify that the transactions in the DoS tests are still enough to cause evictions, and that the `bumpmocktime` amounts are not more than necessary.

  Alternatives:
  - If we don't like mocking the times, we can use outbound connections for all the peers. However, that approach won't improve the runtime as much because we impose a 2-second delay on all txid requests regardless of peer type.
  - Note that `noban_tx_relay` is not relevant for this test because all delays are related to downloading, not announcing.

ACKs for top commit:
  achow101:
    ACK eb65f57f31
  w0xlt:
    ACK eb65f57f31

Tree-SHA512: 6ffe1f9e5144653e2ded744cec9ddb62ad728c587705542565400a0e8f1fba4843aced4e0d929843874ca7f56f670f5871b7e009ff6be58b791ab24d2e6fcc0e
This commit is contained in:
merge-script
2025-08-01 16:06:40 +01:00

View File

@@ -91,6 +91,7 @@ class PackageRelayTest(BitcoinTestFramework):
def test_basic_child_then_parent(self): def test_basic_child_then_parent(self):
node = self.nodes[0] node = self.nodes[0]
self.log.info("Check that opportunistic 1p1c logic works when child is received before parent") self.log.info("Check that opportunistic 1p1c logic works when child is received before parent")
node.setmocktime(int(time.time()))
low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet) low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet)
high_fee_child = self.wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=20*FEERATE_1SAT_VB) high_fee_child = self.wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=20*FEERATE_1SAT_VB)
@@ -100,11 +101,13 @@ class PackageRelayTest(BitcoinTestFramework):
# 1. Child is received first (perhaps the low feerate parent didn't meet feefilter or the requests were sent to different nodes). It is missing an input. # 1. Child is received first (perhaps the low feerate parent didn't meet feefilter or the requests were sent to different nodes). It is missing an input.
high_child_wtxid_int = high_fee_child["tx"].wtxid_int high_child_wtxid_int = high_fee_child["tx"].wtxid_int
peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=high_child_wtxid_int)])) peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=high_child_wtxid_int)]))
node.bumpmocktime(NONPREF_PEER_TX_DELAY)
peer_sender.wait_for_getdata([high_child_wtxid_int]) peer_sender.wait_for_getdata([high_child_wtxid_int])
peer_sender.send_and_ping(msg_tx(high_fee_child["tx"])) peer_sender.send_and_ping(msg_tx(high_fee_child["tx"]))
# 2. Node requests the missing parent by txid. # 2. Node requests the missing parent by txid.
parent_txid_int = int(low_fee_parent["txid"], 16) parent_txid_int = int(low_fee_parent["txid"], 16)
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
peer_sender.wait_for_getdata([parent_txid_int]) peer_sender.wait_for_getdata([parent_txid_int])
# 3. Sender relays the parent. Parent+Child are evaluated as a package and accepted. # 3. Sender relays the parent. Parent+Child are evaluated as a package and accepted.
@@ -120,6 +123,7 @@ class PackageRelayTest(BitcoinTestFramework):
@cleanup @cleanup
def test_basic_parent_then_child(self, wallet): def test_basic_parent_then_child(self, wallet):
node = self.nodes[0] node = self.nodes[0]
node.setmocktime(int(time.time()))
low_fee_parent = self.create_tx_below_mempoolminfee(wallet) low_fee_parent = self.create_tx_below_mempoolminfee(wallet)
high_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=20*FEERATE_1SAT_VB) high_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=20*FEERATE_1SAT_VB)
@@ -146,6 +150,7 @@ class PackageRelayTest(BitcoinTestFramework):
# 3. Node requests the missing parent by txid. # 3. Node requests the missing parent by txid.
# It should do so even if it has previously rejected that parent for being too low feerate. # It should do so even if it has previously rejected that parent for being too low feerate.
parent_txid_int = int(low_fee_parent["txid"], 16) parent_txid_int = int(low_fee_parent["txid"], 16)
node.bumpmocktime(TXID_RELAY_DELAY)
peer_sender.wait_for_getdata([parent_txid_int]) peer_sender.wait_for_getdata([parent_txid_int])
# 4. Sender re-relays the parent. Parent+Child are evaluated as a package and accepted. # 4. Sender re-relays the parent. Parent+Child are evaluated as a package and accepted.
@@ -159,6 +164,7 @@ class PackageRelayTest(BitcoinTestFramework):
@cleanup @cleanup
def test_low_and_high_child(self, wallet): def test_low_and_high_child(self, wallet):
node = self.nodes[0] node = self.nodes[0]
node.setmocktime(int(time.time()))
low_fee_parent = self.create_tx_below_mempoolminfee(wallet) low_fee_parent = self.create_tx_below_mempoolminfee(wallet)
# This feerate is above mempoolminfee, but not enough to also bump the low feerate parent. # This feerate is above mempoolminfee, but not enough to also bump the low feerate parent.
feerate_just_above = node.getmempoolinfo()["mempoolminfee"] feerate_just_above = node.getmempoolinfo()["mempoolminfee"]
@@ -189,6 +195,7 @@ class PackageRelayTest(BitcoinTestFramework):
# 3. Node requests the orphan's missing parent. # 3. Node requests the orphan's missing parent.
parent_txid_int = int(low_fee_parent["txid"], 16) parent_txid_int = int(low_fee_parent["txid"], 16)
node.bumpmocktime(TXID_RELAY_DELAY)
peer_sender.wait_for_getdata([parent_txid_int]) peer_sender.wait_for_getdata([parent_txid_int])
# 4. The low parent + low child are submitted as a package. They are not accepted due to low package feerate. # 4. The low parent + low child are submitted as a package. They are not accepted due to low package feerate.
@@ -216,6 +223,7 @@ class PackageRelayTest(BitcoinTestFramework):
# 6. Node requests the orphan's parent, even though it has already been rejected, both by # 6. Node requests the orphan's parent, even though it has already been rejected, both by
# itself and with a child. This is necessary, otherwise high_fee_child can be censored. # itself and with a child. This is necessary, otherwise high_fee_child can be censored.
parent_txid_int = int(low_fee_parent["txid"], 16) parent_txid_int = int(low_fee_parent["txid"], 16)
node.bumpmocktime(TXID_RELAY_DELAY)
peer_sender.wait_for_getdata([parent_txid_int]) peer_sender.wait_for_getdata([parent_txid_int])
# 7. The low feerate parent + high feerate child are submitted as a package. # 7. The low feerate parent + high feerate child are submitted as a package.
@@ -231,6 +239,7 @@ class PackageRelayTest(BitcoinTestFramework):
def test_orphan_consensus_failure(self): def test_orphan_consensus_failure(self):
self.log.info("Check opportunistic 1p1c logic requires parent and child to be from the same peer") self.log.info("Check opportunistic 1p1c logic requires parent and child to be from the same peer")
node = self.nodes[0] node = self.nodes[0]
node.setmocktime(int(time.time()))
low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet) low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet)
coin = low_fee_parent["new_utxo"] coin = low_fee_parent["new_utxo"]
address = node.get_deterministic_priv_key().address address = node.get_deterministic_priv_key().address
@@ -246,11 +255,13 @@ class PackageRelayTest(BitcoinTestFramework):
# 1. Child is received first. It is missing an input. # 1. Child is received first. It is missing an input.
child_wtxid_int = tx_orphan_bad_wit.wtxid_int child_wtxid_int = tx_orphan_bad_wit.wtxid_int
bad_orphan_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=child_wtxid_int)])) bad_orphan_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=child_wtxid_int)]))
node.bumpmocktime(NONPREF_PEER_TX_DELAY)
bad_orphan_sender.wait_for_getdata([child_wtxid_int]) bad_orphan_sender.wait_for_getdata([child_wtxid_int])
bad_orphan_sender.send_and_ping(msg_tx(tx_orphan_bad_wit)) bad_orphan_sender.send_and_ping(msg_tx(tx_orphan_bad_wit))
# 2. Node requests the missing parent by txid. # 2. Node requests the missing parent by txid.
parent_txid_int = int(low_fee_parent["txid"], 16) parent_txid_int = int(low_fee_parent["txid"], 16)
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
bad_orphan_sender.wait_for_getdata([parent_txid_int]) bad_orphan_sender.wait_for_getdata([parent_txid_int])
# 3. A different peer relays the parent. Package is not evaluated because the transactions # 3. A different peer relays the parent. Package is not evaluated because the transactions
@@ -273,6 +284,8 @@ class PackageRelayTest(BitcoinTestFramework):
def test_parent_consensus_failure(self): def test_parent_consensus_failure(self):
self.log.info("Check opportunistic 1p1c logic with consensus-invalid parent causes disconnect of the correct peer") self.log.info("Check opportunistic 1p1c logic with consensus-invalid parent causes disconnect of the correct peer")
node = self.nodes[0] node = self.nodes[0]
node.setmocktime(int(time.time()))
low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet) low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet)
high_fee_child = self.wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=999*FEERATE_1SAT_VB) high_fee_child = self.wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=999*FEERATE_1SAT_VB)
@@ -287,11 +300,13 @@ class PackageRelayTest(BitcoinTestFramework):
# 1. Child is received first. It is missing an input. # 1. Child is received first. It is missing an input.
child_wtxid_int = high_fee_child["tx"].wtxid_int child_wtxid_int = high_fee_child["tx"].wtxid_int
package_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=child_wtxid_int)])) package_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=child_wtxid_int)]))
node.bumpmocktime(NONPREF_PEER_TX_DELAY)
package_sender.wait_for_getdata([child_wtxid_int]) package_sender.wait_for_getdata([child_wtxid_int])
package_sender.send_and_ping(msg_tx(high_fee_child["tx"])) package_sender.send_and_ping(msg_tx(high_fee_child["tx"]))
# 2. Node requests the missing parent by txid. # 2. Node requests the missing parent by txid.
parent_txid_int = tx_parent_bad_wit.txid_int parent_txid_int = tx_parent_bad_wit.txid_int
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
package_sender.wait_for_getdata([parent_txid_int]) package_sender.wait_for_getdata([parent_txid_int])
# 3. A different node relays the parent. The parent is first evaluated by itself and # 3. A different node relays the parent. The parent is first evaluated by itself and
@@ -309,6 +324,7 @@ class PackageRelayTest(BitcoinTestFramework):
# It can send the "real" parent transaction, and the package is accepted. # It can send the "real" parent transaction, and the package is accepted.
parent_wtxid_int = low_fee_parent["tx"].wtxid_int parent_wtxid_int = low_fee_parent["tx"].wtxid_int
package_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)])) package_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))
node.bumpmocktime(NONPREF_PEER_TX_DELAY)
package_sender.wait_for_getdata([parent_wtxid_int]) package_sender.wait_for_getdata([parent_wtxid_int])
package_sender.send_and_ping(msg_tx(low_fee_parent["tx"])) package_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
@@ -355,6 +371,7 @@ class PackageRelayTest(BitcoinTestFramework):
def test_other_parent_in_mempool(self): def test_other_parent_in_mempool(self):
self.log.info("Check opportunistic 1p1c works when part of a 2p1c (child already has another parent in mempool)") self.log.info("Check opportunistic 1p1c works when part of a 2p1c (child already has another parent in mempool)")
node = self.nodes[0] node = self.nodes[0]
node.setmocktime(int(time.time()))
# Grandparent will enter mempool by itself # Grandparent will enter mempool by itself
grandparent_high = self.wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB*10, confirmed_only=True) grandparent_high = self.wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB*10, confirmed_only=True)
@@ -383,6 +400,7 @@ class PackageRelayTest(BitcoinTestFramework):
# 4. Node requests parent_low. # 4. Node requests parent_low.
parent_low_txid_int = int(parent_low["txid"], 16) parent_low_txid_int = int(parent_low["txid"], 16)
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
peer_sender.wait_for_getdata([parent_low_txid_int]) peer_sender.wait_for_getdata([parent_low_txid_int])
peer_sender.send_and_ping(msg_tx(parent_low["tx"])) peer_sender.send_and_ping(msg_tx(parent_low["tx"]))
@@ -410,9 +428,10 @@ class PackageRelayTest(BitcoinTestFramework):
peer_normal = node.add_p2p_connection(P2PInterface()) peer_normal = node.add_p2p_connection(P2PInterface())
peer_doser = node.add_p2p_connection(P2PInterface()) peer_doser = node.add_p2p_connection(P2PInterface())
num_individual_dosers = 10
self.log.info("Create very large orphans to be sent by DoSy peers (may take a while)") self.log.info("Create very large orphans to be sent by DoSy peers (may take a while)")
large_orphans = [create_large_orphan() for _ in range(100)] large_orphans = [create_large_orphan() for _ in range(50)]
# Check to make sure these are orphans, within max standard size (to be accepted into the orphanage) # Check to make sure these are orphans, within max standard size (to be accepted into the orphanage)
for large_orphan in large_orphans: for large_orphan in large_orphans:
assert_greater_than_or_equal(100000, large_orphan.get_vsize()) assert_greater_than_or_equal(100000, large_orphan.get_vsize())
@@ -422,11 +441,10 @@ class PackageRelayTest(BitcoinTestFramework):
assert not testres[0]["allowed"] assert not testres[0]["allowed"]
assert_equal(testres[0]["reject-reason"], "missing-inputs") assert_equal(testres[0]["reject-reason"], "missing-inputs")
num_individual_dosers = 30
self.log.info(f"Connect {num_individual_dosers} peers and send a very large orphan from each one") self.log.info(f"Connect {num_individual_dosers} peers and send a very large orphan from each one")
# This test assumes that unrequested transactions are processed (skipping inv and # This test assumes that unrequested transactions are processed (skipping inv and
# getdata steps because they require going through request delays) # getdata steps because they require going through request delays)
# Connect 20 peers and have each of them send a large orphan. # Connect 10 peers and have each of them send a large orphan.
for large_orphan in large_orphans[:num_individual_dosers]: for large_orphan in large_orphans[:num_individual_dosers]:
peer_doser_individual = node.add_p2p_connection(P2PInterface()) peer_doser_individual = node.add_p2p_connection(P2PInterface())
peer_doser_individual.send_and_ping(msg_tx(large_orphan)) peer_doser_individual.send_and_ping(msg_tx(large_orphan))
@@ -462,7 +480,7 @@ class PackageRelayTest(BitcoinTestFramework):
peer_normal.wait_for_getdata([parent_txid_int]) peer_normal.wait_for_getdata([parent_txid_int])
self.log.info("Send another round of very large orphans from a DoSy peer") self.log.info("Send another round of very large orphans from a DoSy peer")
for large_orphan in large_orphans[30:]: for large_orphan in large_orphans[num_individual_dosers:]:
peer_doser.send_and_ping(msg_tx(large_orphan)) peer_doser.send_and_ping(msg_tx(large_orphan))
# Something was evicted; the orphanage does not contain all large orphans + the 1p1c child # Something was evicted; the orphanage does not contain all large orphans + the 1p1c child
@@ -480,12 +498,12 @@ class PackageRelayTest(BitcoinTestFramework):
peer_normal = node.add_p2p_connection(P2PInterface()) peer_normal = node.add_p2p_connection(P2PInterface())
# 2 sets of peers: the first set all send the same batch_size orphans. The second set each # The first set of peers all send the same batch_size orphans. Then a single peer sends
# sends batch_size distinct orphans. # batch_single_doser distinct orphans.
batch_size = 51 batch_size = 51
num_peers_shared = 60 num_peers_shared = 60
num_peers_unique = 40 batch_single_doser = 100
assert_greater_than(num_peers_shared * batch_size + batch_single_doser, 3000)
# 60 peers * 51 orphans = 3060 announcements # 60 peers * 51 orphans = 3060 announcements
shared_orphans = [self.create_small_orphan() for _ in range(batch_size)] shared_orphans = [self.create_small_orphan() for _ in range(batch_size)]
self.log.info(f"Send the same {batch_size} orphans from {num_peers_shared} DoSy peers (may take a while)") self.log.info(f"Send the same {batch_size} orphans from {num_peers_shared} DoSy peers (may take a while)")
@@ -525,22 +543,15 @@ class PackageRelayTest(BitcoinTestFramework):
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY) node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
peer_normal.wait_for_getdata([parent_txid_int]) peer_normal.wait_for_getdata([parent_txid_int])
# Each of the num_peers_unique peers creates a distinct set of orphans self.log.info(f"Send {batch_single_doser} new orphans from one DoSy peer")
many_orphans = [self.create_small_orphan() for _ in range(batch_size * num_peers_unique)] peer_doser_batch = node.add_p2p_connection(P2PInterface())
this_batch_orphans = [self.create_small_orphan() for _ in range(batch_single_doser)]
for tx in this_batch_orphans:
# Don't wait for responses, because it dramatically increases the runtime of this test.
peer_doser_batch.send_without_ping(msg_tx(tx))
self.log.info(f"Send sets of {batch_size} orphans from {num_peers_unique} DoSy peers (may take a while)") peer_doser_batch.sync_with_ping()
for peernum in range(num_peers_unique): self.wait_until(lambda: any([tx.txid_hex in node.getorphantxs() for tx in this_batch_orphans]))
peer_doser_batch = node.add_p2p_connection(P2PInterface())
this_batch_orphans = many_orphans[batch_size*peernum : batch_size*(peernum+1)]
for tx in this_batch_orphans:
# Don't wait for responses, because it dramatically increases the runtime of this test.
peer_doser_batch.send_without_ping(msg_tx(tx))
# Ensure at least one of the peer's orphans shows up in getorphantxs. Since each peer is
# reserved a portion of orphanage space, this must happen as long as the orphans are not
# rejected for some other reason.
peer_doser_batch.sync_with_ping()
self.wait_until(lambda: any([tx.txid_hex in node.getorphantxs() for tx in this_batch_orphans]))
self.log.info("Check that orphan from normal peer still exists in orphanage") self.log.info("Check that orphan from normal peer still exists in orphanage")
assert high_fee_child["txid"] in node.getorphantxs() assert high_fee_child["txid"] in node.getorphantxs()