From 70772dd4693ba6b09901a6b812eef21fa240b666 Mon Sep 17 00:00:00 2001 From: glozow Date: Wed, 23 Jul 2025 14:54:52 -0400 Subject: [PATCH 1/2] [test] cut the number of transactions involved in 1p1c DoS tests We just need enough transactions to push us above the orphanage limits and trigger trimming. Reducing the number of transactions cuts the runtime of this test significantly. --- test/functional/p2p_opportunistic_1p1c.py | 39 ++++++++++------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/test/functional/p2p_opportunistic_1p1c.py b/test/functional/p2p_opportunistic_1p1c.py index e841cf0806..546d38ca09 100755 --- a/test/functional/p2p_opportunistic_1p1c.py +++ b/test/functional/p2p_opportunistic_1p1c.py @@ -407,9 +407,10 @@ class PackageRelayTest(BitcoinTestFramework): peer_normal = 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)") - 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) for large_orphan in large_orphans: assert_greater_than_or_equal(100000, large_orphan.get_vsize()) @@ -419,11 +420,10 @@ class PackageRelayTest(BitcoinTestFramework): assert not testres[0]["allowed"] 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") # This test assumes that unrequested transactions are processed (skipping inv and # 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]: peer_doser_individual = node.add_p2p_connection(P2PInterface()) peer_doser_individual.send_and_ping(msg_tx(large_orphan)) @@ -459,7 +459,7 @@ class PackageRelayTest(BitcoinTestFramework): peer_normal.wait_for_getdata([parent_txid_int]) 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)) # Something was evicted; the orphanage does not contain all large orphans + the 1p1c child @@ -477,12 +477,12 @@ class PackageRelayTest(BitcoinTestFramework): 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 - # sends batch_size distinct orphans. + # The first set of peers all send the same batch_size orphans. Then a single peer sends + # batch_single_doser distinct orphans. batch_size = 51 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 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)") @@ -522,22 +522,15 @@ class PackageRelayTest(BitcoinTestFramework): node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY) peer_normal.wait_for_getdata([parent_txid_int]) - # Each of the num_peers_unique peers creates a distinct set of orphans - many_orphans = [self.create_small_orphan() for _ in range(batch_size * num_peers_unique)] + self.log.info(f"Send {batch_single_doser} new orphans from one DoSy peer") + 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)") - for peernum in range(num_peers_unique): - 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])) + 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") assert high_fee_child["txid"] in node.getorphantxs() From eb65f57f319dc4e2ea8c83cf7e283c36f1c0d53b Mon Sep 17 00:00:00 2001 From: glozow Date: Wed, 23 Jul 2025 15:05:17 -0400 Subject: [PATCH 2/2] [test] setmocktime instead of waiting in 1p1c tests Significantly speed up the runtime of the test. --- test/functional/p2p_opportunistic_1p1c.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/functional/p2p_opportunistic_1p1c.py b/test/functional/p2p_opportunistic_1p1c.py index 546d38ca09..07bbca7c5d 100755 --- a/test/functional/p2p_opportunistic_1p1c.py +++ b/test/functional/p2p_opportunistic_1p1c.py @@ -91,6 +91,7 @@ class PackageRelayTest(BitcoinTestFramework): def test_basic_child_then_parent(self): node = self.nodes[0] 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) 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. 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)])) + node.bumpmocktime(NONPREF_PEER_TX_DELAY) peer_sender.wait_for_getdata([high_child_wtxid_int]) peer_sender.send_and_ping(msg_tx(high_fee_child["tx"])) # 2. Node requests the missing parent by txid. 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]) # 3. Sender relays the parent. Parent+Child are evaluated as a package and accepted. @@ -120,6 +123,7 @@ class PackageRelayTest(BitcoinTestFramework): @cleanup def test_basic_parent_then_child(self, wallet): node = self.nodes[0] + node.setmocktime(int(time.time())) 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) @@ -146,6 +150,7 @@ class PackageRelayTest(BitcoinTestFramework): # 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. parent_txid_int = int(low_fee_parent["txid"], 16) + node.bumpmocktime(TXID_RELAY_DELAY) peer_sender.wait_for_getdata([parent_txid_int]) # 4. Sender re-relays the parent. Parent+Child are evaluated as a package and accepted. @@ -159,6 +164,7 @@ class PackageRelayTest(BitcoinTestFramework): @cleanup def test_low_and_high_child(self, wallet): node = self.nodes[0] + node.setmocktime(int(time.time())) low_fee_parent = self.create_tx_below_mempoolminfee(wallet) # This feerate is above mempoolminfee, but not enough to also bump the low feerate parent. feerate_just_above = node.getmempoolinfo()["mempoolminfee"] @@ -189,6 +195,7 @@ class PackageRelayTest(BitcoinTestFramework): # 3. Node requests the orphan's missing parent. parent_txid_int = int(low_fee_parent["txid"], 16) + node.bumpmocktime(TXID_RELAY_DELAY) 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. @@ -216,6 +223,7 @@ class PackageRelayTest(BitcoinTestFramework): # 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. parent_txid_int = int(low_fee_parent["txid"], 16) + node.bumpmocktime(TXID_RELAY_DELAY) peer_sender.wait_for_getdata([parent_txid_int]) # 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): self.log.info("Check opportunistic 1p1c logic requires parent and child to be from the same peer") node = self.nodes[0] + node.setmocktime(int(time.time())) low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet) coin = low_fee_parent["new_utxo"] address = node.get_deterministic_priv_key().address @@ -246,11 +255,13 @@ class PackageRelayTest(BitcoinTestFramework): # 1. Child is received first. It is missing an input. 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)])) + node.bumpmocktime(NONPREF_PEER_TX_DELAY) bad_orphan_sender.wait_for_getdata([child_wtxid_int]) bad_orphan_sender.send_and_ping(msg_tx(tx_orphan_bad_wit)) # 2. Node requests the missing parent by txid. 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]) # 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): self.log.info("Check opportunistic 1p1c logic with consensus-invalid parent causes disconnect of the correct peer") node = self.nodes[0] + node.setmocktime(int(time.time())) + 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) @@ -287,11 +300,13 @@ class PackageRelayTest(BitcoinTestFramework): # 1. Child is received first. It is missing an input. child_wtxid_int = high_fee_child["tx"].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.send_and_ping(msg_tx(high_fee_child["tx"])) # 2. Node requests the missing parent by txid. 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]) # 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. parent_wtxid_int = low_fee_parent["tx"].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.send_and_ping(msg_tx(low_fee_parent["tx"])) @@ -355,6 +371,7 @@ class PackageRelayTest(BitcoinTestFramework): def test_other_parent_in_mempool(self): self.log.info("Check opportunistic 1p1c fails if child already has another parent in mempool") node = self.nodes[0] + node.setmocktime(int(time.time())) # This parent needs CPFP parent_low = self.create_tx_below_mempoolminfee(self.wallet) @@ -376,6 +393,7 @@ class PackageRelayTest(BitcoinTestFramework): # 3. Node requests parent_low. However, 1p1c fails because package-not-child-with-unconfirmed-parents 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.send_and_ping(msg_tx(parent_low["tx"]))