aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xtest/functional/p2p_orphan_handling.py416
-rwxr-xr-xtest/functional/test_framework/p2p.py6
-rwxr-xr-xtest/functional/test_framework/test_node.py18
-rwxr-xr-xtest/functional/test_runner.py1
4 files changed, 441 insertions, 0 deletions
diff --git a/test/functional/p2p_orphan_handling.py b/test/functional/p2p_orphan_handling.py
new file mode 100755
index 0000000000..6166c62aa2
--- /dev/null
+++ b/test/functional/p2p_orphan_handling.py
@@ -0,0 +1,416 @@
+#!/usr/bin/env python3
+# Copyright (c) 2023 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+import time
+
+from test_framework.messages import (
+ CInv,
+ MSG_TX,
+ MSG_WITNESS_TX,
+ MSG_WTX,
+ msg_getdata,
+ msg_inv,
+ msg_notfound,
+ msg_tx,
+ tx_from_hex,
+)
+from test_framework.p2p import (
+ GETDATA_TX_INTERVAL,
+ NONPREF_PEER_TX_DELAY,
+ OVERLOADED_PEER_TX_DELAY,
+ p2p_lock,
+ P2PTxInvStore,
+ TXID_RELAY_DELAY,
+)
+from test_framework.util import (
+ assert_equal,
+)
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.wallet import (
+ MiniWallet,
+ MiniWalletMode,
+)
+
+# Time to bump forward (using setmocktime) before waiting for the node to send getdata(tx) in response
+# to an inv(tx), in seconds. This delay includes all possible delays + 1, so it should only be used
+# when the value of the delay is not interesting. If we want to test that the node waits x seconds
+# 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
+
+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)
+ finally:
+ # Clear mempool
+ self.generate(self.nodes[0], 1)
+ self.nodes[0].disconnect_p2ps()
+ self.nodes[0].bumpmocktime(LONG_TIME_SKIP)
+ return wrapper
+
+class PeerTxRelayer(P2PTxInvStore):
+ """A P2PTxInvStore that also remembers all of the getdata and tx messages it receives."""
+ def __init__(self):
+ super().__init__()
+ self._tx_received = []
+ self._getdata_received = []
+
+ @property
+ def tx_received(self):
+ with p2p_lock:
+ return self._tx_received
+
+ @property
+ def getdata_received(self):
+ with p2p_lock:
+ return self._getdata_received
+
+ def on_tx(self, message):
+ self._tx_received.append(message)
+
+ def on_getdata(self, message):
+ self._getdata_received.append(message)
+
+ def wait_for_parent_requests(self, txids):
+ """Wait for requests for missing parents by txid with witness data (MSG_WITNESS_TX or
+ WitnessTx). Requires that the getdata message match these txids exactly; all txids must be
+ requested and no additional requests are allowed."""
+ def test_function():
+ last_getdata = self.last_message.get('getdata')
+ if not last_getdata:
+ return False
+ return len(last_getdata.inv) == len(txids) and all([item.type == MSG_WITNESS_TX and item.hash in txids for item in last_getdata.inv])
+ self.wait_until(test_function, timeout=10)
+
+ def assert_no_immediate_response(self, message):
+ """Check that the node does not immediately respond to this message with any of getdata,
+ inv, tx. The node may respond later.
+ """
+ prev_lastmessage = self.last_message
+ self.send_and_ping(message)
+ after_lastmessage = self.last_message
+ for msgtype in ["getdata", "inv", "tx"]:
+ if msgtype not in prev_lastmessage:
+ assert msgtype not in after_lastmessage
+ else:
+ assert_equal(prev_lastmessage[msgtype], after_lastmessage[msgtype])
+
+ def assert_never_requested(self, txhash):
+ """Check that the node has never sent us a getdata for this hash (int type)"""
+ for getdata in self.getdata_received:
+ for request in getdata.inv:
+ assert request.hash != txhash
+
+class OrphanHandlingTest(BitcoinTestFramework):
+ def set_test_params(self):
+ self.num_nodes = 1
+ self.extra_args = [[]]
+
+ def create_parent_and_child(self):
+ """Create package with 1 parent and 1 child, normal fees (no cpfp)."""
+ parent = self.wallet.create_self_transfer()
+ child = self.wallet.create_self_transfer(utxo_to_spend=parent['new_utxo'])
+ return child["tx"].getwtxid(), child["tx"], parent["tx"]
+
+ def relay_transaction(self, peer, tx):
+ """Relay transaction using MSG_WTX"""
+ wtxid = int(tx.getwtxid(), 16)
+ peer.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=wtxid)]))
+ self.nodes[0].bumpmocktime(TXREQUEST_TIME_SKIP)
+ peer.wait_for_getdata([wtxid])
+ peer.send_and_ping(msg_tx(tx))
+
+ @cleanup
+ def test_arrival_timing_orphan(self):
+ self.log.info("Test missing parents that arrive during delay are not requested")
+ node = self.nodes[0]
+ tx_parent_arrives = self.wallet.create_self_transfer()
+ tx_parent_doesnt_arrive = self.wallet.create_self_transfer()
+ # Fake orphan spends nonexistent outputs of the two parents
+ tx_fake_orphan = self.wallet.create_self_transfer_multi(utxos_to_spend=[
+ {"txid": tx_parent_doesnt_arrive["txid"], "vout": 10, "value": tx_parent_doesnt_arrive["new_utxo"]["value"]},
+ {"txid": tx_parent_arrives["txid"], "vout": 10, "value": tx_parent_arrives["new_utxo"]["value"]}
+ ])
+
+ peer_spy = node.add_p2p_connection(PeerTxRelayer())
+ peer_normal = node.add_p2p_connection(PeerTxRelayer())
+ # This transaction is an orphan because it is missing inputs. It is a "fake" orphan that the
+ # spy peer has crafted to learn information about tx_parent_arrives even though it isn't
+ # able to spend a real output of it, but it could also just be a normal, real child tx.
+ # The node should not immediately respond with a request for orphan parents.
+ # Also, no request should be sent later because it will be resolved by
+ # the time the request is scheduled to be sent.
+ peer_spy.assert_no_immediate_response(msg_tx(tx_fake_orphan["tx"]))
+
+ # Node receives transaction. It attempts to obfuscate the exact timing at which this
+ # transaction entered its mempool. Send unsolicited because otherwise we need to wait for
+ # request delays.
+ peer_normal.send_and_ping(msg_tx(tx_parent_arrives["tx"]))
+ assert tx_parent_arrives["txid"] in node.getrawmempool()
+
+ # Spy peer should not be able to query the node for the parent yet, since it hasn't been
+ # announced / insufficient time has elapsed.
+ parent_inv = CInv(t=MSG_WTX, h=int(tx_parent_arrives["tx"].getwtxid(), 16))
+ assert_equal(len(peer_spy.get_invs()), 0)
+ peer_spy.assert_no_immediate_response(msg_getdata([parent_inv]))
+
+ # Request would be scheduled with this delay because it is not a preferred relay peer.
+ self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY)
+ peer_spy.assert_never_requested(int(tx_parent_arrives["txid"], 16))
+ peer_spy.assert_never_requested(int(tx_parent_doesnt_arrive["txid"], 16))
+ # Request would be scheduled with this delay because it is by txid.
+ self.nodes[0].bumpmocktime(TXID_RELAY_DELAY)
+ peer_spy.wait_for_parent_requests([int(tx_parent_doesnt_arrive["txid"], 16)])
+ peer_spy.assert_never_requested(int(tx_parent_arrives["txid"], 16))
+
+ @cleanup
+ def test_orphan_rejected_parents_exceptions(self):
+ node = self.nodes[0]
+ peer1 = node.add_p2p_connection(PeerTxRelayer())
+ peer2 = node.add_p2p_connection(PeerTxRelayer())
+
+ self.log.info("Test orphan handling when a nonsegwit parent is known to be invalid")
+ parent_low_fee_nonsegwit = self.wallet_nonsegwit.create_self_transfer(fee_rate=0)
+ assert_equal(parent_low_fee_nonsegwit["txid"], parent_low_fee_nonsegwit["tx"].getwtxid())
+ parent_other = self.wallet_nonsegwit.create_self_transfer()
+ child_nonsegwit = self.wallet_nonsegwit.create_self_transfer_multi(
+ utxos_to_spend=[parent_other["new_utxo"], parent_low_fee_nonsegwit["new_utxo"]])
+
+ # Relay the parent. It should be rejected because it pays 0 fees.
+ self.relay_transaction(peer1, parent_low_fee_nonsegwit["tx"])
+ assert parent_low_fee_nonsegwit["txid"] not in node.getrawmempool()
+
+ # Relay the child. It should not be accepted because it has missing inputs.
+ # Its parent should not be requested because its hash (txid == wtxid) has been added to the rejection filter.
+ with node.assert_debug_log(['not keeping orphan with rejected parents {}'.format(child_nonsegwit["txid"])]):
+ self.relay_transaction(peer2, child_nonsegwit["tx"])
+ assert child_nonsegwit["txid"] not in node.getrawmempool()
+
+ # No parents are requested.
+ self.nodes[0].bumpmocktime(GETDATA_TX_INTERVAL)
+ peer1.assert_never_requested(int(parent_other["txid"], 16))
+ peer2.assert_never_requested(int(parent_other["txid"], 16))
+ peer2.assert_never_requested(int(parent_low_fee_nonsegwit["txid"], 16))
+
+ self.log.info("Test orphan handling when a segwit parent was invalid but may be retried with another witness")
+ parent_low_fee = self.wallet.create_self_transfer(fee_rate=0)
+ child_low_fee = self.wallet.create_self_transfer(utxo_to_spend=parent_low_fee["new_utxo"])
+
+ # Relay the low fee parent. It should not be accepted.
+ self.relay_transaction(peer1, parent_low_fee["tx"])
+ assert parent_low_fee["txid"] not in node.getrawmempool()
+
+ # Relay the child. It should not be accepted because it has missing inputs.
+ self.relay_transaction(peer2, child_low_fee["tx"])
+ assert child_low_fee["txid"] not in node.getrawmempool()
+
+ # The parent should be requested because even though the txid commits to the fee, it doesn't
+ # commit to the feerate. Delayed because it's by txid and this is not a preferred relay peer.
+ self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
+ peer2.wait_for_getdata([int(parent_low_fee["tx"].rehash(), 16)])
+
+ self.log.info("Test orphan handling when a parent was previously downloaded with witness stripped")
+ parent_normal = self.wallet.create_self_transfer()
+ parent1_witness_stripped = tx_from_hex(parent_normal["tx"].serialize_without_witness().hex())
+ child_invalid_witness = self.wallet.create_self_transfer(utxo_to_spend=parent_normal["new_utxo"])
+
+ # Relay the parent with witness stripped. It should not be accepted.
+ self.relay_transaction(peer1, parent1_witness_stripped)
+ assert_equal(parent_normal["txid"], parent1_witness_stripped.rehash())
+ assert parent1_witness_stripped.rehash() not in node.getrawmempool()
+
+ # Relay the child. It should not be accepted because it has missing inputs.
+ self.relay_transaction(peer2, child_invalid_witness["tx"])
+ assert child_invalid_witness["txid"] not in node.getrawmempool()
+
+ # The parent should be requested since the unstripped wtxid would differ. Delayed because
+ # it's by txid and this is not a preferred relay peer.
+ self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
+ peer2.wait_for_getdata([int(parent_normal["tx"].rehash(), 16)])
+
+ # parent_normal can be relayed again even though parent1_witness_stripped was rejected
+ self.relay_transaction(peer1, parent_normal["tx"])
+ assert_equal(set(node.getrawmempool()), set([parent_normal["txid"], child_invalid_witness["txid"]]))
+
+ @cleanup
+ def test_orphan_multiple_parents(self):
+ node = self.nodes[0]
+ peer = node.add_p2p_connection(PeerTxRelayer())
+
+ self.log.info("Test orphan parent requests with a mixture of confirmed, in-mempool and missing parents")
+ # This UTXO confirmed a long time ago.
+ utxo_conf_old = self.wallet.send_self_transfer(from_node=node)["new_utxo"]
+ txid_conf_old = utxo_conf_old["txid"]
+ self.generate(self.wallet, 10)
+
+ # Create a fake reorg to trigger BlockDisconnected, which resets the rolling bloom filter.
+ # The alternative is to mine thousands of transactions to push it out of the filter.
+ last_block = node.getbestblockhash()
+ node.invalidateblock(last_block)
+ node.preciousblock(last_block)
+ node.syncwithvalidationinterfacequeue()
+
+ # This UTXO confirmed recently.
+ utxo_conf_recent = self.wallet.send_self_transfer(from_node=node)["new_utxo"]
+ self.generate(node, 1)
+
+ # This UTXO is unconfirmed and in the mempool.
+ assert_equal(len(node.getrawmempool()), 0)
+ mempool_tx = self.wallet.send_self_transfer(from_node=node)
+ utxo_unconf_mempool = mempool_tx["new_utxo"]
+
+ # This UTXO is unconfirmed and missing.
+ missing_tx = self.wallet.create_self_transfer()
+ utxo_unconf_missing = missing_tx["new_utxo"]
+ assert missing_tx["txid"] not in node.getrawmempool()
+
+ orphan = self.wallet.create_self_transfer_multi(utxos_to_spend=[utxo_conf_old,
+ utxo_conf_recent, utxo_unconf_mempool, utxo_unconf_missing])
+
+ self.relay_transaction(peer, orphan["tx"])
+ self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
+ peer.sync_with_ping()
+ assert_equal(len(peer.last_message["getdata"].inv), 2)
+ peer.wait_for_parent_requests([int(txid_conf_old, 16), int(missing_tx["txid"], 16)])
+
+ # Even though the peer would send a notfound for the "old" confirmed transaction, the node
+ # doesn't give up on the orphan. Once all of the missing parents are received, it should be
+ # submitted to mempool.
+ peer.send_message(msg_notfound(vec=[CInv(MSG_WITNESS_TX, int(txid_conf_old, 16))]))
+ peer.send_and_ping(msg_tx(missing_tx["tx"]))
+ peer.sync_with_ping()
+ assert_equal(node.getmempoolentry(orphan["txid"])["ancestorcount"], 3)
+
+ @cleanup
+ def test_orphans_overlapping_parents(self):
+ node = self.nodes[0]
+ # In the process of relaying inflight_parent_AB
+ peer_txrequest = node.add_p2p_connection(PeerTxRelayer())
+ # Sends the orphans
+ peer_orphans = node.add_p2p_connection(PeerTxRelayer())
+
+ confirmed_utxos = [self.wallet_nonsegwit.get_utxo() for _ in range(4)]
+ assert all([utxo["confirmations"] > 0 for utxo in confirmed_utxos])
+ self.log.info("Test handling of multiple orphans with missing parents that are already being requested")
+ # Parent of child_A only
+ missing_parent_A = self.wallet_nonsegwit.create_self_transfer(utxo_to_spend=confirmed_utxos[0])
+ # Parents of child_A and child_B
+ missing_parent_AB = self.wallet_nonsegwit.create_self_transfer(utxo_to_spend=confirmed_utxos[1])
+ inflight_parent_AB = self.wallet_nonsegwit.create_self_transfer(utxo_to_spend=confirmed_utxos[2])
+ # Parent of child_B only
+ missing_parent_B = self.wallet_nonsegwit.create_self_transfer(utxo_to_spend=confirmed_utxos[3])
+ child_A = self.wallet_nonsegwit.create_self_transfer_multi(
+ utxos_to_spend=[missing_parent_A["new_utxo"], missing_parent_AB["new_utxo"], inflight_parent_AB["new_utxo"]]
+ )
+ child_B = self.wallet_nonsegwit.create_self_transfer_multi(
+ utxos_to_spend=[missing_parent_B["new_utxo"], missing_parent_AB["new_utxo"], inflight_parent_AB["new_utxo"]]
+ )
+
+ # The wtxid and txid need to be the same for the node to recognize that the missing input
+ # and in-flight request for inflight_parent_AB are the same transaction.
+ assert_equal(inflight_parent_AB["txid"], inflight_parent_AB["tx"].getwtxid())
+
+ # Announce inflight_parent_AB and wait for getdata
+ peer_txrequest.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=int(inflight_parent_AB["tx"].getwtxid(), 16))]))
+ self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY)
+ peer_txrequest.wait_for_getdata([int(inflight_parent_AB["tx"].getwtxid(), 16)])
+
+ self.log.info("Test that the node does not request a parent if it has an in-flight txrequest")
+ # Relay orphan child_A
+ self.relay_transaction(peer_orphans, child_A["tx"])
+ self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
+ # There are 3 missing parents. missing_parent_A and missing_parent_AB should be requested.
+ # But inflight_parent_AB should not, because there is already an in-flight request for it.
+ peer_orphans.wait_for_parent_requests([int(missing_parent_A["txid"], 16), int(missing_parent_AB["txid"], 16)])
+
+ self.log.info("Test that the node does not request a parent if it has an in-flight orphan parent request")
+ # Relay orphan child_B
+ self.relay_transaction(peer_orphans, child_B["tx"])
+ self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
+ # Only missing_parent_B should be requested. Not inflight_parent_AB or missing_parent_AB
+ # because they are already being requested from peer_txrequest and peer_orphans respectively.
+ peer_orphans.wait_for_parent_requests([int(missing_parent_B["txid"], 16)])
+ peer_orphans.assert_never_requested(int(inflight_parent_AB["txid"], 16))
+
+ @cleanup
+ def test_orphan_of_orphan(self):
+ node = self.nodes[0]
+ peer = node.add_p2p_connection(PeerTxRelayer())
+
+ self.log.info("Test handling of an orphan with a parent who is another orphan")
+ missing_grandparent = self.wallet_nonsegwit.create_self_transfer()
+ missing_parent_orphan = self.wallet_nonsegwit.create_self_transfer(utxo_to_spend=missing_grandparent["new_utxo"])
+ missing_parent = self.wallet_nonsegwit.create_self_transfer()
+ orphan = self.wallet_nonsegwit.create_self_transfer_multi(utxos_to_spend=[missing_parent["new_utxo"], missing_parent_orphan["new_utxo"]])
+
+ # The node should put missing_parent_orphan into the orphanage and request missing_grandparent
+ self.relay_transaction(peer, missing_parent_orphan["tx"])
+ self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
+ peer.wait_for_parent_requests([int(missing_grandparent["txid"], 16)])
+
+ # The node should put the orphan into the orphanage and request missing_parent, skipping
+ # missing_parent_orphan because it already has it in the orphanage.
+ self.relay_transaction(peer, orphan["tx"])
+ self.nodes[0].bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)
+ peer.wait_for_parent_requests([int(missing_parent["txid"], 16)])
+
+ @cleanup
+ def test_orphan_inherit_rejection(self):
+ node = self.nodes[0]
+ peer1 = node.add_p2p_connection(PeerTxRelayer())
+ peer2 = node.add_p2p_connection(PeerTxRelayer())
+ peer3 = node.add_p2p_connection(PeerTxRelayer())
+
+ self.log.info("Test that an orphan with rejected parents, along with any descendants, cannot be retried with an alternate witness")
+ parent_low_fee_nonsegwit = self.wallet_nonsegwit.create_self_transfer(fee_rate=0)
+ assert_equal(parent_low_fee_nonsegwit["txid"], parent_low_fee_nonsegwit["tx"].getwtxid())
+ child = self.wallet.create_self_transfer(utxo_to_spend=parent_low_fee_nonsegwit["new_utxo"])
+ grandchild = self.wallet.create_self_transfer(utxo_to_spend=child["new_utxo"])
+ assert child["txid"] != child["tx"].getwtxid()
+ assert grandchild["txid"] != grandchild["tx"].getwtxid()
+
+ # Relay the parent. It should be rejected because it pays 0 fees.
+ self.relay_transaction(peer1, parent_low_fee_nonsegwit["tx"])
+
+ # Relay the child. It should be rejected for having missing parents, and this rejection is
+ # cached by txid and wtxid.
+ with node.assert_debug_log(['not keeping orphan with rejected parents {}'.format(child["txid"])]):
+ self.relay_transaction(peer1, child["tx"])
+ assert_equal(0, len(node.getrawmempool()))
+ peer1.assert_never_requested(parent_low_fee_nonsegwit["txid"])
+
+ # Grandchild should also not be kept in orphanage because its parent has been rejected.
+ with node.assert_debug_log(['not keeping orphan with rejected parents {}'.format(grandchild["txid"])]):
+ self.relay_transaction(peer2, grandchild["tx"])
+ assert_equal(0, len(node.getrawmempool()))
+ peer2.assert_never_requested(child["txid"])
+ peer2.assert_never_requested(child["tx"].getwtxid())
+
+ # The child should never be requested, even if announced again with potentially different witness.
+ peer3.send_and_ping(msg_inv([CInv(t=MSG_TX, h=int(child["txid"], 16))]))
+ self.nodes[0].bumpmocktime(TXREQUEST_TIME_SKIP)
+ peer3.assert_never_requested(child["txid"])
+
+ def run_test(self):
+ self.nodes[0].setmocktime(int(time.time()))
+ self.wallet_nonsegwit = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_P2PK)
+ self.generate(self.wallet_nonsegwit, 10)
+ self.wallet = MiniWallet(self.nodes[0])
+ self.generate(self.wallet, 160)
+ self.test_arrival_timing_orphan()
+ self.test_orphan_rejected_parents_exceptions()
+ self.test_orphan_multiple_parents()
+ self.test_orphans_overlapping_parents()
+ self.test_orphan_of_orphan()
+ self.test_orphan_inherit_rejection()
+
+
+if __name__ == '__main__':
+ OrphanHandlingTest().main()
diff --git a/test/functional/test_framework/p2p.py b/test/functional/test_framework/p2p.py
index 2433e52671..d9c7f9f390 100755
--- a/test/functional/test_framework/p2p.py
+++ b/test/functional/test_framework/p2p.py
@@ -95,6 +95,12 @@ P2P_SUBVERSION = "/python-p2p-tester:0.0.3/"
P2P_VERSION_RELAY = 1
# Delay after receiving a tx inv before requesting transactions from non-preferred peers, in seconds
NONPREF_PEER_TX_DELAY = 2
+# Delay for requesting transactions via txids if we have wtxid-relaying peers, in seconds
+TXID_RELAY_DELAY = 2
+# Delay for requesting transactions if the peer has MAX_PEER_TX_REQUEST_IN_FLIGHT or more requests
+OVERLOADED_PEER_TX_DELAY = 2
+# How long to wait before downloading a transaction from an additional peer
+GETDATA_TX_INTERVAL = 60
MESSAGEMAP = {
b"addr": msg_addr,
diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py
index 544a81602e..6e12f6c964 100755
--- a/test/functional/test_framework/test_node.py
+++ b/test/functional/test_framework/test_node.py
@@ -144,6 +144,8 @@ class TestNode():
self.p2ps = []
self.timeout_factor = timeout_factor
+ self.mocktime = None
+
AddressKeyPair = collections.namedtuple('AddressKeyPair', ['address', 'key'])
PRIV_KEYS = [
# address , privkey
@@ -324,6 +326,15 @@ class TestNode():
assert not invalid_call
return self.__getattr__('generatetodescriptor')(*args, **kwargs)
+ def setmocktime(self, timestamp):
+ """Wrapper for setmocktime RPC, sets self.mocktime"""
+ if timestamp == 0:
+ # setmocktime(0) resets to system time.
+ self.mocktime = None
+ else:
+ self.mocktime = timestamp
+ return self.__getattr__('setmocktime')(timestamp)
+
def get_wallet_rpc(self, wallet_name):
if self.use_cli:
return RPCOverloadWrapper(self.cli("-rpcwallet={}".format(wallet_name)), True, self.descriptors)
@@ -704,6 +715,13 @@ class TestNode():
wait_until_helper(lambda: self.num_test_p2p_connections() == 0, timeout_factor=self.timeout_factor)
+ def bumpmocktime(self, seconds):
+ """Fast forward using setmocktime to self.mocktime + seconds. Requires setmocktime to have
+ been called at some point in the past."""
+ assert self.mocktime
+ self.mocktime += seconds
+ self.setmocktime(self.mocktime)
+
class TestNodeCLIAttr:
def __init__(self, cli, command):
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index d93e6fd6da..0c19a9ff93 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -123,6 +123,7 @@ BASE_SCRIPTS = [
'feature_abortnode.py',
'wallet_address_types.py --legacy-wallet',
'wallet_address_types.py --descriptors',
+ 'p2p_orphan_handling.py',
'wallet_basic.py --legacy-wallet',
'wallet_basic.py --descriptors',
'feature_maxtipage.py',