diff options
Diffstat (limited to 'test')
-rwxr-xr-x | test/functional/feature_rbf.py | 96 | ||||
-rwxr-xr-x | test/functional/p2p_getaddr_caching.py | 66 | ||||
-rw-r--r-- | test/functional/test_framework/wallet.py | 14 | ||||
-rwxr-xr-x | test/lint/lint-format-strings.py | 1 |
4 files changed, 160 insertions, 17 deletions
diff --git a/test/functional/feature_rbf.py b/test/functional/feature_rbf.py index f0ed914461..a8492bd6eb 100755 --- a/test/functional/feature_rbf.py +++ b/test/functional/feature_rbf.py @@ -32,7 +32,7 @@ from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE MAX_REPLACEMENT_LIMIT = 100 class ReplaceByFeeTest(BitcoinTestFramework): def set_test_params(self): - self.num_nodes = 1 + self.num_nodes = 2 self.extra_args = [ [ "-acceptnonstdtxn=1", @@ -42,6 +42,9 @@ class ReplaceByFeeTest(BitcoinTestFramework): "-limitdescendantcount=200", "-limitdescendantsize=101", ], + # second node has default mempool parameters + [ + ], ] self.supports_cli = False @@ -73,6 +76,9 @@ class ReplaceByFeeTest(BitcoinTestFramework): self.log.info("Running test too many replacements...") self.test_too_many_replacements() + self.log.info("Running test too many replacements using default mempool params...") + self.test_too_many_replacements_with_default_mempool_params() + self.log.info("Running test opt-in...") self.test_opt_in() @@ -397,6 +403,94 @@ class ReplaceByFeeTest(BitcoinTestFramework): double_tx_hex = double_tx.serialize().hex() self.nodes[0].sendrawtransaction(double_tx_hex, 0) + def test_too_many_replacements_with_default_mempool_params(self): + """ + Test rule 5 of BIP125 (do not allow replacements that cause more than 100 + evictions) without having to rely on non-default mempool parameters. + + In order to do this, create a number of "root" UTXOs, and then hang + enough transactions off of each root UTXO to exceed the MAX_REPLACEMENT_LIMIT. + Then create a conflicting RBF replacement transaction. + """ + normal_node = self.nodes[1] + wallet = MiniWallet(normal_node) + wallet.rescan_utxos() + # Clear mempools to avoid cross-node sync failure. + for node in self.nodes: + self.generate(node, 1) + + # This has to be chosen so that the total number of transactions can exceed + # MAX_REPLACEMENT_LIMIT without having any one tx graph run into the descendant + # limit; 10 works. + num_tx_graphs = 10 + + # (Number of transactions per graph, BIP125 rule 5 failure expected) + cases = [ + # Test the base case of evicting fewer than MAX_REPLACEMENT_LIMIT + # transactions. + ((MAX_REPLACEMENT_LIMIT // num_tx_graphs) - 1, False), + + # Test hitting the rule 5 eviction limit. + (MAX_REPLACEMENT_LIMIT // num_tx_graphs, True), + ] + + for (txs_per_graph, failure_expected) in cases: + self.log.debug(f"txs_per_graph: {txs_per_graph}, failure: {failure_expected}") + # "Root" utxos of each txn graph that we will attempt to double-spend with + # an RBF replacement. + root_utxos = [] + + # For each root UTXO, create a package that contains the spend of that + # UTXO and `txs_per_graph` children tx. + for graph_num in range(num_tx_graphs): + root_utxos.append(wallet.get_utxo()) + + optin_parent_tx = wallet.send_self_transfer_multi( + from_node=normal_node, + sequence=BIP125_SEQUENCE_NUMBER, + utxos_to_spend=[root_utxos[graph_num]], + num_outputs=txs_per_graph, + ) + assert_equal(True, normal_node.getmempoolentry(optin_parent_tx['txid'])['bip125-replaceable']) + new_utxos = optin_parent_tx['new_utxos'] + + for utxo in new_utxos: + # Create spends for each output from the "root" of this graph. + child_tx = wallet.send_self_transfer( + from_node=normal_node, + utxo_to_spend=utxo, + ) + + assert normal_node.getmempoolentry(child_tx['txid']) + + num_txs_invalidated = len(root_utxos) + (num_tx_graphs * txs_per_graph) + + if failure_expected: + assert num_txs_invalidated > MAX_REPLACEMENT_LIMIT + else: + assert num_txs_invalidated <= MAX_REPLACEMENT_LIMIT + + # Now attempt to submit a tx that double-spends all the root tx inputs, which + # would invalidate `num_txs_invalidated` transactions. + double_tx = wallet.create_self_transfer_multi( + from_node=normal_node, + utxos_to_spend=root_utxos, + fee_per_output=10_000_000, # absurdly high feerate + ) + tx_hex = double_tx.serialize().hex() + + if failure_expected: + assert_raises_rpc_error( + -26, "too many potential replacements", normal_node.sendrawtransaction, tx_hex, 0) + else: + txid = normal_node.sendrawtransaction(tx_hex, 0) + assert normal_node.getmempoolentry(txid) + + # Clear the mempool once finished, and rescan the other nodes' wallet + # to account for the spends we've made on `normal_node`. + self.generate(normal_node, 1) + self.wallet.rescan_utxos() + def test_opt_in(self): """Replacing should only work if orig tx opted in""" tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) diff --git a/test/functional/p2p_getaddr_caching.py b/test/functional/p2p_getaddr_caching.py index d375af6fe1..c934a97729 100755 --- a/test/functional/p2p_getaddr_caching.py +++ b/test/functional/p2p_getaddr_caching.py @@ -14,6 +14,8 @@ from test_framework.p2p import ( from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + PORT_MIN, + PORT_RANGE, ) # As defined in net_processing. @@ -42,6 +44,13 @@ class AddrReceiver(P2PInterface): class AddrTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 + # Start onion ports after p2p and rpc ports. + port = PORT_MIN + 2 * PORT_RANGE + self.onion_port1 = port + self.onion_port2 = port + 1 + self.extra_args = [ + [f"-bind=127.0.0.1:{self.onion_port1}=onion", f"-bind=127.0.0.1:{self.onion_port2}=onion"], + ] def run_test(self): self.log.info('Fill peer AddrMan with a lot of records') @@ -55,35 +64,66 @@ class AddrTest(BitcoinTestFramework): # only a fraction of all known addresses can be cached and returned. assert(len(self.nodes[0].getnodeaddresses(0)) > int(MAX_ADDR_TO_SEND / (MAX_PCT_ADDR_TO_SEND / 100))) - responses = [] + last_response_on_local_bind = None + last_response_on_onion_bind1 = None + last_response_on_onion_bind2 = None self.log.info('Send many addr requests within short time to receive same response') N = 5 cur_mock_time = int(time.time()) for i in range(N): - addr_receiver = self.nodes[0].add_p2p_connection(AddrReceiver()) - addr_receiver.send_and_ping(msg_getaddr()) + addr_receiver_local = self.nodes[0].add_p2p_connection(AddrReceiver()) + addr_receiver_local.send_and_ping(msg_getaddr()) + addr_receiver_onion1 = self.nodes[0].add_p2p_connection(AddrReceiver(), dstport=self.onion_port1) + addr_receiver_onion1.send_and_ping(msg_getaddr()) + addr_receiver_onion2 = self.nodes[0].add_p2p_connection(AddrReceiver(), dstport=self.onion_port2) + addr_receiver_onion2.send_and_ping(msg_getaddr()) + # Trigger response cur_mock_time += 5 * 60 self.nodes[0].setmocktime(cur_mock_time) - addr_receiver.wait_until(addr_receiver.addr_received) - responses.append(addr_receiver.get_received_addrs()) - for response in responses[1:]: - assert_equal(response, responses[0]) - assert(len(response) == MAX_ADDR_TO_SEND) + addr_receiver_local.wait_until(addr_receiver_local.addr_received) + addr_receiver_onion1.wait_until(addr_receiver_onion1.addr_received) + addr_receiver_onion2.wait_until(addr_receiver_onion2.addr_received) + + if i > 0: + # Responses from different binds should be unique + assert(last_response_on_local_bind != addr_receiver_onion1.get_received_addrs()) + assert(last_response_on_local_bind != addr_receiver_onion2.get_received_addrs()) + assert(last_response_on_onion_bind1 != addr_receiver_onion2.get_received_addrs()) + # Responses on from the same bind should be the same + assert_equal(last_response_on_local_bind, addr_receiver_local.get_received_addrs()) + assert_equal(last_response_on_onion_bind1, addr_receiver_onion1.get_received_addrs()) + assert_equal(last_response_on_onion_bind2, addr_receiver_onion2.get_received_addrs()) + + last_response_on_local_bind = addr_receiver_local.get_received_addrs() + last_response_on_onion_bind1 = addr_receiver_onion1.get_received_addrs() + last_response_on_onion_bind2 = addr_receiver_onion2.get_received_addrs() + + for response in [last_response_on_local_bind, last_response_on_onion_bind1, last_response_on_onion_bind2]: + assert_equal(len(response), MAX_ADDR_TO_SEND) cur_mock_time += 3 * 24 * 60 * 60 self.nodes[0].setmocktime(cur_mock_time) self.log.info('After time passed, see a new response to addr request') - last_addr_receiver = self.nodes[0].add_p2p_connection(AddrReceiver()) - last_addr_receiver.send_and_ping(msg_getaddr()) + addr_receiver_local = self.nodes[0].add_p2p_connection(AddrReceiver()) + addr_receiver_local.send_and_ping(msg_getaddr()) + addr_receiver_onion1 = self.nodes[0].add_p2p_connection(AddrReceiver(), dstport=self.onion_port1) + addr_receiver_onion1.send_and_ping(msg_getaddr()) + addr_receiver_onion2 = self.nodes[0].add_p2p_connection(AddrReceiver(), dstport=self.onion_port2) + addr_receiver_onion2.send_and_ping(msg_getaddr()) + # Trigger response cur_mock_time += 5 * 60 self.nodes[0].setmocktime(cur_mock_time) - last_addr_receiver.wait_until(last_addr_receiver.addr_received) - # new response is different - assert(set(responses[0]) != set(last_addr_receiver.get_received_addrs())) + addr_receiver_local.wait_until(addr_receiver_local.addr_received) + addr_receiver_onion1.wait_until(addr_receiver_onion1.addr_received) + addr_receiver_onion2.wait_until(addr_receiver_onion2.addr_received) + # new response is different + assert(set(last_response_on_local_bind) != set(addr_receiver_local.get_received_addrs())) + assert(set(last_response_on_onion_bind1) != set(addr_receiver_onion1.get_received_addrs())) + assert(set(last_response_on_onion_bind2) != set(addr_receiver_onion2.get_received_addrs())) if __name__ == '__main__': AddrTest().main() diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py index 336b2afe26..e43dd9f61a 100644 --- a/test/functional/test_framework/wallet.py +++ b/test/functional/test_framework/wallet.py @@ -10,6 +10,7 @@ from enum import Enum from random import choice from typing import ( Any, + List, Optional, ) from test_framework.address import ( @@ -147,7 +148,7 @@ class MiniWallet: def get_address(self): return self._address - def get_utxo(self, *, txid: str = '', vout: Optional[int] = None, mark_as_spent=True): + def get_utxo(self, *, txid: str = '', vout: Optional[int] = None, mark_as_spent=True) -> dict: """ Returns a utxo and marks it as spent (pops it from the internal list) @@ -215,14 +216,21 @@ class MiniWallet: return {'new_utxos': [self.get_utxo(txid=txid, vout=vout) for vout in range(len(tx.vout))], 'txid': txid, 'hex': tx.serialize().hex(), 'tx': tx} - def create_self_transfer_multi(self, *, from_node, utxos_to_spend=None, num_outputs=1, fee_per_output=1000): + def create_self_transfer_multi( + self, *, from_node, + utxos_to_spend: Optional[List[dict]] = None, + num_outputs=1, + sequence=0, + fee_per_output=1000): """ Create and return a transaction that spends the given UTXOs and creates a certain number of outputs with equal amounts. """ utxos_to_spend = utxos_to_spend or [self.get_utxo()] # create simple tx template (1 input, 1 output) - tx = self.create_self_transfer(fee_rate=0, from_node=from_node, utxo_to_spend=utxos_to_spend[0], mempool_valid=False)['tx'] + tx = self.create_self_transfer( + fee_rate=0, from_node=from_node, + utxo_to_spend=utxos_to_spend[0], sequence=sequence, mempool_valid=False)['tx'] # duplicate inputs, witnesses and outputs tx.vin = [deepcopy(tx.vin[0]) for _ in range(len(utxos_to_spend))] diff --git a/test/lint/lint-format-strings.py b/test/lint/lint-format-strings.py index 28e7b1e4ff..412cf86791 100755 --- a/test/lint/lint-format-strings.py +++ b/test/lint/lint-format-strings.py @@ -22,6 +22,7 @@ FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS = [ 'LogConnectFailure,1', 'LogPrint,1', 'LogPrintf,0', + 'LogPrintLevel,2', 'printf,0', 'snprintf,2', 'sprintf,1', |