diff options
Diffstat (limited to 'test/functional')
28 files changed, 349 insertions, 97 deletions
diff --git a/test/functional/feature_asmap.py b/test/functional/feature_asmap.py index 9cff8042a8..ae483fe449 100755 --- a/test/functional/feature_asmap.py +++ b/test/functional/feature_asmap.py @@ -111,6 +111,14 @@ class AsmapTest(BitcoinTestFramework): self.node.assert_start_raises_init_error(extra_args=['-asmap'], expected_msg=msg) os.remove(self.default_asmap) + def test_asmap_health_check(self): + self.log.info('Test bitcoind -asmap logs ASMap Health Check with basic stats') + shutil.copyfile(self.asmap_raw, self.default_asmap) + msg = "ASMap Health Check: 2 clearnet peers are mapped to 1 ASNs with 0 peers being unmapped" + with self.node.assert_debug_log(expected_msgs=[msg]): + self.start_node(0, extra_args=['-asmap']) + os.remove(self.default_asmap) + def run_test(self): self.node = self.nodes[0] self.datadir = self.node.chain_path @@ -124,6 +132,7 @@ class AsmapTest(BitcoinTestFramework): self.test_asmap_interaction_with_addrman_containing_entries() self.test_default_asmap_with_missing_file() self.test_empty_asmap() + self.test_asmap_health_check() if __name__ == '__main__': diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index d1232c5133..2e3589b020 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -75,7 +75,7 @@ class AssumeutxoTest(BitcoinTestFramework): with self.nodes[1].assert_debug_log([log_msg]): assert_raises_rpc_error(-32603, f"Unable to load UTXO snapshot{rpc_details}", self.nodes[1].loadtxoutset, bad_snapshot_path) - self.log.info(" - snapshot file refering to a block that is not in the assumeutxo parameters") + self.log.info(" - snapshot file referring to a block that is not in the assumeutxo parameters") prev_block_hash = self.nodes[0].getblockhash(SNAPSHOT_BASE_HEIGHT - 1) bogus_block_hash = "0" * 64 # Represents any unknown block hash for bad_block_hash in [bogus_block_hash, prev_block_hash]: @@ -112,7 +112,7 @@ class AssumeutxoTest(BitcoinTestFramework): def test_invalid_chainstate_scenarios(self): self.log.info("Test different scenarios of invalid snapshot chainstate in datadir") - self.log.info(" - snapshot chainstate refering to a block that is not in the assumeutxo parameters") + self.log.info(" - snapshot chainstate referring to a block that is not in the assumeutxo parameters") self.stop_node(0) chainstate_snapshot_path = self.nodes[0].chain_path / "chainstate_snapshot" chainstate_snapshot_path.mkdir() diff --git a/test/functional/feature_filelock.py b/test/functional/feature_filelock.py index 24a68a04bf..567207915e 100755 --- a/test/functional/feature_filelock.py +++ b/test/functional/feature_filelock.py @@ -30,6 +30,12 @@ class FilelockTest(BitcoinTestFramework): expected_msg = f"Error: Cannot obtain a lock on data directory {datadir}. {self.config['environment']['PACKAGE_NAME']} is probably already running." self.nodes[1].assert_start_raises_init_error(extra_args=[f'-datadir={self.nodes[0].datadir_path}', '-noserver'], expected_msg=expected_msg) + self.log.info("Check that cookie and PID file are not deleted when attempting to start a second bitcoind using the same datadir") + cookie_file = datadir / ".cookie" + assert cookie_file.exists() # should not be deleted during the second bitcoind instance shutdown + pid_file = datadir / "bitcoind.pid" + assert pid_file.exists() + if self.is_wallet_compiled(): def check_wallet_filelock(descriptors): wallet_name = ''.join([random.choice(string.ascii_lowercase) for _ in range(6)]) diff --git a/test/functional/feature_segwit.py b/test/functional/feature_segwit.py index 6c467fa613..4dc19222c4 100755 --- a/test/functional/feature_segwit.py +++ b/test/functional/feature_segwit.py @@ -88,14 +88,11 @@ class SegWitTest(BitcoinTestFramework): self.extra_args = [ [ "-acceptnonstdtxn=1", - "-rpcserialversion=0", - "-deprecatedrpc=serialversion", "-testactivationheight=segwit@165", "-addresstype=legacy", ], [ "-acceptnonstdtxn=1", - "-rpcserialversion=1", "-testactivationheight=segwit@165", "-addresstype=legacy", ], @@ -224,18 +221,6 @@ class SegWitTest(BitcoinTestFramework): self.fail_accept(self.nodes[0], "mandatory-script-verify-flag-failed (Witness program hash mismatch)", p2sh_ids[NODE_0][P2WPKH][0], sign=False, redeem_script=witness_script(False, self.pubkey[0])) self.fail_accept(self.nodes[0], "mandatory-script-verify-flag-failed (Witness program was passed an empty witness)", p2sh_ids[NODE_0][P2WSH][0], sign=False, redeem_script=witness_script(True, self.pubkey[0])) - self.log.info("Verify block and transaction serialization rpcs return differing serializations depending on rpc serialization flag") - assert self.nodes[2].getblock(blockhash, False) != self.nodes[0].getblock(blockhash, False) - assert self.nodes[1].getblock(blockhash, False) == self.nodes[2].getblock(blockhash, False) - - for tx_id in segwit_tx_list: - tx = tx_from_hex(self.nodes[2].gettransaction(tx_id)["hex"]) - assert self.nodes[2].getrawtransaction(tx_id, False, blockhash) != self.nodes[0].getrawtransaction(tx_id, False, blockhash) - assert self.nodes[1].getrawtransaction(tx_id, False, blockhash) == self.nodes[2].getrawtransaction(tx_id, False, blockhash) - assert self.nodes[0].getrawtransaction(tx_id, False, blockhash) != self.nodes[2].gettransaction(tx_id)["hex"] - assert self.nodes[1].getrawtransaction(tx_id, False, blockhash) == self.nodes[2].gettransaction(tx_id)["hex"] - assert self.nodes[0].getrawtransaction(tx_id, False, blockhash) == tx.serialize_without_witness().hex() - # Coinbase contains the witness commitment nonce, check that RPC shows us coinbase_txid = self.nodes[2].getblock(blockhash)['tx'][0] coinbase_tx = self.nodes[2].gettransaction(txid=coinbase_txid, verbose=True) @@ -276,9 +261,6 @@ class SegWitTest(BitcoinTestFramework): # tx3 (non-segwit input, paying to a non-segwit output). # tx1 is allowed to appear in the block, but no others. txid1 = send_to_witness(1, self.nodes[0], find_spendable_utxo(self.nodes[0], 50), self.pubkey[0], False, Decimal("49.996")) - hex_tx = self.nodes[0].gettransaction(txid)['hex'] - tx = tx_from_hex(hex_tx) - assert tx.wit.is_null() # This should not be a segwit input assert txid1 in self.nodes[0].getrawmempool() tx1_hex = self.nodes[0].gettransaction(txid1)['hex'] @@ -613,11 +595,6 @@ class SegWitTest(BitcoinTestFramework): assert_equal(self.nodes[1].gettransaction(txid, True)["txid"], txid) assert_equal(self.nodes[1].listtransactions("*", 1, 0, True)[0]["txid"], txid) - self.log.info('Test negative and unknown rpcserialversion throw an init error') - self.stop_node(0) - self.nodes[0].assert_start_raises_init_error(["-rpcserialversion=-1"], "Error: rpcserialversion must be non-negative.") - self.nodes[0].assert_start_raises_init_error(["-rpcserialversion=100"], "Error: Unknown rpcserialversion requested.") - def mine_and_test_listunspent(self, script_list, ismine): utxo = find_spendable_utxo(self.nodes[0], 50) tx = CTransaction() diff --git a/test/functional/interface_rest.py b/test/functional/interface_rest.py index c0679c5ba9..b81eae2506 100755 --- a/test/functional/interface_rest.py +++ b/test/functional/interface_rest.py @@ -65,7 +65,7 @@ class RESTTest (BitcoinTestFramework): body: str = '', status: int = 200, ret_type: RetType = RetType.JSON, - query_params: Optional[typing.Dict[str, typing.Any]] = None, + query_params: Optional[dict[str, typing.Any]] = None, ) -> typing.Union[http.client.HTTPResponse, bytes, str, None]: rest_uri = '/rest' + uri if req_type in ReqType: diff --git a/test/functional/mempool_dust.py b/test/functional/mempool_dust.py index f4e385a112..e0c026207a 100755 --- a/test/functional/mempool_dust.py +++ b/test/functional/mempool_dust.py @@ -40,6 +40,7 @@ DUST_RELAY_TX_FEE = 3000 # default setting [sat/kvB] class DustRelayFeeTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 + self.extra_args = [['-permitbaremultisig']] def test_dust_output(self, node: TestNode, dust_relay_fee: Decimal, output_script: CScript, type_desc: str) -> None: @@ -101,7 +102,7 @@ class DustRelayFeeTest(BitcoinTestFramework): else: dust_parameter = f"-dustrelayfee={dustfee_btc_kvb:.8f}" self.log.info(f"Test dust limit setting {dust_parameter} ({dustfee_sat_kvb} sat/kvB)...") - self.restart_node(0, extra_args=[dust_parameter]) + self.restart_node(0, extra_args=[dust_parameter, "-permitbaremultisig"]) for output_script, description in output_scripts: self.test_dust_output(self.nodes[0], dustfee_btc_kvb, output_script, description) diff --git a/test/functional/mempool_limit.py b/test/functional/mempool_limit.py index a1147f70f3..6215610c31 100755 --- a/test/functional/mempool_limit.py +++ b/test/functional/mempool_limit.py @@ -125,8 +125,9 @@ class MempoolLimitTest(BitcoinTestFramework): utxo_to_spend=tx_B["new_utxo"], confirmed_only=True ) - - assert_raises_rpc_error(-26, "too-long-mempool-chain", node.submitpackage, [tx_B["hex"], tx_C["hex"]]) + res = node.submitpackage([tx_B["hex"], tx_C["hex"]]) + assert_equal(res["package_msg"], "transaction failed") + assert "too-long-mempool-chain" in res["tx-results"][tx_C["wtxid"]]["error"] def test_mid_package_eviction(self): node = self.nodes[0] @@ -205,7 +206,7 @@ class MempoolLimitTest(BitcoinTestFramework): # Package should be submitted, temporarily exceeding maxmempool, and then evicted. with node.assert_debug_log(expected_msgs=["rolling minimum fee bumped"]): - assert_raises_rpc_error(-26, "mempool full", node.submitpackage, package_hex) + assert_equal(node.submitpackage(package_hex)["package_msg"], "transaction failed") # Maximum size must never be exceeded. assert_greater_than(node.getmempoolinfo()["maxmempool"], node.getmempoolinfo()["bytes"]) @@ -273,7 +274,9 @@ class MempoolLimitTest(BitcoinTestFramework): package_hex = [cpfp_parent["hex"], replacement_tx["hex"], child["hex"]] # Package should be submitted, temporarily exceeding maxmempool, and then evicted. - assert_raises_rpc_error(-26, "bad-txns-inputs-missingorspent", node.submitpackage, package_hex) + res = node.submitpackage(package_hex) + assert_equal(res["package_msg"], "transaction failed") + assert len([tx_res for _, tx_res in res["tx-results"].items() if "error" in tx_res and tx_res["error"] == "bad-txns-inputs-missingorspent"]) # Maximum size must never be exceeded. assert_greater_than(node.getmempoolinfo()["maxmempool"], node.getmempoolinfo()["bytes"]) @@ -321,6 +324,7 @@ class MempoolLimitTest(BitcoinTestFramework): package_txns.append(tx_child) submitpackage_result = node.submitpackage([tx["hex"] for tx in package_txns]) + assert_equal(submitpackage_result["package_msg"], "success") rich_parent_result = submitpackage_result["tx-results"][tx_rich["wtxid"]] poor_parent_result = submitpackage_result["tx-results"][tx_poor["wtxid"]] @@ -366,7 +370,9 @@ class MempoolLimitTest(BitcoinTestFramework): assert_greater_than(worst_feerate_btcvb, (parent_fee + child_fee) / (tx_parent_just_below["tx"].get_vsize() + tx_child_just_above["tx"].get_vsize())) assert_greater_than(mempoolmin_feerate, (parent_fee) / (tx_parent_just_below["tx"].get_vsize())) assert_greater_than((parent_fee + child_fee) / (tx_parent_just_below["tx"].get_vsize() + tx_child_just_above["tx"].get_vsize()), mempoolmin_feerate / 1000) - assert_raises_rpc_error(-26, "mempool full", node.submitpackage, [tx_parent_just_below["hex"], tx_child_just_above["hex"]]) + res = node.submitpackage([tx_parent_just_below["hex"], tx_child_just_above["hex"]]) + for wtxid in [tx_parent_just_below["wtxid"], tx_child_just_above["wtxid"]]: + assert_equal(res["tx-results"][wtxid]["error"], "mempool full") self.log.info('Test passing a value below the minimum (5 MB) to -maxmempool throws an error') self.stop_node(0) diff --git a/test/functional/mempool_sigoplimit.py b/test/functional/mempool_sigoplimit.py index fbec6d0dc8..2e7850fb40 100755 --- a/test/functional/mempool_sigoplimit.py +++ b/test/functional/mempool_sigoplimit.py @@ -34,7 +34,6 @@ from test_framework.util import ( assert_equal, assert_greater_than, assert_greater_than_or_equal, - assert_raises_rpc_error, ) from test_framework.wallet import MiniWallet from test_framework.wallet_util import generate_keypair @@ -140,7 +139,7 @@ class BytesPerSigOpTest(BitcoinTestFramework): self.log.info("Test a overly-large sigops-vbyte hits package limits") # Make a 2-transaction package which fails vbyte checks even though # separately they would work. - self.restart_node(0, extra_args=["-bytespersigop=5000"] + self.extra_args[0]) + self.restart_node(0, extra_args=["-bytespersigop=5000","-permitbaremultisig=1"] + self.extra_args[0]) def create_bare_multisig_tx(utxo_to_spend=None): _, pubkey = generate_keypair() @@ -169,7 +168,8 @@ class BytesPerSigOpTest(BitcoinTestFramework): assert_equal([x["package-error"] for x in packet_test], ["package-mempool-limits", "package-mempool-limits"]) # When we actually try to submit, the parent makes it into the mempool, but the child would exceed ancestor vsize limits - assert_raises_rpc_error(-26, "too-long-mempool-chain", self.nodes[0].submitpackage, [tx_parent.serialize().hex(), tx_child.serialize().hex()]) + res = self.nodes[0].submitpackage([tx_parent.serialize().hex(), tx_child.serialize().hex()]) + assert "too-long-mempool-chain" in res["tx-results"][tx_child.getwtxid()]["error"] assert tx_parent.rehash() in self.nodes[0].getrawmempool() # Transactions are tiny in weight diff --git a/test/functional/p2p_filter.py b/test/functional/p2p_filter.py index 665f57365f..62d55cc101 100755 --- a/test/functional/p2p_filter.py +++ b/test/functional/p2p_filter.py @@ -11,6 +11,7 @@ from test_framework.messages import ( COIN, MAX_BLOOM_FILTER_SIZE, MAX_BLOOM_HASH_FUNCS, + MSG_WTX, MSG_BLOCK, MSG_FILTERED_BLOCK, msg_filteradd, @@ -135,14 +136,22 @@ class FilterTest(BitcoinTestFramework): self.log.info("Check that a node with bloom filters enabled services p2p mempool messages") filter_peer = P2PBloomFilter() - self.log.debug("Create a tx relevant to the peer before connecting") - txid = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=filter_peer.watch_script_pubkey, amount=9 * COIN)["txid"] + self.log.info("Create two tx before connecting, one relevant to the node another that is not") + rel_txid = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=filter_peer.watch_script_pubkey, amount=1 * COIN)["txid"] + irr_result = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=getnewdestination()[1], amount=2 * COIN) + irr_txid = irr_result["txid"] + irr_wtxid = irr_result["wtxid"] - self.log.debug("Send a mempool msg after connecting and check that the tx is received") + self.log.info("Send a mempool msg after connecting and check that the relevant tx is announced") self.nodes[0].add_p2p_connection(filter_peer) filter_peer.send_and_ping(filter_peer.watch_filter_init) filter_peer.send_message(msg_mempool()) - filter_peer.wait_for_tx(txid) + filter_peer.wait_for_tx(rel_txid) + + self.log.info("Request the irrelevant transaction even though it was not announced") + filter_peer.send_message(msg_getdata([CInv(t=MSG_WTX, h=int(irr_wtxid, 16))])) + self.log.info("We should get it anyway because it was in the mempool on connection to peer") + filter_peer.wait_for_tx(irr_txid) def test_frelay_false(self, filter_peer): self.log.info("Check that a node with fRelay set to false does not receive invs until the filter is set") diff --git a/test/functional/p2p_node_network_limited.py b/test/functional/p2p_node_network_limited.py index a56afbcf7b..89c35e943b 100755 --- a/test/functional/p2p_node_network_limited.py +++ b/test/functional/p2p_node_network_limited.py @@ -8,7 +8,15 @@ Tests that a node configured with -prune=550 signals NODE_NETWORK_LIMITED correc and that it responds to getdata requests for blocks correctly: - send a block within 288 + 2 of the tip - disconnect peers who request blocks older than that.""" -from test_framework.messages import CInv, MSG_BLOCK, msg_getdata, msg_verack, NODE_NETWORK_LIMITED, NODE_WITNESS +from test_framework.messages import ( + CInv, + MSG_BLOCK, + NODE_NETWORK_LIMITED, + NODE_P2P_V2, + NODE_WITNESS, + msg_getdata, + msg_verack, +) from test_framework.p2p import P2PInterface from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( @@ -50,6 +58,8 @@ class NodeNetworkLimitedTest(BitcoinTestFramework): node = self.nodes[0].add_p2p_connection(P2PIgnoreInv()) expected_services = NODE_WITNESS | NODE_NETWORK_LIMITED + if self.options.v2transport: + expected_services |= NODE_P2P_V2 self.log.info("Check that node has signalled expected services.") assert_equal(node.nServices, expected_services) diff --git a/test/functional/p2p_timeouts.py b/test/functional/p2p_timeouts.py index a308577c02..b4fa5099d8 100755 --- a/test/functional/p2p_timeouts.py +++ b/test/functional/p2p_timeouts.py @@ -68,11 +68,14 @@ class TimeoutsTest(BitcoinTestFramework): with self.nodes[0].assert_debug_log(['Unsupported message "ping" prior to verack from peer=0']): no_verack_node.send_message(msg_ping()) - with self.nodes[0].assert_debug_log(['non-version message before version handshake. Message "ping" from peer=1']): - no_version_node.send_message(msg_ping()) - self.mock_forward(1) + # With v2, non-version messages before the handshake would be interpreted as part of the key exchange. + # Therefore, don't execute this part of the test if v2transport is chosen. + if not self.options.v2transport: + with self.nodes[0].assert_debug_log(['non-version message before version handshake. Message "ping" from peer=1']): + no_version_node.send_message(msg_ping()) + self.mock_forward(1) assert "version" in no_verack_node.last_message assert no_verack_node.is_connected @@ -80,11 +83,12 @@ class TimeoutsTest(BitcoinTestFramework): assert no_send_node.is_connected no_verack_node.send_message(msg_ping()) - no_version_node.send_message(msg_ping()) + if not self.options.v2transport: + no_version_node.send_message(msg_ping()) expected_timeout_logs = [ "version handshake timeout peer=0", - "socket no message in first 3 seconds, 1 0 peer=1", + f"socket no message in first 3 seconds, {'0' if self.options.v2transport else '1'} 0 peer=1", "socket no message in first 3 seconds, 0 0 peer=2", ] @@ -100,5 +104,6 @@ class TimeoutsTest(BitcoinTestFramework): extra_args=['-peertimeout=0'], ) + if __name__ == '__main__': TimeoutsTest().main() diff --git a/test/functional/p2p_v2_transport.py b/test/functional/p2p_v2_transport.py index 1a3b4a6d0a..72d22cb77f 100755 --- a/test/functional/p2p_v2_transport.py +++ b/test/functional/p2p_v2_transport.py @@ -133,9 +133,8 @@ class V2TransportTest(BitcoinTestFramework): V1_PREFIX = MAGIC_BYTES["regtest"] + b"version\x00\x00\x00\x00\x00" assert_equal(len(V1_PREFIX), 16) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - num_peers = len(self.nodes[0].getpeerinfo()) - s.connect(("127.0.0.1", p2p_port(0))) - self.wait_until(lambda: len(self.nodes[0].getpeerinfo()) == num_peers + 1) + with self.nodes[0].wait_for_new_peer(): + s.connect(("127.0.0.1", p2p_port(0))) s.sendall(V1_PREFIX[:-1]) assert_equal(self.nodes[0].getpeerinfo()[-1]["transport_protocol_type"], "detecting") s.sendall(bytes([V1_PREFIX[-1]])) # send out last prefix byte @@ -144,22 +143,23 @@ class V2TransportTest(BitcoinTestFramework): # Check wrong network prefix detection (hits if the next 12 bytes correspond to a v1 version message) wrong_network_magic_prefix = MAGIC_BYTES["signet"] + V1_PREFIX[4:] with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.connect(("127.0.0.1", p2p_port(0))) + with self.nodes[0].wait_for_new_peer(): + s.connect(("127.0.0.1", p2p_port(0))) with self.nodes[0].assert_debug_log(["V2 transport error: V1 peer with wrong MessageStart"]): s.sendall(wrong_network_magic_prefix + b"somepayload") # Check detection of missing garbage terminator (hits after fixed amount of data if terminator never matches garbage) MAX_KEY_GARB_AND_GARBTERM_LEN = 64 + 4095 + 16 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - num_peers = len(self.nodes[0].getpeerinfo()) - s.connect(("127.0.0.1", p2p_port(0))) - self.wait_until(lambda: len(self.nodes[0].getpeerinfo()) == num_peers + 1) + with self.nodes[0].wait_for_new_peer(): + s.connect(("127.0.0.1", p2p_port(0))) s.sendall(b'\x00' * (MAX_KEY_GARB_AND_GARBTERM_LEN - 1)) self.wait_until(lambda: self.nodes[0].getpeerinfo()[-1]["bytesrecv"] == MAX_KEY_GARB_AND_GARBTERM_LEN - 1) with self.nodes[0].assert_debug_log(["V2 transport error: missing garbage terminator"]): + peer_id = self.nodes[0].getpeerinfo()[-1]["id"] s.sendall(b'\x00') # send out last byte # should disconnect immediately - self.wait_until(lambda: len(self.nodes[0].getpeerinfo()) == num_peers) + self.wait_until(lambda: not peer_id in [p["id"] for p in self.nodes[0].getpeerinfo()]) if __name__ == '__main__': diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index 8eb9f3aeb1..9b7743cafa 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -437,7 +437,6 @@ class BlockchainTest(BitcoinTestFramework): def _test_getnetworkhashps(self): self.log.info("Test getnetworkhashps") - hashes_per_second = self.nodes[0].getnetworkhashps() assert_raises_rpc_error( -3, textwrap.dedent(""" @@ -449,17 +448,46 @@ class BlockchainTest(BitcoinTestFramework): """).strip(), lambda: self.nodes[0].getnetworkhashps("a", []), ) + assert_raises_rpc_error( + -8, + "Block does not exist at specified height", + lambda: self.nodes[0].getnetworkhashps(100, self.nodes[0].getblockcount() + 1), + ) + assert_raises_rpc_error( + -8, + "Block does not exist at specified height", + lambda: self.nodes[0].getnetworkhashps(100, -10), + ) + assert_raises_rpc_error( + -8, + "Invalid nblocks. Must be a positive number or -1.", + lambda: self.nodes[0].getnetworkhashps(-100), + ) + assert_raises_rpc_error( + -8, + "Invalid nblocks. Must be a positive number or -1.", + lambda: self.nodes[0].getnetworkhashps(0), + ) + + # Genesis block height estimate should return 0 + hashes_per_second = self.nodes[0].getnetworkhashps(100, 0) + assert_equal(hashes_per_second, 0) + # This should be 2 hashes every 10 minutes or 1/300 + hashes_per_second = self.nodes[0].getnetworkhashps() assert abs(hashes_per_second * 300 - 1) < 0.0001 - # Test setting the first param of getnetworkhashps to negative value returns the average network + # Test setting the first param of getnetworkhashps to -1 returns the average network # hashes per second from the last difficulty change. current_block_height = self.nodes[0].getmininginfo()['blocks'] blocks_since_last_diff_change = current_block_height % DIFFICULTY_ADJUSTMENT_INTERVAL + 1 expected_hashes_per_second_since_diff_change = self.nodes[0].getnetworkhashps(blocks_since_last_diff_change) assert_equal(self.nodes[0].getnetworkhashps(-1), expected_hashes_per_second_since_diff_change) - assert_equal(self.nodes[0].getnetworkhashps(-2), expected_hashes_per_second_since_diff_change) + + # Ensure long lookups get truncated to chain length + hashes_per_second = self.nodes[0].getnetworkhashps(self.nodes[0].getblockcount() + 1000) + assert hashes_per_second > 0.003 def _test_stopatheight(self): self.log.info("Test stopping at height") diff --git a/test/functional/rpc_net.py b/test/functional/rpc_net.py index 773ab3b50e..e1820b0f55 100755 --- a/test/functional/rpc_net.py +++ b/test/functional/rpc_net.py @@ -9,6 +9,7 @@ Tests correspond to code in rpc/net.cpp. from decimal import Decimal from itertools import product +import platform import time import test_framework.messages @@ -110,7 +111,7 @@ class NetTest(BitcoinTestFramework): no_version_peer_id = 2 no_version_peer_conntime = int(time.time()) self.nodes[0].setmocktime(no_version_peer_conntime) - with self.nodes[0].assert_debug_log([f"Added connection peer={no_version_peer_id}"]): + with self.nodes[0].wait_for_new_peer(): no_version_peer = self.nodes[0].add_p2p_connection(P2PInterface(), send_version=False, wait_for_verack=False) self.nodes[0].setmocktime(0) peer_info = self.nodes[0].getpeerinfo()[no_version_peer_id] @@ -150,7 +151,7 @@ class NetTest(BitcoinTestFramework): "synced_blocks": -1, "synced_headers": -1, "timeoffset": 0, - "transport_protocol_type": "v1", + "transport_protocol_type": "v1" if not self.options.v2transport else "detecting", "version": 0, }, ) @@ -160,19 +161,23 @@ class NetTest(BitcoinTestFramework): def test_getnettotals(self): self.log.info("Test getnettotals") # Test getnettotals and getpeerinfo by doing a ping. The bytes - # sent/received should increase by at least the size of one ping (32 - # bytes) and one pong (32 bytes). + # sent/received should increase by at least the size of one ping + # and one pong. Both have a payload size of 8 bytes, but the total + # size depends on the used p2p version: + # - p2p v1: 24 bytes (header) + 8 bytes (payload) = 32 bytes + # - p2p v2: 21 bytes (header/tag with short-id) + 8 bytes (payload) = 29 bytes + ping_size = 32 if not self.options.v2transport else 29 net_totals_before = self.nodes[0].getnettotals() peer_info_before = self.nodes[0].getpeerinfo() self.nodes[0].ping() - self.wait_until(lambda: (self.nodes[0].getnettotals()['totalbytessent'] >= net_totals_before['totalbytessent'] + 32 * 2), timeout=1) - self.wait_until(lambda: (self.nodes[0].getnettotals()['totalbytesrecv'] >= net_totals_before['totalbytesrecv'] + 32 * 2), timeout=1) + self.wait_until(lambda: (self.nodes[0].getnettotals()['totalbytessent'] >= net_totals_before['totalbytessent'] + ping_size * 2), timeout=1) + self.wait_until(lambda: (self.nodes[0].getnettotals()['totalbytesrecv'] >= net_totals_before['totalbytesrecv'] + ping_size * 2), timeout=1) for peer_before in peer_info_before: peer_after = lambda: next(p for p in self.nodes[0].getpeerinfo() if p['id'] == peer_before['id']) - self.wait_until(lambda: peer_after()['bytesrecv_per_msg'].get('pong', 0) >= peer_before['bytesrecv_per_msg'].get('pong', 0) + 32, timeout=1) - self.wait_until(lambda: peer_after()['bytessent_per_msg'].get('ping', 0) >= peer_before['bytessent_per_msg'].get('ping', 0) + 32, timeout=1) + self.wait_until(lambda: peer_after()['bytesrecv_per_msg'].get('pong', 0) >= peer_before['bytesrecv_per_msg'].get('pong', 0) + ping_size, timeout=1) + self.wait_until(lambda: peer_after()['bytessent_per_msg'].get('ping', 0) >= peer_before['bytessent_per_msg'].get('ping', 0) + ping_size, timeout=1) def test_getnetworkinfo(self): self.log.info("Test getnetworkinfo") @@ -216,8 +221,10 @@ class NetTest(BitcoinTestFramework): ip_port = "127.0.0.1:{}".format(p2p_port(2)) self.nodes[0].addnode(node=ip_port, command='add') # try to add an equivalent ip - ip_port2 = "127.1:{}".format(p2p_port(2)) - assert_raises_rpc_error(-23, "Node already added", self.nodes[0].addnode, node=ip_port2, command='add') + # (note that OpenBSD doesn't support the IPv4 shorthand notation with omitted zero-bytes) + if platform.system() != "OpenBSD": + ip_port2 = "127.1:{}".format(p2p_port(2)) + assert_raises_rpc_error(-23, "Node already added", self.nodes[0].addnode, node=ip_port2, command='add') # check that the node has indeed been added added_nodes = self.nodes[0].getaddednodeinfo() assert_equal(len(added_nodes), 1) @@ -345,7 +352,10 @@ class NetTest(BitcoinTestFramework): node = self.nodes[0] self.restart_node(0) - self.connect_nodes(0, 1) + # we want to use a p2p v1 connection here in order to ensure + # a peer id of zero (a downgrade from v2 to v1 would lead + # to an increase of the peer id) + self.connect_nodes(0, 1, peer_advertises_v2=False) self.log.info("Test sendmsgtopeer") self.log.debug("Send a valid message") diff --git a/test/functional/rpc_packages.py b/test/functional/rpc_packages.py index 5644a9f5a8..664f2df3f1 100755 --- a/test/functional/rpc_packages.py +++ b/test/functional/rpc_packages.py @@ -304,6 +304,7 @@ class RPCPackagesTest(BitcoinTestFramework): submitpackage_result = node.submitpackage(package=[tx["hex"] for tx in package_txns]) # Check that each result is present, with the correct size and fees + assert_equal(submitpackage_result["package_msg"], "success") for package_txn in package_txns: tx = package_txn["tx"] assert tx.getwtxid() in submitpackage_result["tx-results"] @@ -334,9 +335,26 @@ class RPCPackagesTest(BitcoinTestFramework): self.log.info("Submitpackage only allows packages of 1 child with its parents") # Chain of 3 transactions has too many generations - chain_hex = [t["hex"] for t in self.wallet.create_self_transfer_chain(chain_length=25)] + legacy_pool = node.getrawmempool() + chain_hex = [t["hex"] for t in self.wallet.create_self_transfer_chain(chain_length=3)] assert_raises_rpc_error(-25, "package topology disallowed", node.submitpackage, chain_hex) + assert_equal(legacy_pool, node.getrawmempool()) + # Create a transaction chain such as only the parent gets accepted (by making the child's + # version non-standard). Make sure the parent does get broadcast. + self.log.info("If a package is partially submitted, transactions included in mempool get broadcast") + peer = node.add_p2p_connection(P2PTxInvStore()) + txs = self.wallet.create_self_transfer_chain(chain_length=2) + bad_child = tx_from_hex(txs[1]["hex"]) + bad_child.nVersion = -1 + hex_partial_acceptance = [txs[0]["hex"], bad_child.serialize().hex()] + res = node.submitpackage(hex_partial_acceptance) + assert_equal(res["package_msg"], "transaction failed") + first_wtxid = txs[0]["tx"].getwtxid() + assert "error" not in res["tx-results"][first_wtxid] + sec_wtxid = bad_child.getwtxid() + assert_equal(res["tx-results"][sec_wtxid]["error"], "version") + peer.wait_for_broadcast([first_wtxid]) if __name__ == "__main__": RPCPackagesTest().main() diff --git a/test/functional/rpc_rawtransaction.py b/test/functional/rpc_rawtransaction.py index 2395935620..c12865b5e3 100755 --- a/test/functional/rpc_rawtransaction.py +++ b/test/functional/rpc_rawtransaction.py @@ -32,6 +32,7 @@ from test_framework.script import ( from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + assert_greater_than, assert_raises_rpc_error, ) from test_framework.wallet import ( @@ -70,7 +71,7 @@ class RawTransactionsTest(BitcoinTestFramework): self.extra_args = [ ["-txindex"], ["-txindex"], - [], + ["-fastprune", "-prune=1"], ] # whitelist all peers to speed up tx relay / mempool sync for args in self.extra_args: @@ -85,7 +86,6 @@ class RawTransactionsTest(BitcoinTestFramework): self.wallet = MiniWallet(self.nodes[0]) self.getrawtransaction_tests() - self.getrawtransaction_verbosity_tests() self.createrawtransaction_tests() self.sendrawtransaction_tests() self.sendrawtransaction_testmempoolaccept_tests() @@ -94,6 +94,8 @@ class RawTransactionsTest(BitcoinTestFramework): if self.is_specified_wallet_compiled() and not self.options.descriptors: self.import_deterministic_coinbase_privkeys() self.raw_multisig_transaction_legacy_tests() + self.getrawtransaction_verbosity_tests() + def getrawtransaction_tests(self): tx = self.wallet.send_self_transfer(from_node=self.nodes[0]) @@ -243,6 +245,13 @@ class RawTransactionsTest(BitcoinTestFramework): coin_base = self.nodes[1].getblock(block1)['tx'][0] gottx = self.nodes[1].getrawtransaction(txid=coin_base, verbosity=2, blockhash=block1) assert 'fee' not in gottx + # check that verbosity 2 for a mempool tx will fallback to verbosity 1 + # Do this with a pruned chain, as a regression test for https://github.com/bitcoin/bitcoin/pull/29003 + self.generate(self.nodes[2], 400) + assert_greater_than(self.nodes[2].pruneblockchain(250), 0) + mempool_tx = self.wallet.send_self_transfer(from_node=self.nodes[2])['txid'] + gottx = self.nodes[2].getrawtransaction(txid=mempool_tx, verbosity=2) + assert 'fee' not in gottx def createrawtransaction_tests(self): self.log.info("Test createrawtransaction") diff --git a/test/functional/test_framework/blockfilter.py b/test/functional/test_framework/blockfilter.py index 3d6b38a23d..a16aa3d34f 100644 --- a/test/functional/test_framework/blockfilter.py +++ b/test/functional/test_framework/blockfilter.py @@ -29,7 +29,7 @@ def bip158_basic_element_hash(script_pub_key, N, block_hash): def bip158_relevant_scriptpubkeys(node, block_hash): - """ Determines the basic filter relvant scriptPubKeys as defined in BIP158: + """ Determines the basic filter relevant scriptPubKeys as defined in BIP158: 'A basic filter MUST contain exactly the following items for each transaction in a block: - The previous output script (the script being spent) for each input, except for diff --git a/test/functional/test_framework/script.py b/test/functional/test_framework/script.py index f4628bf4af..78d8580794 100644 --- a/test/functional/test_framework/script.py +++ b/test/functional/test_framework/script.py @@ -10,7 +10,6 @@ This file is modified from python-bitcoinlib. from collections import namedtuple import struct import unittest -from typing import List, Dict from .key import TaggedHash, tweak_add_pubkey, compute_xonly_pubkey @@ -110,8 +109,8 @@ class CScriptOp(int): _opcode_instances.append(super().__new__(cls, n)) return _opcode_instances[n] -OPCODE_NAMES: Dict[CScriptOp, str] = {} -_opcode_instances: List[CScriptOp] = [] +OPCODE_NAMES: dict[CScriptOp, str] = {} +_opcode_instances: list[CScriptOp] = [] # Populate opcode instance table for n in range(0xff + 1): diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 70b3943478..0ee332b75b 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -19,7 +19,6 @@ import sys import tempfile import time -from typing import List from .address import create_deterministic_address_bcrt1_p2tr_op_true from .authproxy import JSONRPCException from . import coverage @@ -97,7 +96,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): """Sets test framework defaults. Do not override this method. Instead, override the set_test_params() method""" self.chain: str = 'regtest' self.setup_clean_chain: bool = False - self.nodes: List[TestNode] = [] + self.nodes: list[TestNode] = [] self.extra_args = None self.network_thread = None self.rpc_timeout = 60 # Wait for up to 60 seconds for the RPC server to respond @@ -508,8 +507,6 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): assert_equal(len(binary_cli), num_nodes) for i in range(num_nodes): args = list(extra_args[i]) - if self.options.v2transport and ("-v2transport=0" not in args): - args.append("-v2transport=1") test_node_i = TestNode( i, get_datadir_path(self.options.tmpdir, i), @@ -528,6 +525,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): start_perf=self.options.perf, use_valgrind=self.options.valgrind, descriptors=self.options.descriptors, + v2transport=self.options.v2transport, ) self.nodes.append(test_node_i) if not test_node_i.version_is_at_least(170000): @@ -602,7 +600,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): ip_port = "127.0.0.1:" + str(p2p_port(b)) if peer_advertises_v2 is None: - peer_advertises_v2 = self.options.v2transport + peer_advertises_v2 = from_connection.use_v2transport if peer_advertises_v2: from_connection.addnode(node=ip_port, command="onetry", v2transport=True) diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index 281469f9ed..b3f777b9df 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -67,7 +67,7 @@ class TestNode(): To make things easier for the test writer, any unrecognised messages will be dispatched to the RPC connection.""" - def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, bitcoind, bitcoin_cli, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, descriptors=False): + def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, bitcoind, bitcoin_cli, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, descriptors=False, v2transport=False): """ Kwargs: start_perf (bool): If True, begin profiling the node with `perf` as soon as @@ -126,6 +126,12 @@ class TestNode(): if self.version_is_at_least(239000): self.args.append("-loglevel=trace") + # Default behavior from global -v2transport flag is added to args to persist it over restarts. + # May be overwritten in individual tests, using extra_args. + self.default_to_v2 = v2transport + if self.default_to_v2: + self.args.append("-v2transport=1") + self.cli = TestNodeCLI(bitcoin_cli, self.datadir_path) self.use_cli = use_cli self.start_perf = start_perf @@ -198,6 +204,8 @@ class TestNode(): if extra_args is None: extra_args = self.extra_args + self.use_v2transport = "-v2transport=1" in extra_args or (self.default_to_v2 and "-v2transport=0" not in extra_args) + # Add a new stdout and stderr file each time bitcoind is started if stderr is None: stderr = tempfile.NamedTemporaryFile(dir=self.stderr_dir, delete=False) @@ -252,7 +260,7 @@ class TestNode(): if self.version_is_at_least(190000): # getmempoolinfo.loaded is available since commit # bb8ae2c (version 0.19.0) - wait_until_helper_internal(lambda: rpc.getmempoolinfo()['loaded'], timeout_factor=self.timeout_factor) + self.wait_until(lambda: rpc.getmempoolinfo()['loaded']) # Wait for the node to finish reindex, block import, and # loading the mempool. Usually importing happens fast or # even "immediate" when the node is started. However, there @@ -406,7 +414,7 @@ class TestNode(): def wait_until_stopped(self, *, timeout=BITCOIND_PROC_WAIT_TIMEOUT, expect_error=False, **kwargs): expected_ret_code = 1 if expect_error else 0 # Whether node shutdown return EXIT_FAILURE or EXIT_SUCCESS - wait_until_helper_internal(lambda: self.is_node_stopped(expected_ret_code=expected_ret_code, **kwargs), timeout=timeout, timeout_factor=self.timeout_factor) + self.wait_until(lambda: self.is_node_stopped(expected_ret_code=expected_ret_code, **kwargs), timeout=timeout) def replace_in_config(self, replacements): """ @@ -512,6 +520,23 @@ class TestNode(): str(expected_msgs), print_log)) @contextlib.contextmanager + def wait_for_new_peer(self, timeout=5): + """ + Wait until the node is connected to at least one new peer. We detect this + by watching for an increased highest peer id, using the `getpeerinfo` RPC call. + Note that the simpler approach of only accounting for the number of peers + suffers from race conditions, as disconnects from unrelated previous peers + could happen anytime in-between. + """ + def get_highest_peer_id(): + peer_info = self.getpeerinfo() + return peer_info[-1]["id"] if peer_info else -1 + + initial_peer_id = get_highest_peer_id() + yield + self.wait_until(lambda: get_highest_peer_id() > initial_peer_id, timeout=timeout) + + @contextlib.contextmanager def profile_with_perf(self, profile_name: str): """ Context manager that allows easy profiling of node activity using `perf`. @@ -721,7 +746,7 @@ class TestNode(): p.peer_disconnect() del self.p2ps[:] - wait_until_helper_internal(lambda: self.num_test_p2p_connections() == 0, timeout_factor=self.timeout_factor) + self.wait_until(lambda: self.num_test_p2p_connections() == 0) def bumpmocktime(self, seconds): """Fast forward using setmocktime to self.mocktime + seconds. Requires setmocktime to have @@ -730,6 +755,9 @@ class TestNode(): self.mocktime += seconds self.setmocktime(self.mocktime) + def wait_until(self, test_function, timeout=60): + return wait_until_helper_internal(test_function, timeout=timeout, timeout_factor=self.timeout_factor) + class TestNodeCLIAttr: def __init__(self, cli, command): @@ -782,15 +810,15 @@ class TestNodeCLI(): results.append(dict(error=e)) return results - def send_cli(self, command=None, *args, **kwargs): + def send_cli(self, clicommand=None, *args, **kwargs): """Run bitcoin-cli command. Deserializes returned string as python object.""" pos_args = [arg_to_cli(arg) for arg in args] named_args = [str(key) + "=" + arg_to_cli(value) for (key, value) in kwargs.items()] p_args = [self.binary, f"-datadir={self.datadir}"] + self.options if named_args: p_args += ["-named"] - if command is not None: - p_args += [command] + if clicommand is not None: + p_args += [clicommand] p_args += pos_args + named_args self.log.debug("Running bitcoin-cli {}".format(p_args[2:])) process = subprocess.Popen(p_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index 104f158d92..c65e3e38e6 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -19,7 +19,8 @@ import time from . import coverage from .authproxy import AuthServiceProxy, JSONRPCException -from typing import Callable, Optional, Tuple +from collections.abc import Callable +from typing import Optional logger = logging.getLogger("TestFramework.utils") @@ -409,7 +410,7 @@ def get_datadir_path(dirname, n): return pathlib.Path(dirname) / f"node{n}" -def get_temp_default_datadir(temp_dir: pathlib.Path) -> Tuple[dict, pathlib.Path]: +def get_temp_default_datadir(temp_dir: pathlib.Path) -> tuple[dict, pathlib.Path]: """Return os-specific environment variables that can be set to make the GetDefaultDataDir() function return a datadir path under the provided temp_dir, as well as the complete path it would return.""" diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py index 035a482f4c..53c8e1b0cc 100644 --- a/test/functional/test_framework/wallet.py +++ b/test/functional/test_framework/wallet.py @@ -9,7 +9,6 @@ from decimal import Decimal from enum import Enum from typing import ( Any, - List, Optional, ) from test_framework.address import ( @@ -284,7 +283,7 @@ class MiniWallet: def create_self_transfer_multi( self, *, - utxos_to_spend: Optional[List[dict]] = None, + utxos_to_spend: Optional[list[dict]] = None, num_outputs=1, amount_per_output=0, locktime=0, diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 35c662b0d9..2460b2e3e6 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -156,6 +156,7 @@ BASE_SCRIPTS = [ 'p2p_invalid_messages.py', 'rpc_createmultisig.py', 'p2p_timeouts.py', + 'p2p_timeouts.py --v2transport', 'wallet_dump.py --legacy-wallet', 'rpc_signer.py', 'wallet_signer.py --descriptors', @@ -206,6 +207,8 @@ BASE_SCRIPTS = [ 'wallet_createwallet.py --descriptors', 'wallet_watchonly.py --legacy-wallet', 'wallet_watchonly.py --usecli --legacy-wallet', + 'wallet_reindex.py --legacy-wallet', + 'wallet_reindex.py --descriptors', 'wallet_reorgsrestore.py', 'wallet_conflicts.py --legacy-wallet', 'wallet_conflicts.py --descriptors', @@ -243,6 +246,7 @@ BASE_SCRIPTS = [ 'p2p_getdata.py', 'p2p_addrfetch.py', 'rpc_net.py', + 'rpc_net.py --v2transport', 'wallet_keypool.py --legacy-wallet', 'wallet_keypool.py --descriptors', 'wallet_descriptor.py --descriptors', @@ -368,6 +372,7 @@ BASE_SCRIPTS = [ 'wallet_orphanedreward.py', 'wallet_timelock.py', 'p2p_node_network_limited.py', + 'p2p_node_network_limited.py --v2transport', 'p2p_permissions.py', 'feature_blocksdir.py', 'wallet_startup.py', @@ -562,8 +567,7 @@ def run_tests(*, test_list, src_dir, build_dir, tmpdir, jobs=1, enable_coverage= test_framework_tests.addTest(unittest.TestLoader().loadTestsFromName("test_framework.{}".format(module))) result = unittest.TextTestRunner(verbosity=1, failfast=True).run(test_framework_tests) if not result.wasSuccessful(): - logging.debug("Early exiting after failure in TestFramework unit tests") - sys.exit(False) + sys.exit("Early exiting after failure in TestFramework unit tests") flags = ['--cachedir={}'.format(cache_dir)] + args diff --git a/test/functional/wallet_avoidreuse.py b/test/functional/wallet_avoidreuse.py index c257bda452..9d3c55d6b6 100755 --- a/test/functional/wallet_avoidreuse.py +++ b/test/functional/wallet_avoidreuse.py @@ -257,7 +257,7 @@ class AvoidReuseTest(BitcoinTestFramework): if not self.options.descriptors: # For the second send, we transmute it to a related single-key address - # to make sure it's also detected as re-use + # to make sure it's also detected as reuse fund_spk = address_to_scriptpubkey(fundaddr).hex() fund_decoded = self.nodes[0].decodescript(fund_spk) if second_addr_type == "p2sh-segwit": diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py index 78bfa97212..f798eee365 100755 --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -639,7 +639,7 @@ class WalletTest(BitcoinTestFramework): node0_balance = self.nodes[0].getbalance() # With walletrejectlongchains we will not create the tx and store it in our wallet. - assert_raises_rpc_error(-6, "Transaction has too long of a mempool chain", self.nodes[0].sendtoaddress, sending_addr, node0_balance - Decimal('0.01')) + assert_raises_rpc_error(-6, f"too many unconfirmed ancestors [limit: {chainlimit * 2}]", self.nodes[0].sendtoaddress, sending_addr, node0_balance - Decimal('0.01')) # Verify nothing new in wallet assert_equal(total_txs, len(self.nodes[0].listtransactions("*", 99999))) diff --git a/test/functional/wallet_fast_rescan.py b/test/functional/wallet_fast_rescan.py index 2f9c924e71..1315bccafd 100755 --- a/test/functional/wallet_fast_rescan.py +++ b/test/functional/wallet_fast_rescan.py @@ -4,8 +4,6 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test that fast rescan using block filters for descriptor wallets detects top-ups correctly and finds the same transactions than the slow variant.""" -from typing import List - from test_framework.address import address_to_scriptpubkey from test_framework.descriptors import descsum_create from test_framework.test_framework import BitcoinTestFramework @@ -32,7 +30,7 @@ class WalletFastRescanTest(BitcoinTestFramework): self.skip_if_no_wallet() self.skip_if_no_sqlite() - def get_wallet_txids(self, node: TestNode, wallet_name: str) -> List[str]: + def get_wallet_txids(self, node: TestNode, wallet_name: str) -> list[str]: w = node.get_wallet_rpc(wallet_name) txs = w.listtransactions('*', 1000000) return [tx['txid'] for tx in txs] diff --git a/test/functional/wallet_listtransactions.py b/test/functional/wallet_listtransactions.py index 18bb8a0cd8..064ce12108 100755 --- a/test/functional/wallet_listtransactions.py +++ b/test/functional/wallet_listtransactions.py @@ -111,6 +111,7 @@ class ListTransactionsTest(BitcoinTestFramework): self.run_rbf_opt_in_test() self.run_externally_generated_address_test() + self.run_coinjoin_test() self.run_invalid_parameters_test() self.test_op_return() @@ -281,6 +282,34 @@ class ListTransactionsTest(BitcoinTestFramework): assert_equal(['pizza2'], self.nodes[0].getaddressinfo(addr2)['labels']) assert_equal(['pizza3'], self.nodes[0].getaddressinfo(addr3)['labels']) + def run_coinjoin_test(self): + self.log.info('Check "coin-join" transaction') + input_0 = next(i for i in self.nodes[0].listunspent(query_options={"minimumAmount": 0.2}, include_unsafe=False)) + input_1 = next(i for i in self.nodes[1].listunspent(query_options={"minimumAmount": 0.2}, include_unsafe=False)) + raw_hex = self.nodes[0].createrawtransaction( + inputs=[ + { + "txid": input_0["txid"], + "vout": input_0["vout"], + }, + { + "txid": input_1["txid"], + "vout": input_1["vout"], + }, + ], + outputs={ + self.nodes[0].getnewaddress(): 0.123, + self.nodes[1].getnewaddress(): 0.123, + }, + ) + raw_hex = self.nodes[0].signrawtransactionwithwallet(raw_hex)["hex"] + raw_hex = self.nodes[1].signrawtransactionwithwallet(raw_hex)["hex"] + txid_join = self.nodes[0].sendrawtransaction(hexstring=raw_hex, maxfeerate=0) + fee_join = self.nodes[0].getmempoolentry(txid_join)["fees"]["base"] + # Fee should be correct: assert_equal(fee_join, self.nodes[0].gettransaction(txid_join)['fee']) + # But it is not, see for example https://github.com/bitcoin/bitcoin/issues/14136: + assert fee_join != self.nodes[0].gettransaction(txid_join)["fee"] + def run_invalid_parameters_test(self): self.log.info("Test listtransactions RPC parameter validity") assert_raises_rpc_error(-8, 'Label argument must be a valid label name or "*".', self.nodes[0].listtransactions, label="") diff --git a/test/functional/wallet_reindex.py b/test/functional/wallet_reindex.py new file mode 100755 index 0000000000..5388de4b71 --- /dev/null +++ b/test/functional/wallet_reindex.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023-present The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php. + +"""Test wallet-reindex interaction""" + +import time + +from test_framework.blocktools import COINBASE_MATURITY +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, +) +BLOCK_TIME = 60 * 10 + +class WalletReindexTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def advance_time(self, node, secs): + self.node_time += secs + node.setmocktime(self.node_time) + + # Verify the wallet updates the birth time accordingly when it detects a transaction + # with a time older than the oldest descriptor timestamp. + # This could happen when the user blindly imports a descriptor with 'timestamp=now'. + def birthtime_test(self, node, miner_wallet): + self.log.info("Test birth time update during tx scanning") + # Fund address to test + wallet_addr = miner_wallet.getnewaddress() + tx_id = miner_wallet.sendtoaddress(wallet_addr, 2) + + # Generate 50 blocks, one every 10 min to surpass the 2 hours rescan window the wallet has + for _ in range(50): + self.generate(node, 1) + self.advance_time(node, BLOCK_TIME) + + # Now create a new wallet, and import the descriptor + node.createwallet(wallet_name='watch_only', disable_private_keys=True, load_on_startup=True) + wallet_watch_only = node.get_wallet_rpc('watch_only') + # Blank wallets don't have a birth time + assert 'birthtime' not in wallet_watch_only.getwalletinfo() + + # For a descriptors wallet: Import address with timestamp=now. + # For legacy wallet: There is no way of importing a script/address with a custom time. The wallet always imports it with birthtime=1. + # In both cases, disable rescan to not detect the transaction. + wallet_watch_only.importaddress(wallet_addr, rescan=False) + assert_equal(len(wallet_watch_only.listtransactions()), 0) + + # Depending on the wallet type, the birth time changes. + wallet_birthtime = wallet_watch_only.getwalletinfo()['birthtime'] + if self.options.descriptors: + # As blocks were generated every 10 min, the chain MTP timestamp is node_time - 60 min. + assert_equal(self.node_time - BLOCK_TIME * 6, wallet_birthtime) + else: + # No way of importing scripts/addresses with a custom time on a legacy wallet. + # It's always set to the beginning of time. + assert_equal(wallet_birthtime, 1) + + # Rescan the wallet to detect the missing transaction + wallet_watch_only.rescanblockchain() + assert_equal(wallet_watch_only.gettransaction(tx_id)['confirmations'], 50) + assert_equal(wallet_watch_only.getbalances()['mine' if self.options.descriptors else 'watchonly']['trusted'], 2) + + # Reindex and wait for it to finish + with node.assert_debug_log(expected_msgs=["initload thread exit"]): + self.restart_node(0, extra_args=['-reindex=1', f'-mocktime={self.node_time}']) + node.syncwithvalidationinterfacequeue() + + # Verify the transaction is still 'confirmed' after reindex + wallet_watch_only = node.get_wallet_rpc('watch_only') + tx_info = wallet_watch_only.gettransaction(tx_id) + assert_equal(tx_info['confirmations'], 50) + + # Depending on the wallet type, the birth time changes. + if self.options.descriptors: + # For descriptors, verify the wallet updated the birth time to the transaction time + assert_equal(tx_info['time'], wallet_watch_only.getwalletinfo()['birthtime']) + else: + # For legacy, as the birth time was set to the beginning of time, verify it did not change + assert_equal(wallet_birthtime, 1) + + wallet_watch_only.unloadwallet() + + def run_test(self): + node = self.nodes[0] + self.node_time = int(time.time()) + node.setmocktime(self.node_time) + + # Fund miner + node.createwallet(wallet_name='miner', load_on_startup=True) + miner_wallet = node.get_wallet_rpc('miner') + self.generatetoaddress(node, COINBASE_MATURITY + 10, miner_wallet.getnewaddress()) + + # Tests + self.birthtime_test(node, miner_wallet) + + +if __name__ == '__main__': + WalletReindexTest().main() |