Merge bitcoin/bitcoin#31829: p2p: improve TxOrphanage denial of service bounds

50024620b9 [bench] worst case LimitOrphans and EraseForBlock (glozow)
45c7a4b56d [functional test] orphan resolution works in the presence of DoSy peers (glozow)
835f5c77cd [prep/test] restart instead of bumpmocktime between p2p_orphan_handling subtests (glozow)
b113877545 [fuzz] Add simulation fuzz test for TxOrphanage (Pieter Wuille)
03aaaedc6d [prep] Return the made-reconsiderable announcements in AddChildrenToWorkSet (Pieter Wuille)
ea29c4371e [p2p] bump DEFAULT_MAX_ORPHANAGE_LATENCY_SCORE to 3,000 (glozow)
24afee8d8f [fuzz] TxOrphanage protects peers that don't go over limit (glozow)
a2878cfb4a [unit test] strengthen GetChildrenFromSamePeer tests: results are in recency order (glozow)
7ce3b7ee57 [unit test] basic TxOrphanage eviction and protection (glozow)
4d23d1d7e7 [cleanup] remove unused rng param from LimitOrphans (glozow)
067365d2a8 [p2p] overhaul TxOrphanage with smarter limits (glozow)
1a41e7962d [refactor] create aliases for TxOrphanage Count and Usage (glozow)
b50bd72c42 [prep] change return type of EraseTx to bool (glozow)
3da6d7f8f6 [prep/refactor] make TxOrphanage a virtual class implemented by TxOrphanageImpl (glozow)
77ebe8f280 [prep/test] have TxOrphanage remember its own limits in LimitOrphans (glozow)
d0af4239b7 [prep/refactor] move DEFAULT_MAX_ORPHAN_TRANSACTIONS to txorphanage.h (glozow)
51365225b8 [prep/config] remove -maxorphantx (glozow)
8dd24c29ae [prep/test] modify test to not access TxOrphanage internals (glozow)
44f5327824 [fuzz] add SeedRandomStateForTest(SeedRand::ZEROS) to txorphan (glozow)
15a4ec9069 [prep/rpc] remove entry and expiry time from getorphantxs (glozow)
08e58fa911 [prep/refactor] move txorphanage to node namespace and directory (glozow)
bb91d23fa9 [txorphanage] change type of usage to int64_t (glozow)

Pull request description:

  This PR is part of the orphan resolution project, see #27463.

  This design came from collaboration with sipa - thanks.

  We want to limit the CPU work and memory used by `TxOrphanage` to avoid denial of service attacks. On master, this is achieved by limiting the number of transactions in this data structure to 100, and the weight of each transaction to 400KWu (the largest standard tx) [0]. We always allow new orphans, but if the addition causes us to exceed 100, we evict one randomly. This is dead simple, but has problems:
  - It makes the orphanage trivially churnable: any one peer can render it useless by spamming us with lots of orphans. It's possible this is happening: "Looking at data from node alice on 2024-09-14 shows that we’re sometimes removing more than 100k orphans per minute. This feels like someone flooding us with orphans." [1]
  - Effectively, opportunistic 1p1c is useless in the presence of adversaries: it is *opportunistic* and pairs a low feerate tx with a child that happens to be in the orphanage. So if nothing is able to stay in orphanages, we can't expect 1p1cs to propagate.
  - This number is also often insufficient for the volume of orphans we handle: historical data show that overflows are pretty common, and there are times where "it seems like [the node] forgot about the orphans and re-requested them multiple times." [1]

  Just jacking up the `-maxorphantxs` number is not a good enough solution, because it doesn't solve the churnability problem, and the effective resource bounds scale poorly.

  This PR introduces numbers for {global, per-peer} {memory usage, announcements + number of inputs}, representing resource limits:
  - The (constant) **global latency score limit** is the number of unique (wtxid, peer) pairs in the orphanage + the number of inputs spent by those (deduplicated) transactions floor-divided by 10 [2]. This represents a cap on CPU or latency for any given operation, and does not change with the number of peers we have. Evictions must happen whenever this limit is reached. The primary goal of this limit is to ensure we do not spend more than a few ms on any call to `LimitOrphans` or `EraseForBlock`.
  - The (variable) **per-peer latency score limit** is the global latency score limit divided by the number of peers. Peers are allowed to exceed this limit provided the global announcement limit has not been reached. The per-peer announcement limit decreases with more peers.
  - The (constant) **per-peer memory usage reservation** is the amount of orphan weight [3] reserved per peer [4]. Reservation means that peers are effectively guaranteed this amount of space. Peers are allowed to exceed this limit provided the global usage limit is not reached. The primary goal of this limit is to ensure we don't oom.
  - The (variable) **global memory usage limit** is the number of peers multiplied by the per-peer reservation [5]. As such, the global memory usage limit scales up with the number of peers we have. Evictions must happen whenever this limit is reached.
  - We introduce a "Peer DoS Score" which is the maximum between its "CPU Score" and "Memory Score." The CPU score is the ratio between the number of orphans announced by this peer / peer announcement limit. The memory score is the total usage of all orphans announced by this peer / peer usage reservation.

  Eviction changes in a few ways:
  - It is triggered if either limit is exceeded.
  - On each iteration of the loop, instead of selecting a random orphan, we select a peer and delete 1 of its announcements. Specifically, we select the peer with the highest DoS score, which is the maximum between its CPU DoS score (based on announcements) and Memory DoS score (based on tx weight). After the peer has been selected, we evict the oldest orphan (non-reconsiderable sorted before reconsiderable).
  - Instead of evicting orphans, we evict announcements. An orphan is still in the orphanage as long as there is 1 peer announcer. Of course, over the course of several iteration loops, we may erase all announcers, thus erasing the orphan itself. The purpose of this change is to prevent a peer from being able to trigger eviction of another peer's orphans.

  This PR also:
  - Reimplements `TxOrphanage` as single multi-index container.
  - Effectively bounds the number of transactions that can be in a peer's work set by ensuring it is a subset of the peer's announcements.
  - Removes the `-maxorphantxs` config option, as the orphanage no longer limits by unique orphans.

  This means we can receive 1p1c packages in the presence of spammy peers. It also makes the orphanage more useful and increases our download capacity without drastically increasing orphanage resource usage.

  [0]: This means the effective memory limit in orphan weight is 100 * 400KWu = 40MWu
  [1]: https://delvingbitcoin.org/t/stats-on-orphanage-overflows/1421
  [2]: Limit is 3000, which is equivalent to one max size ancestor package (24 transactions can be missing inputs) for each peer (default max connections is 125).
  [3]: Orphan weight is used in place of actual memory usage because something like "one maximally sized standard tx" is easier to reason about than "considering the bytes allocated for vin and vout vectors, it needs to be within N bytes..." etc. We can also consider a different formula to encapsulate more the memory overhead but still have an interface that is easy to reason about.
  [4]: The limit is 404KWu, which is the maximum size of an ancestor package.
  [5]: With 125 peers, this is 50.5MWu, which is a small increase from the existing limit of 40MWu. While the actual memory usage limit is higher (this number does not include the other memory used by `TxOrphanage` to store the outpoints map, etc.), this is within the same ballpark as the old limit.

ACKs for top commit:
  marcofleon:
    ReACK 50024620b9
  achow101:
    light ACK 50024620b9
  instagibbs:
    ACK 50024620b9
  theStack:
    Code-review ACK 50024620b9

Tree-SHA512: 270c11a2d116a1bf222358a1b4e25ffd1f01e24da958284fa8c4678bee5547f9e0554e87da7b7d5d5d172ca11da147f54a69b3436cc8f382debb6a45a90647fd
This commit is contained in:
Ava Chow
2025-07-18 13:01:24 -07:00
24 changed files with 2657 additions and 880 deletions

View File

@@ -143,14 +143,14 @@ class InvalidTxRequestTest(BitcoinTestFramework):
self.wait_until(lambda: 1 == len(node.getpeerinfo()), timeout=12) # p2ps[1] is no longer connected
assert_equal(expected_mempool, set(node.getrawmempool()))
self.log.info('Test orphan pool overflow')
self.log.info('Test orphanage can store more than 100 transactions')
orphan_tx_pool = [CTransaction() for _ in range(101)]
for i in range(len(orphan_tx_pool)):
orphan_tx_pool[i].vin.append(CTxIn(outpoint=COutPoint(i, 333)))
orphan_tx_pool[i].vout.append(CTxOut(nValue=11 * COIN, scriptPubKey=SCRIPT_PUB_KEY_OP_TRUE))
with node.assert_debug_log(['orphanage overflow, removed 1 tx']):
node.p2ps[0].send_txs_and_test(orphan_tx_pool, node, success=False)
node.p2ps[0].send_txs_and_test(orphan_tx_pool, node, success=False)
self.wait_until(lambda: len(node.getorphantxs()) >= 101)
self.log.info('Test orphan with rejected parents')
rejected_parent = CTransaction()
@@ -160,8 +160,8 @@ class InvalidTxRequestTest(BitcoinTestFramework):
node.p2ps[0].send_txs_and_test([rejected_parent], node, success=False)
self.log.info('Test that a peer disconnection causes erase its transactions from the orphan pool')
with node.assert_debug_log(['Erased 100 orphan transaction(s) from peer=26']):
self.reconnect_p2p(num_connections=1)
self.reconnect_p2p(num_connections=1)
self.wait_until(lambda: len(node.getorphantxs()) == 0)
self.log.info('Test that a transaction in the orphan pool is included in a new tip block causes erase this transaction from the orphan pool')
tx_withhold_until_block_A = CTransaction()

View File

@@ -7,12 +7,20 @@ Test opportunistic 1p1c package submission logic.
"""
from decimal import Decimal
import random
import time
from test_framework.blocktools import MAX_STANDARD_TX_WEIGHT
from test_framework.mempool_util import (
create_large_orphan,
fill_mempool,
)
from test_framework.messages import (
CInv,
COutPoint,
CTransaction,
CTxIn,
CTxOut,
CTxInWitness,
MAX_BIP125_RBF_SEQUENCE,
MSG_WTX,
@@ -21,12 +29,20 @@ from test_framework.messages import (
tx_from_hex,
)
from test_framework.p2p import (
NONPREF_PEER_TX_DELAY,
P2PInterface,
TXID_RELAY_DELAY,
)
from test_framework.script import (
CScript,
OP_NOP,
OP_RETURN,
)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_greater_than,
assert_greater_than_or_equal,
)
from test_framework.wallet import (
MiniWallet,
@@ -373,6 +389,164 @@ class PackageRelayTest(BitcoinTestFramework):
result_missing_parent = node.submitpackage(package_hex_missing_parent)
assert_equal(result_missing_parent["package_msg"], "package-not-child-with-unconfirmed-parents")
def create_small_orphan(self):
"""Create small orphan transaction"""
tx = CTransaction()
# Nonexistent UTXO
tx.vin = [CTxIn(COutPoint(random.randrange(1 << 256), random.randrange(1, 100)))]
tx.wit.vtxinwit = [CTxInWitness()]
tx.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_NOP] * 5)]
tx.vout = [CTxOut(100, CScript([OP_RETURN, b'a' * 3]))]
return tx
@cleanup
def test_orphanage_dos_large(self):
self.log.info("Test that the node can still resolve orphans when peers use lots of orphanage space")
node = self.nodes[0]
node.setmocktime(int(time.time()))
peer_normal = node.add_p2p_connection(P2PInterface())
peer_doser = node.add_p2p_connection(P2PInterface())
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)]
# 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())
assert_greater_than(MAX_STANDARD_TX_WEIGHT, large_orphan.get_weight())
assert_greater_than_or_equal(3 * large_orphan.get_vsize(), 2 * 100000)
testres = node.testmempoolaccept([large_orphan.serialize().hex()])
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.
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))
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
peer_doser_individual.wait_for_getdata([large_orphan.vin[0].prevout.hash])
# Make sure that these transactions are going through the orphan handling codepaths.
# Subsequent rounds will not wait for getdata because the time mocking will cause the
# normal package request to time out.
self.wait_until(lambda: len(node.getorphantxs()) == num_individual_dosers)
self.log.info("Send an orphan from a non-DoSy peer. Its orphan should not be evicted.")
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=200*FEERATE_1SAT_VB,
target_vsize=100000
)
# Announce
orphan_tx = high_fee_child["tx"]
orphan_inv = CInv(t=MSG_WTX, h=orphan_tx.wtxid_int)
# Wait for getdata
peer_normal.send_and_ping(msg_inv([orphan_inv]))
node.bumpmocktime(NONPREF_PEER_TX_DELAY)
peer_normal.wait_for_getdata([orphan_tx.wtxid_int])
peer_normal.send_and_ping(msg_tx(orphan_tx))
# Wait for parent request
parent_txid_int = int(low_fee_parent["txid"], 16)
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
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:]:
peer_doser.send_and_ping(msg_tx(large_orphan))
# Something was evicted; the orphanage does not contain all large orphans + the 1p1c child
self.wait_until(lambda: len(node.getorphantxs()) < len(large_orphans) + 1)
self.log.info("Provide the orphan's parent. This 1p1c package should be successfully accepted.")
peer_normal.send_and_ping(msg_tx(low_fee_parent["tx"]))
assert_equal(node.getmempoolentry(orphan_tx.txid_hex)["ancestorcount"], 2)
@cleanup
def test_orphanage_dos_many(self):
self.log.info("Test that the node can still resolve orphans when peers are sending tons of orphans")
node = self.nodes[0]
node.setmocktime(int(time.time()))
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.
batch_size = 51
num_peers_shared = 60
num_peers_unique = 40
# 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)")
peer_doser_shared = [node.add_p2p_connection(P2PInterface()) for _ in range(num_peers_shared)]
for i in range(num_peers_shared):
for orphan in shared_orphans:
peer_doser_shared[i].send_without_ping(msg_tx(orphan))
# We sync peers to make sure we have processed as many orphans as possible. Ensure at least
# one of the orphans was processed.
for peer_doser in peer_doser_shared:
peer_doser.sync_with_ping()
self.wait_until(lambda: any([tx.txid_hex in node.getorphantxs() for tx in shared_orphans]))
self.log.info("Send an orphan from a non-DoSy peer. Its orphan should not be evicted.")
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=200*FEERATE_1SAT_VB,
)
# Announce
orphan_tx = high_fee_child["tx"]
orphan_inv = CInv(t=MSG_WTX, h=orphan_tx.wtxid_int)
# Wait for getdata
peer_normal.send_and_ping(msg_inv([orphan_inv]))
node.bumpmocktime(NONPREF_PEER_TX_DELAY)
peer_normal.wait_for_getdata([orphan_tx.wtxid_int])
peer_normal.send_and_ping(msg_tx(orphan_tx))
# Orphan has been entered and evicted something else
self.wait_until(lambda: high_fee_child["txid"] in node.getorphantxs())
# Wait for parent request
parent_txid_int = low_fee_parent["tx"].txid_int
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 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]))
self.log.info("Check that orphan from normal peer still exists in orphanage")
assert high_fee_child["txid"] in node.getorphantxs()
self.log.info("Provide the orphan's parent. This 1p1c package should be successfully accepted.")
peer_normal.send_and_ping(msg_tx(low_fee_parent["tx"]))
assert orphan_tx.txid_hex in node.getrawmempool()
assert_equal(node.getmempoolentry(orphan_tx.txid_hex)["ancestorcount"], 2)
def run_test(self):
node = self.nodes[0]
# To avoid creating transactions with the same txid (can happen if we set the same feerate
@@ -407,6 +581,9 @@ class PackageRelayTest(BitcoinTestFramework):
self.test_multiple_parents()
self.test_other_parent_in_mempool()
self.test_orphanage_dos_large()
self.test_orphanage_dos_many()
if __name__ == '__main__':
PackageRelayTest(__file__).main()

View File

@@ -5,10 +5,14 @@
import time
from test_framework.mempool_util import tx_in_orphanage
from test_framework.mempool_util import (
create_large_orphan,
tx_in_orphanage,
)
from test_framework.messages import (
CInv,
CTxInWitness,
DEFAULT_ANCESTOR_LIMIT,
MSG_TX,
MSG_WITNESS_TX,
MSG_WTX,
@@ -43,14 +47,7 @@ from test_framework.wallet import (
# for one peer and y seconds for another, use specific values instead.
TXREQUEST_TIME_SKIP = NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY + OVERLOADED_PEER_TX_DELAY + 1
DEFAULT_MAX_ORPHAN_TRANSACTIONS = 100
def cleanup(func):
# Time to fastfoward (using setmocktime) in between subtests to ensure they do not interfere with
# one another, in seconds. Equal to 12 hours, which is enough to expire anything that may exist
# (though nothing should since state should be cleared) in p2p data structures.
LONG_TIME_SKIP = 12 * 60 * 60
def wrapper(self):
try:
func(self)
@@ -58,10 +55,13 @@ def cleanup(func):
# Clear mempool
self.generate(self.nodes[0], 1)
self.nodes[0].disconnect_p2ps()
self.nodes[0].bumpmocktime(LONG_TIME_SKIP)
# Check that mempool and orphanage have been cleared
self.wait_until(lambda: len(self.nodes[0].getorphantxs()) == 0)
assert_equal(0, len(self.nodes[0].getrawmempool()))
self.restart_node(0, extra_args=["-persistmempool=0"])
# Allow use of bumpmocktime again
self.nodes[0].setmocktime(int(time.time()))
self.wallet.rescan_utxos(include_mempool=True)
return wrapper
@@ -593,46 +593,6 @@ class OrphanHandlingTest(BitcoinTestFramework):
assert_equal(node.getmempoolentry(tx_child["txid"])["wtxid"], tx_child["wtxid"])
self.wait_until(lambda: len(node.getorphantxs()) == 0)
@cleanup
def test_max_orphan_amount(self):
self.log.info("Check that we never exceed our storage limits for orphans")
node = self.nodes[0]
self.generate(self.wallet, 1)
peer_1 = node.add_p2p_connection(P2PInterface())
self.log.info("Check that orphanage is empty on start of test")
assert len(node.getorphantxs()) == 0
self.log.info("Filling up orphanage with " + str(DEFAULT_MAX_ORPHAN_TRANSACTIONS) + "(DEFAULT_MAX_ORPHAN_TRANSACTIONS) orphans")
orphans = []
parent_orphans = []
for _ in range(DEFAULT_MAX_ORPHAN_TRANSACTIONS):
tx_parent_1 = self.wallet.create_self_transfer()
tx_child_1 = self.wallet.create_self_transfer(utxo_to_spend=tx_parent_1["new_utxo"])
parent_orphans.append(tx_parent_1["tx"])
orphans.append(tx_child_1["tx"])
peer_1.send_without_ping(msg_tx(tx_child_1["tx"]))
peer_1.sync_with_ping()
orphanage = node.getorphantxs()
self.wait_until(lambda: len(node.getorphantxs()) == DEFAULT_MAX_ORPHAN_TRANSACTIONS)
for orphan in orphans:
assert tx_in_orphanage(node, orphan)
self.log.info("Check that we do not add more than the max orphan amount")
tx_parent_1 = self.wallet.create_self_transfer()
tx_child_1 = self.wallet.create_self_transfer(utxo_to_spend=tx_parent_1["new_utxo"])
peer_1.send_and_ping(msg_tx(tx_child_1["tx"]))
parent_orphans.append(tx_parent_1["tx"])
orphanage = node.getorphantxs()
assert_equal(len(orphanage), DEFAULT_MAX_ORPHAN_TRANSACTIONS)
self.log.info("Clearing the orphanage")
for index, parent_orphan in enumerate(parent_orphans):
peer_1.send_and_ping(msg_tx(parent_orphan))
self.wait_until(lambda: len(node.getorphantxs()) == 0)
@cleanup
def test_orphan_handling_prefer_outbound(self):
@@ -671,6 +631,72 @@ class OrphanHandlingTest(BitcoinTestFramework):
peer_inbound.sync_with_ping()
peer_inbound.wait_for_parent_requests([parent_tx.txid_int])
@cleanup
def test_maximal_package_protected(self):
self.log.info("Test that a node only announcing a maximally sized ancestor package is protected in orphanage")
self.nodes[0].setmocktime(int(time.time()))
node = self.nodes[0]
peer_normal = node.add_p2p_connection(P2PInterface())
peer_doser = node.add_p2p_connection(P2PInterface())
# Each of the num_peers peers creates a distinct set of orphans
large_orphans = [create_large_orphan() for _ in range(60)]
# Check to make sure these are orphans, within max standard size (to be accepted into the orphanage)
for large_orphan in large_orphans:
testres = node.testmempoolaccept([large_orphan.serialize().hex()])
assert not testres[0]["allowed"]
assert_equal(testres[0]["reject-reason"], "missing-inputs")
num_individual_dosers = 20
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.
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))
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY + 1)
peer_doser_individual.wait_for_getdata([large_orphan.vin[0].prevout.hash])
# Make sure that these transactions are going through the orphan handling codepaths.
# Subsequent rounds will not wait for getdata because the time mocking will cause the
# normal package request to time out.
self.wait_until(lambda: len(node.getorphantxs()) == num_individual_dosers)
# Now honest peer will send a maximally sized ancestor package of 24 orphans chaining
# off of a single missing transaction, with a total vsize 404,000Wu
ancestor_package = self.wallet.create_self_transfer_chain(chain_length=DEFAULT_ANCESTOR_LIMIT - 1)
sum_ancestor_package_vsize = sum([tx["tx"].get_vsize() for tx in ancestor_package])
final_tx = self.wallet.create_self_transfer(utxo_to_spend=ancestor_package[-1]["new_utxo"], target_vsize=101000 - sum_ancestor_package_vsize)
ancestor_package.append(final_tx)
# Peer sends all but first tx to fill up orphange with their orphans
for orphan in ancestor_package[1:]:
peer_normal.send_and_ping(msg_tx(orphan["tx"]))
orphan_set = node.getorphantxs()
for orphan in ancestor_package[1:]:
assert orphan["txid"] in orphan_set
# Wait for ultimate parent request (the root ancestor transaction)
parent_txid_int = ancestor_package[0]["tx"].txid_int
node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
self.wait_until(lambda: "getdata" in peer_normal.last_message and parent_txid_int in [inv.hash for inv in peer_normal.last_message.get("getdata").inv])
self.log.info("Send another round of very large orphans from a DoSy peer")
for large_orphan in large_orphans[num_individual_dosers:]:
peer_doser.send_and_ping(msg_tx(large_orphan))
self.log.info("Provide the top ancestor. The whole package should be re-evaluated after enough time.")
peer_normal.send_and_ping(msg_tx(ancestor_package[0]["tx"]))
# Wait until all transactions have been processed. When the last tx is accepted, it's
# guaranteed to have all ancestors.
self.wait_until(lambda: node.getmempoolentry(final_tx["txid"])["ancestorcount"] == DEFAULT_ANCESTOR_LIMIT)
@cleanup
def test_announcers_before_and_after(self):
self.log.info("Test that the node uses all peers who announced the tx prior to realizing it's an orphan")
@@ -820,10 +846,10 @@ class OrphanHandlingTest(BitcoinTestFramework):
self.test_same_txid_orphan()
self.test_same_txid_orphan_of_orphan()
self.test_orphan_txid_inv()
self.test_max_orphan_amount()
self.test_orphan_handling_prefer_outbound()
self.test_announcers_before_and_after()
self.test_parents_change()
self.test_maximal_package_protected()
if __name__ == '__main__':

View File

@@ -4,10 +4,7 @@
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Tests for orphan related RPCs."""
import time
from test_framework.mempool_util import (
ORPHAN_TX_EXPIRE_TIME,
tx_in_orphanage,
)
from test_framework.messages import (
@@ -101,8 +98,6 @@ class OrphanRPCsTest(BitcoinTestFramework):
tx_child_2 = self.wallet.create_self_transfer(utxo_to_spend=tx_parent_2["new_utxo"])
peer_1 = node.add_p2p_connection(P2PInterface())
peer_2 = node.add_p2p_connection(P2PInterface())
entry_time = int(time.time())
node.setmocktime(entry_time)
peer_1.send_and_ping(msg_tx(tx_child_1["tx"]))
peer_2.send_and_ping(msg_tx(tx_child_2["tx"]))
@@ -128,9 +123,6 @@ class OrphanRPCsTest(BitcoinTestFramework):
assert_equal(len(node.getorphantxs()), 1)
orphan_1 = orphanage[0]
self.orphan_details_match(orphan_1, tx_child_1, verbosity=1)
self.log.info("Checking orphan entry/expiration times")
assert_equal(orphan_1["entry"], entry_time)
assert_equal(orphan_1["expiration"], entry_time + ORPHAN_TX_EXPIRE_TIME)
self.log.info("Checking orphan details (verbosity 2)")
orphanage = node.getorphantxs(verbosity=2)

View File

@@ -4,11 +4,22 @@
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Helpful routines for mempool testing."""
from decimal import Decimal
import random
from .blocktools import (
COINBASE_MATURITY,
)
from .messages import CTransaction
from .messages import (
COutPoint,
CTransaction,
CTxIn,
CTxInWitness,
CTxOut,
)
from .script import (
CScript,
OP_RETURN,
)
from .util import (
assert_equal,
assert_greater_than,
@@ -19,8 +30,6 @@ from .wallet import (
MiniWallet,
)
ORPHAN_TX_EXPIRE_TIME = 1200
def assert_mempool_contents(test_framework, node, expected=None, sync=True):
"""Assert that all transactions in expected are in the mempool,
and no additional ones exist. 'expected' is an array of
@@ -106,3 +115,13 @@ def tx_in_orphanage(node, tx: CTransaction) -> bool:
"""Returns true if the transaction is in the orphanage."""
found = [o for o in node.getorphantxs(verbosity=1) if o["txid"] == tx.txid_hex and o["wtxid"] == tx.wtxid_hex]
return len(found) == 1
def create_large_orphan():
"""Create huge orphan transaction"""
tx = CTransaction()
# Nonexistent UTXO
tx.vin = [CTxIn(COutPoint(random.randrange(1 << 256), random.randrange(1, 100)))]
tx.wit.vtxinwit = [CTxInWitness()]
tx.wit.vtxinwit[0].scriptWitness.stack = [CScript(b'X' * 390000)]
tx.vout = [CTxOut(100, CScript([OP_RETURN, b'a' * 20]))]
return tx