diff options
Diffstat (limited to 'test/functional')
-rwxr-xr-x | test/functional/interface_rest.py | 28 | ||||
-rwxr-xr-x | test/functional/interface_usdt_mempool.py | 343 | ||||
-rwxr-xr-x | test/functional/mempool_sigoplimit.py | 154 | ||||
-rwxr-xr-x | test/functional/mining_getblocktemplate_longpoll.py | 14 | ||||
-rwxr-xr-x | test/functional/p2p_invalid_messages.py | 7 | ||||
-rwxr-xr-x | test/functional/rpc_blockchain.py | 8 | ||||
-rwxr-xr-x | test/functional/rpc_generate.py | 7 | ||||
-rwxr-xr-x | test/functional/rpc_getblockfrompeer.py | 49 | ||||
-rwxr-xr-x | test/functional/rpc_invalid_address_message.py | 18 | ||||
-rwxr-xr-x | test/functional/rpc_psbt.py | 45 | ||||
-rwxr-xr-x | test/functional/test_framework/p2p.py | 2 | ||||
-rw-r--r-- | test/functional/test_framework/psbt.py | 10 | ||||
-rwxr-xr-x | test/functional/test_framework/test_framework.py | 7 | ||||
-rwxr-xr-x | test/functional/test_framework/test_node.py | 2 | ||||
-rwxr-xr-x | test/functional/test_runner.py | 2 | ||||
-rwxr-xr-x | test/functional/wallet_basic.py | 2 | ||||
-rwxr-xr-x | test/functional/wallet_importdescriptors.py | 25 | ||||
-rwxr-xr-x | test/functional/wallet_listdescriptors.py | 8 | ||||
-rwxr-xr-x | test/functional/wallet_transactiontime_rescan.py | 25 |
19 files changed, 691 insertions, 65 deletions
diff --git a/test/functional/interface_rest.py b/test/functional/interface_rest.py index 0dddc8946a..aafee7e87c 100755 --- a/test/functional/interface_rest.py +++ b/test/functional/interface_rest.py @@ -347,6 +347,34 @@ class RESTTest (BitcoinTestFramework): assert_equal(json_obj[tx]['spentby'], txs[i + 1:i + 2]) assert_equal(json_obj[tx]['depends'], txs[i - 1:i]) + # Check the mempool response for explicit parameters + json_obj = self.test_rest_request("/mempool/contents", query_params={"verbose": "true", "mempool_sequence": "false"}) + assert_equal(json_obj, raw_mempool_verbose) + + # Check the mempool response for not verbose + json_obj = self.test_rest_request("/mempool/contents", query_params={"verbose": "false"}) + raw_mempool = self.nodes[0].getrawmempool(verbose=False) + + assert_equal(json_obj, raw_mempool) + + # Check the mempool response for sequence + json_obj = self.test_rest_request("/mempool/contents", query_params={"verbose": "false", "mempool_sequence": "true"}) + raw_mempool = self.nodes[0].getrawmempool(verbose=False, mempool_sequence=True) + + assert_equal(json_obj, raw_mempool) + + # Check for error response if verbose=true and mempool_sequence=true + resp = self.test_rest_request("/mempool/contents", ret_type=RetType.OBJ, status=400, query_params={"verbose": "true", "mempool_sequence": "true"}) + assert_equal(resp.read().decode('utf-8').strip(), 'Verbose results cannot contain mempool sequence values. (hint: set "verbose=false")') + + # Check for error response if verbose is not "true" or "false" + resp = self.test_rest_request("/mempool/contents", ret_type=RetType.OBJ, status=400, query_params={"verbose": "TRUE"}) + assert_equal(resp.read().decode('utf-8').strip(), 'The "verbose" query parameter must be either "true" or "false".') + + # Check for error response if mempool_sequence is not "true" or "false" + resp = self.test_rest_request("/mempool/contents", ret_type=RetType.OBJ, status=400, query_params={"verbose": "false", "mempool_sequence": "TRUE"}) + assert_equal(resp.read().decode('utf-8').strip(), 'The "mempool_sequence" query parameter must be either "true" or "false".') + # Now mine the transactions newblockhash = self.generate(self.nodes[1], 1) diff --git a/test/functional/interface_usdt_mempool.py b/test/functional/interface_usdt_mempool.py new file mode 100755 index 0000000000..ec2f9e4e77 --- /dev/null +++ b/test/functional/interface_usdt_mempool.py @@ -0,0 +1,343 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +""" Tests the mempool:* tracepoint API interface. + See https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#context-mempool +""" + +from decimal import Decimal + +# Test will be skipped if we don't have bcc installed +try: + from bcc import BPF, USDT # type: ignore[import] +except ImportError: + pass + +from test_framework.blocktools import COINBASE_MATURITY +from test_framework.messages import COIN, DEFAULT_MEMPOOL_EXPIRY_HOURS +from test_framework.p2p import P2PDataStore +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal +from test_framework.wallet import MiniWallet + +MEMPOOL_TRACEPOINTS_PROGRAM = """ +# include <uapi/linux/ptrace.h> + +// The longest rejection reason is 118 chars and is generated in case of SCRIPT_ERR_EVAL_FALSE by +// strprintf("mandatory-script-verify-flag-failed (%s)", ScriptErrorString(check.GetScriptError())) +#define MAX_REJECT_REASON_LENGTH 118 +// The longest string returned by RemovalReasonToString() is 'sizelimit' +#define MAX_REMOVAL_REASON_LENGTH 9 +#define HASH_LENGTH 32 + +struct added_event +{ + u8 hash[HASH_LENGTH]; + u64 vsize; + s64 fee; +}; + +struct removed_event +{ + u8 hash[HASH_LENGTH]; + char reason[MAX_REMOVAL_REASON_LENGTH]; + u64 vsize; + s64 fee; + u64 entry_time; +}; + +struct rejected_event +{ + u8 hash[HASH_LENGTH]; + char reason[MAX_REJECT_REASON_LENGTH]; +}; + +struct replaced_event +{ + u8 replaced_hash[HASH_LENGTH]; + u64 replaced_vsize; + s64 replaced_fee; + u64 replaced_entry_time; + u8 replacement_hash[HASH_LENGTH]; + u64 replacement_vsize; + s64 replacement_fee; +}; + +// BPF perf buffer to push the data to user space. +BPF_PERF_OUTPUT(added_events); +BPF_PERF_OUTPUT(removed_events); +BPF_PERF_OUTPUT(rejected_events); +BPF_PERF_OUTPUT(replaced_events); + +int trace_added(struct pt_regs *ctx) { + struct added_event added = {}; + + bpf_usdt_readarg_p(1, ctx, &added.hash, HASH_LENGTH); + bpf_usdt_readarg(2, ctx, &added.vsize); + bpf_usdt_readarg(3, ctx, &added.fee); + + added_events.perf_submit(ctx, &added, sizeof(added)); + return 0; +} + +int trace_removed(struct pt_regs *ctx) { + struct removed_event removed = {}; + + bpf_usdt_readarg_p(1, ctx, &removed.hash, HASH_LENGTH); + bpf_usdt_readarg_p(2, ctx, &removed.reason, MAX_REMOVAL_REASON_LENGTH); + bpf_usdt_readarg(3, ctx, &removed.vsize); + bpf_usdt_readarg(4, ctx, &removed.fee); + bpf_usdt_readarg(5, ctx, &removed.entry_time); + + removed_events.perf_submit(ctx, &removed, sizeof(removed)); + return 0; +} + +int trace_rejected(struct pt_regs *ctx) { + struct rejected_event rejected = {}; + + bpf_usdt_readarg_p(1, ctx, &rejected.hash, HASH_LENGTH); + bpf_usdt_readarg_p(2, ctx, &rejected.reason, MAX_REJECT_REASON_LENGTH); + + rejected_events.perf_submit(ctx, &rejected, sizeof(rejected)); + return 0; +} + +int trace_replaced(struct pt_regs *ctx) { + struct replaced_event replaced = {}; + + bpf_usdt_readarg_p(1, ctx, &replaced.replaced_hash, HASH_LENGTH); + bpf_usdt_readarg(2, ctx, &replaced.replaced_vsize); + bpf_usdt_readarg(3, ctx, &replaced.replaced_fee); + bpf_usdt_readarg(4, ctx, &replaced.replaced_entry_time); + bpf_usdt_readarg_p(5, ctx, &replaced.replacement_hash, HASH_LENGTH); + bpf_usdt_readarg(6, ctx, &replaced.replacement_vsize); + bpf_usdt_readarg(7, ctx, &replaced.replacement_fee); + + replaced_events.perf_submit(ctx, &replaced, sizeof(replaced)); + return 0; +} +""" + + +class MempoolTracepointTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + + def skip_test_if_missing_module(self): + self.skip_if_platform_not_linux() + self.skip_if_no_bitcoind_tracepoints() + self.skip_if_no_python_bcc() + self.skip_if_no_bpf_permissions() + + def added_test(self): + """Add a transaction to the mempool and make sure the tracepoint returns + the expected txid, vsize, and fee.""" + + EXPECTED_ADDED_EVENTS = 1 + handled_added_events = 0 + + self.log.info("Hooking into mempool:added tracepoint...") + node = self.nodes[0] + ctx = USDT(pid=node.process.pid) + ctx.enable_probe(probe="mempool:added", fn_name="trace_added") + bpf = BPF(text=MEMPOOL_TRACEPOINTS_PROGRAM, usdt_contexts=[ctx], debug=0) + + def handle_added_event(_, data, __): + nonlocal handled_added_events + event = bpf["added_events"].event(data) + assert_equal(txid, bytes(event.hash)[::-1].hex()) + assert_equal(vsize, event.vsize) + assert_equal(fee, event.fee) + handled_added_events += 1 + + bpf["added_events"].open_perf_buffer(handle_added_event) + + self.log.info("Sending transaction...") + fee = Decimal(31200) + tx = self.wallet.send_self_transfer(from_node=node, fee=fee / COIN) + # expected data + txid = tx["txid"] + vsize = tx["tx"].get_vsize() + + self.log.info("Polling buffer...") + bpf.perf_buffer_poll(timeout=200) + + self.log.info("Cleaning up mempool...") + self.generate(node, 1) + + bpf.cleanup() + + self.log.info("Ensuring mempool:added event was handled successfully...") + assert_equal(EXPECTED_ADDED_EVENTS, handled_added_events) + + def removed_test(self): + """Expire a transaction from the mempool and make sure the tracepoint returns + the expected txid, expiry reason, vsize, and fee.""" + + EXPECTED_REMOVED_EVENTS = 1 + handled_removed_events = 0 + + self.log.info("Hooking into mempool:removed tracepoint...") + node = self.nodes[0] + ctx = USDT(pid=node.process.pid) + ctx.enable_probe(probe="mempool:removed", fn_name="trace_removed") + bpf = BPF(text=MEMPOOL_TRACEPOINTS_PROGRAM, usdt_contexts=[ctx], debug=0) + + def handle_removed_event(_, data, __): + nonlocal handled_removed_events + event = bpf["removed_events"].event(data) + assert_equal(txid, bytes(event.hash)[::-1].hex()) + assert_equal(reason, event.reason.decode("UTF-8")) + assert_equal(vsize, event.vsize) + assert_equal(fee, event.fee) + assert_equal(entry_time, event.entry_time) + handled_removed_events += 1 + + bpf["removed_events"].open_perf_buffer(handle_removed_event) + + self.log.info("Sending transaction...") + fee = Decimal(31200) + tx = self.wallet.send_self_transfer(from_node=node, fee=fee / COIN) + # expected data + txid = tx["txid"] + reason = "expiry" + vsize = tx["tx"].get_vsize() + + self.log.info("Fast-forwarding time to mempool expiry...") + entry_time = node.getmempoolentry(txid)["time"] + expiry_time = entry_time + 60 * 60 * DEFAULT_MEMPOOL_EXPIRY_HOURS + 5 + node.setmocktime(expiry_time) + + self.log.info("Triggering expiry...") + self.wallet.get_utxo(txid=txid) + self.wallet.send_self_transfer(from_node=node) + + self.log.info("Polling buffer...") + bpf.perf_buffer_poll(timeout=200) + + bpf.cleanup() + + self.log.info("Ensuring mempool:removed event was handled successfully...") + assert_equal(EXPECTED_REMOVED_EVENTS, handled_removed_events) + + def replaced_test(self): + """Replace one and two transactions in the mempool and make sure the tracepoint + returns the expected txids, vsizes, and fees.""" + + EXPECTED_REPLACED_EVENTS = 1 + handled_replaced_events = 0 + + self.log.info("Hooking into mempool:replaced tracepoint...") + node = self.nodes[0] + ctx = USDT(pid=node.process.pid) + ctx.enable_probe(probe="mempool:replaced", fn_name="trace_replaced") + bpf = BPF(text=MEMPOOL_TRACEPOINTS_PROGRAM, usdt_contexts=[ctx], debug=0) + + def handle_replaced_event(_, data, __): + nonlocal handled_replaced_events + event = bpf["replaced_events"].event(data) + assert_equal(replaced_txid, bytes(event.replaced_hash)[::-1].hex()) + assert_equal(replaced_vsize, event.replaced_vsize) + assert_equal(replaced_fee, event.replaced_fee) + assert_equal(replaced_entry_time, event.replaced_entry_time) + assert_equal(replacement_txid, bytes(event.replacement_hash)[::-1].hex()) + assert_equal(replacement_vsize, event.replacement_vsize) + assert_equal(replacement_fee, event.replacement_fee) + handled_replaced_events += 1 + + bpf["replaced_events"].open_perf_buffer(handle_replaced_event) + + self.log.info("Sending RBF transaction...") + utxo = self.wallet.get_utxo(mark_as_spent=True) + original_fee = Decimal(40000) + original_tx = self.wallet.send_self_transfer( + from_node=node, utxo_to_spend=utxo, fee=original_fee / COIN + ) + entry_time = node.getmempoolentry(original_tx["txid"])["time"] + + self.log.info("Sending replacement transaction...") + replacement_fee = Decimal(45000) + replacement_tx = self.wallet.send_self_transfer( + from_node=node, utxo_to_spend=utxo, fee=replacement_fee / COIN + ) + + # expected data + replaced_txid = original_tx["txid"] + replaced_vsize = original_tx["tx"].get_vsize() + replaced_fee = original_fee + replaced_entry_time = entry_time + replacement_txid = replacement_tx["txid"] + replacement_vsize = replacement_tx["tx"].get_vsize() + + self.log.info("Polling buffer...") + bpf.perf_buffer_poll(timeout=200) + + bpf.cleanup() + + self.log.info("Ensuring mempool:replaced event was handled successfully...") + assert_equal(EXPECTED_REPLACED_EVENTS, handled_replaced_events) + + def rejected_test(self): + """Create an invalid transaction and make sure the tracepoint returns + the expected txid, rejection reason, peer id, and peer address.""" + + EXPECTED_REJECTED_EVENTS = 1 + handled_rejected_events = 0 + + self.log.info("Adding P2P connection...") + node = self.nodes[0] + node.add_p2p_connection(P2PDataStore()) + + self.log.info("Hooking into mempool:rejected tracepoint...") + ctx = USDT(pid=node.process.pid) + ctx.enable_probe(probe="mempool:rejected", fn_name="trace_rejected") + bpf = BPF(text=MEMPOOL_TRACEPOINTS_PROGRAM, usdt_contexts=[ctx], debug=0) + + def handle_rejected_event(_, data, __): + nonlocal handled_rejected_events + event = bpf["rejected_events"].event(data) + assert_equal(txid, bytes(event.hash)[::-1].hex()) + assert_equal(reason, event.reason.decode("UTF-8")) + handled_rejected_events += 1 + + bpf["rejected_events"].open_perf_buffer(handle_rejected_event) + + self.log.info("Sending invalid transaction...") + tx = self.wallet.create_self_transfer(fee_rate=Decimal(0)) + node.p2ps[0].send_txs_and_test([tx["tx"]], node, success=False) + + # expected data + txid = tx["tx"].hash + reason = "min relay fee not met" + + self.log.info("Polling buffer...") + bpf.perf_buffer_poll(timeout=200) + + bpf.cleanup() + + self.log.info("Ensuring mempool:rejected event was handled successfully...") + assert_equal(EXPECTED_REJECTED_EVENTS, handled_rejected_events) + + def run_test(self): + """Tests the mempool:added, mempool:removed, mempool:replaced, + and mempool:rejected tracepoints.""" + + # Create some coinbase transactions and mature them so they can be spent + node = self.nodes[0] + self.wallet = MiniWallet(node) + self.generate(self.wallet, 4) + self.generate(node, COINBASE_MATURITY) + + # Test individual tracepoints + self.added_test() + self.removed_test() + self.replaced_test() + self.rejected_test() + + +if __name__ == "__main__": + MempoolTracepointTest().main() diff --git a/test/functional/mempool_sigoplimit.py b/test/functional/mempool_sigoplimit.py new file mode 100755 index 0000000000..b178b9feda --- /dev/null +++ b/test/functional/mempool_sigoplimit.py @@ -0,0 +1,154 @@ +#!/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. +"""Test sigop limit mempool policy (`-bytespersigop` parameter)""" +from math import ceil + +from test_framework.messages import ( + COutPoint, + CTransaction, + CTxIn, + CTxInWitness, + CTxOut, + WITNESS_SCALE_FACTOR, + tx_from_hex, +) +from test_framework.script import ( + CScript, + OP_CHECKMULTISIG, + OP_CHECKSIG, + OP_ENDIF, + OP_FALSE, + OP_IF, + OP_RETURN, + OP_TRUE, +) +from test_framework.script_util import ( + script_to_p2wsh_script, +) +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 + + +DEFAULT_BYTES_PER_SIGOP = 20 # default setting + + +class BytesPerSigOpTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + # allow large datacarrier output to pad transactions + self.extra_args = [['-datacarriersize=100000']] + + def create_p2wsh_spending_tx(self, witness_script, output_script): + """Create a 1-input-1-output P2WSH spending transaction with only the + witness script in the witness stack and the given output script.""" + # create P2WSH address and fund it via MiniWallet first + txid, vout = self.wallet.send_to( + from_node=self.nodes[0], + scriptPubKey=script_to_p2wsh_script(witness_script), + amount=1000000, + ) + + # create spending transaction + tx = CTransaction() + tx.vin = [CTxIn(COutPoint(int(txid, 16), vout))] + tx.wit.vtxinwit = [CTxInWitness()] + tx.wit.vtxinwit[0].scriptWitness.stack = [bytes(witness_script)] + tx.vout = [CTxOut(500000, output_script)] + return tx + + def test_sigops_limit(self, bytes_per_sigop, num_sigops): + sigop_equivalent_vsize = ceil(num_sigops * bytes_per_sigop / WITNESS_SCALE_FACTOR) + self.log.info(f"- {num_sigops} sigops (equivalent size of {sigop_equivalent_vsize} vbytes)") + + # create a template tx with the specified sigop cost in the witness script + # (note that the sigops count even though being in a branch that's not executed) + num_multisigops = num_sigops // 20 + num_singlesigops = num_sigops % 20 + witness_script = CScript( + [OP_FALSE, OP_IF] + + [OP_CHECKMULTISIG]*num_multisigops + + [OP_CHECKSIG]*num_singlesigops + + [OP_ENDIF, OP_TRUE] + ) + # use a 256-byte data-push as lower bound in the output script, in order + # to avoid having to compensate for tx size changes caused by varying + # length serialization sizes (both for scriptPubKey and data-push lengths) + tx = self.create_p2wsh_spending_tx(witness_script, CScript([OP_RETURN, b'X'*256])) + + # bump the tx to reach the sigop-limit equivalent size by padding the datacarrier output + assert_greater_than_or_equal(sigop_equivalent_vsize, tx.get_vsize()) + vsize_to_pad = sigop_equivalent_vsize - tx.get_vsize() + tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad)]) + assert_equal(sigop_equivalent_vsize, tx.get_vsize()) + + res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0] + assert_equal(res['allowed'], True) + assert_equal(res['vsize'], sigop_equivalent_vsize) + + # increase the tx's vsize to be right above the sigop-limit equivalent size + # => tx's vsize in mempool should also grow accordingly + tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad+1)]) + res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0] + assert_equal(res['allowed'], True) + assert_equal(res['vsize'], sigop_equivalent_vsize+1) + + # decrease the tx's vsize to be right below the sigop-limit equivalent size + # => tx's vsize in mempool should stick at the sigop-limit equivalent + # bytes level, as it is higher than the tx's serialized vsize + # (the maximum of both is taken) + tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad-1)]) + res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0] + assert_equal(res['allowed'], True) + assert_equal(res['vsize'], sigop_equivalent_vsize) + + # check that the ancestor and descendant size calculations in the mempool + # also use the same max(sigop_equivalent_vsize, serialized_vsize) logic + # (to keep it simple, we only test the case here where the sigop vsize + # is much larger than the serialized vsize, i.e. we create a small child + # tx by getting rid of the large padding output) + tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'test123']) + assert_greater_than(sigop_equivalent_vsize, tx.get_vsize()) + self.nodes[0].sendrawtransaction(hexstring=tx.serialize().hex(), maxburnamount='1.0') + + # fetch parent tx, which doesn't contain any sigops + parent_txid = tx.vin[0].prevout.hash.to_bytes(32, 'big').hex() + parent_tx = tx_from_hex(self.nodes[0].getrawtransaction(txid=parent_txid)) + + entry_child = self.nodes[0].getmempoolentry(tx.rehash()) + assert_equal(entry_child['descendantcount'], 1) + assert_equal(entry_child['descendantsize'], sigop_equivalent_vsize) + assert_equal(entry_child['ancestorcount'], 2) + assert_equal(entry_child['ancestorsize'], sigop_equivalent_vsize + parent_tx.get_vsize()) + + entry_parent = self.nodes[0].getmempoolentry(parent_tx.rehash()) + assert_equal(entry_parent['ancestorcount'], 1) + assert_equal(entry_parent['ancestorsize'], parent_tx.get_vsize()) + assert_equal(entry_parent['descendantcount'], 2) + assert_equal(entry_parent['descendantsize'], parent_tx.get_vsize() + sigop_equivalent_vsize) + + def run_test(self): + self.wallet = MiniWallet(self.nodes[0]) + + for bytes_per_sigop in (DEFAULT_BYTES_PER_SIGOP, 43, 81, 165, 327, 649, 1072): + if bytes_per_sigop == DEFAULT_BYTES_PER_SIGOP: + self.log.info(f"Test default sigops limit setting ({bytes_per_sigop} bytes per sigop)...") + else: + bytespersigop_parameter = f"-bytespersigop={bytes_per_sigop}" + self.log.info(f"Test sigops limit setting {bytespersigop_parameter}...") + self.restart_node(0, extra_args=[bytespersigop_parameter] + self.extra_args[0]) + + for num_sigops in (69, 101, 142, 183, 222): + self.test_sigops_limit(bytes_per_sigop, num_sigops) + + self.generate(self.wallet, 1) + + +if __name__ == '__main__': + BytesPerSigOpTest().main() diff --git a/test/functional/mining_getblocktemplate_longpoll.py b/test/functional/mining_getblocktemplate_longpoll.py index ec492f9e72..53182eb79e 100755 --- a/test/functional/mining_getblocktemplate_longpoll.py +++ b/test/functional/mining_getblocktemplate_longpoll.py @@ -4,7 +4,6 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test longpolling with getblocktemplate.""" -from decimal import Decimal import random import threading @@ -47,9 +46,9 @@ class GetBlockTemplateLPTest(BitcoinTestFramework): thr.join(5) # wait 5 seconds or until thread exits assert thr.is_alive() - miniwallets = [MiniWallet(node) for node in self.nodes] + self.miniwallet = MiniWallet(self.nodes[0]) self.log.info("Test that longpoll will terminate if another node generates a block") - self.generate(miniwallets[1], 1) # generate a block on another node + self.generate(self.nodes[1], 1) # generate a block on another node # check that thread will exit now that new transaction entered mempool thr.join(5) # wait 5 seconds or until thread exits assert not thr.is_alive() @@ -57,18 +56,15 @@ class GetBlockTemplateLPTest(BitcoinTestFramework): self.log.info("Test that longpoll will terminate if we generate a block ourselves") thr = LongpollThread(self.nodes[0]) thr.start() - self.generate(miniwallets[0], 1) # generate a block on own node + self.generate(self.nodes[0], 1) # generate a block on own node thr.join(5) # wait 5 seconds or until thread exits assert not thr.is_alive() self.log.info("Test that introducing a new transaction into the mempool will terminate the longpoll") thr = LongpollThread(self.nodes[0]) thr.start() - # generate a random transaction and submit it - min_relay_fee = self.nodes[0].getnetworkinfo()["relayfee"] - fee_rate = min_relay_fee + Decimal('0.00000010') * random.randint(0,20) - miniwallets[0].send_self_transfer(from_node=random.choice(self.nodes), - fee_rate=fee_rate) + # generate a transaction and submit it + self.miniwallet.send_self_transfer(from_node=random.choice(self.nodes)) # after one minute, every 10 seconds the mempool is probed, so in 80 seconds it should have returned thr.join(60 + 20) assert not thr.is_alive() diff --git a/test/functional/p2p_invalid_messages.py b/test/functional/p2p_invalid_messages.py index ea4999a965..644abda914 100755 --- a/test/functional/p2p_invalid_messages.py +++ b/test/functional/p2p_invalid_messages.py @@ -80,6 +80,11 @@ class InvalidMessagesTest(BitcoinTestFramework): def test_buffer(self): self.log.info("Test message with header split across two buffers is received") conn = self.nodes[0].add_p2p_connection(P2PDataStore()) + # After add_p2p_connection both sides have the verack processed. + # However the pong from conn in reply to the ping from the node has not + # been processed and recorded in totalbytesrecv. + # Flush the pong from conn by sending a ping from conn. + conn.sync_with_ping(timeout=1) # Create valid message msg = conn.build_message(msg_ping(nonce=12345)) cut_pos = 12 # Chosen at an arbitrary position within the header @@ -89,8 +94,6 @@ class InvalidMessagesTest(BitcoinTestFramework): # Wait until node has processed the first half of the message self.wait_until(lambda: self.nodes[0].getnettotals()['totalbytesrecv'] != before) middle = self.nodes[0].getnettotals()['totalbytesrecv'] - # If this assert fails, we've hit an unlikely race - # where the test framework sent a message in between the two halves assert_equal(middle, before + cut_pos) conn.send_raw_message(msg[cut_pos:]) conn.sync_with_ping(timeout=1) diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index 7a0cedb1f5..6022042c11 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -69,6 +69,7 @@ class BlockchainTest(BitcoinTestFramework): def run_test(self): self.wallet = MiniWallet(self.nodes[0]) + self._test_prune_disk_space() self.mine_chain() self._test_max_future_block_time() self.restart_node( @@ -100,6 +101,13 @@ class BlockchainTest(BitcoinTestFramework): self.generate(self.wallet, 1) assert_equal(self.nodes[0].getblockchaininfo()['blocks'], HEIGHT) + def _test_prune_disk_space(self): + self.log.info("Test that a manually pruned node does not run into " + "integer overflow on first start up") + self.restart_node(0, extra_args=["-prune=1"]) + self.log.info("Avoid warning when assumed chain size is enough") + self.restart_node(0, extra_args=["-prune=123456789"]) + def _test_max_future_block_time(self): self.stop_node(0) self.log.info("A block tip of more than MAX_FUTURE_BLOCK_TIME in the future raises an error") diff --git a/test/functional/rpc_generate.py b/test/functional/rpc_generate.py index 8948ccb48d..20f62079fd 100755 --- a/test/functional/rpc_generate.py +++ b/test/functional/rpc_generate.py @@ -29,8 +29,13 @@ class RPCGenerateTest(BitcoinTestFramework): node = self.nodes[0] miniwallet = MiniWallet(node) - self.log.info('Generate an empty block to address') + self.log.info('Mine an empty block to address and return the hex') address = miniwallet.get_address() + generated_block = self.generateblock(node, output=address, transactions=[], submit=False) + node.submitblock(hexdata=generated_block['hex']) + assert_equal(generated_block['hash'], node.getbestblockhash()) + + self.log.info('Generate an empty block to address') hash = self.generateblock(node, output=address, transactions=[])['hash'] block = node.getblock(blockhash=hash, verbose=2) assert_equal(len(block['tx']), 1) diff --git a/test/functional/rpc_getblockfrompeer.py b/test/functional/rpc_getblockfrompeer.py index dddc779763..2f093bebff 100755 --- a/test/functional/rpc_getblockfrompeer.py +++ b/test/functional/rpc_getblockfrompeer.py @@ -24,14 +24,19 @@ from test_framework.util import ( class GetBlockFromPeerTest(BitcoinTestFramework): def set_test_params(self): - self.num_nodes = 2 + self.num_nodes = 3 + self.extra_args = [ + [], + [], + ["-fastprune", "-prune=1"] + ] def setup_network(self): self.setup_nodes() - def check_for_block(self, hash): + def check_for_block(self, node, hash): try: - self.nodes[0].getblock(hash) + self.nodes[node].getblock(hash) return True except JSONRPCException: return False @@ -48,7 +53,7 @@ class GetBlockFromPeerTest(BitcoinTestFramework): self.log.info("Connect nodes to sync headers") self.connect_nodes(0, 1) - self.sync_blocks() + self.sync_blocks(self.nodes[0:2]) self.log.info("Node 0 should only have the header for node 1's block 3") x = next(filter(lambda x: x['hash'] == short_tip, self.nodes[0].getchaintips())) @@ -81,7 +86,7 @@ class GetBlockFromPeerTest(BitcoinTestFramework): self.log.info("Successful fetch") result = self.nodes[0].getblockfrompeer(short_tip, peer_0_peer_1_id) - self.wait_until(lambda: self.check_for_block(short_tip), timeout=1) + self.wait_until(lambda: self.check_for_block(node=0, hash=short_tip), timeout=1) assert_equal(result, {}) self.log.info("Don't fetch blocks we already have") @@ -111,6 +116,40 @@ class GetBlockFromPeerTest(BitcoinTestFramework): error_msg = "In prune mode, only blocks that the node has already synced previously can be fetched from a peer" assert_raises_rpc_error(-1, error_msg, self.nodes[1].getblockfrompeer, blockhash, node1_interface_id) + self.log.info("Connect pruned node") + # We need to generate more blocks to be able to prune + self.connect_nodes(0, 2) + pruned_node = self.nodes[2] + self.generate(self.nodes[0], 400, sync_fun=self.no_op) + self.sync_blocks([self.nodes[0], pruned_node]) + pruneheight = pruned_node.pruneblockchain(300) + assert_equal(pruneheight, 248) + # Ensure the block is actually pruned + pruned_block = self.nodes[0].getblockhash(2) + assert_raises_rpc_error(-1, "Block not available (pruned data)", pruned_node.getblock, pruned_block) + + self.log.info("Fetch pruned block") + peers = pruned_node.getpeerinfo() + assert_equal(len(peers), 1) + pruned_node_peer_0_id = peers[0]["id"] + result = pruned_node.getblockfrompeer(pruned_block, pruned_node_peer_0_id) + self.wait_until(lambda: self.check_for_block(node=2, hash=pruned_block), timeout=1) + assert_equal(result, {}) + + self.log.info("Fetched block persists after next pruning event") + self.generate(self.nodes[0], 250, sync_fun=self.no_op) + self.sync_blocks([self.nodes[0], pruned_node]) + pruneheight += 251 + assert_equal(pruned_node.pruneblockchain(700), pruneheight) + assert_equal(pruned_node.getblock(pruned_block)["hash"], "36c56c5b5ebbaf90d76b0d1a074dcb32d42abab75b7ec6fa0ffd9b4fbce8f0f7") + + self.log.info("Fetched block can be pruned again when prune height exceeds the height of the tip at the time when the block was fetched") + self.generate(self.nodes[0], 250, sync_fun=self.no_op) + self.sync_blocks([self.nodes[0], pruned_node]) + pruneheight += 250 + assert_equal(pruned_node.pruneblockchain(1000), pruneheight) + assert_raises_rpc_error(-1, "Block not available (pruned data)", pruned_node.getblock, pruned_block) + if __name__ == '__main__': GetBlockFromPeerTest().main() diff --git a/test/functional/rpc_invalid_address_message.py b/test/functional/rpc_invalid_address_message.py index 0c29efb85a..fd282a9bc1 100755 --- a/test/functional/rpc_invalid_address_message.py +++ b/test/functional/rpc_invalid_address_message.py @@ -64,7 +64,7 @@ class InvalidAddressErrorMessageTest(BitcoinTestFramework): def test_validateaddress(self): # Invalid Bech32 self.check_invalid(BECH32_INVALID_SIZE, 'Invalid Bech32 address data size') - self.check_invalid(BECH32_INVALID_PREFIX, 'Not a valid Bech32 or Base58 encoding') + self.check_invalid(BECH32_INVALID_PREFIX, 'Invalid or unsupported Segwit (Bech32) or Base58 encoding.') self.check_invalid(BECH32_INVALID_BECH32, 'Version 1+ witness address must use Bech32m checksum') self.check_invalid(BECH32_INVALID_BECH32M, 'Version 0 witness address must use Bech32 checksum') self.check_invalid(BECH32_INVALID_VERSION, 'Invalid Bech32 address witness version') @@ -84,16 +84,16 @@ class InvalidAddressErrorMessageTest(BitcoinTestFramework): self.check_valid(BECH32_VALID_MULTISIG) # Invalid Base58 - self.check_invalid(BASE58_INVALID_PREFIX, 'Invalid prefix for Base58-encoded address') - self.check_invalid(BASE58_INVALID_CHECKSUM, 'Invalid checksum or length of Base58 address') - self.check_invalid(BASE58_INVALID_LENGTH, 'Invalid checksum or length of Base58 address') + self.check_invalid(BASE58_INVALID_PREFIX, 'Invalid or unsupported Base58-encoded address.') + self.check_invalid(BASE58_INVALID_CHECKSUM, 'Invalid checksum or length of Base58 address (P2PKH or P2SH)') + self.check_invalid(BASE58_INVALID_LENGTH, 'Invalid checksum or length of Base58 address (P2PKH or P2SH)') # Valid Base58 self.check_valid(BASE58_VALID) # Invalid address format - self.check_invalid(INVALID_ADDRESS, 'Not a valid Bech32 or Base58 encoding') - self.check_invalid(INVALID_ADDRESS_2, 'Not a valid Bech32 or Base58 encoding') + self.check_invalid(INVALID_ADDRESS, 'Invalid or unsupported Segwit (Bech32) or Base58 encoding.') + self.check_invalid(INVALID_ADDRESS_2, 'Invalid or unsupported Segwit (Bech32) or Base58 encoding.') node = self.nodes[0] @@ -106,9 +106,9 @@ class InvalidAddressErrorMessageTest(BitcoinTestFramework): node = self.nodes[0] assert_raises_rpc_error(-5, "Invalid Bech32 address data size", node.getaddressinfo, BECH32_INVALID_SIZE) - assert_raises_rpc_error(-5, "Not a valid Bech32 or Base58 encoding", node.getaddressinfo, BECH32_INVALID_PREFIX) - assert_raises_rpc_error(-5, "Invalid prefix for Base58-encoded address", node.getaddressinfo, BASE58_INVALID_PREFIX) - assert_raises_rpc_error(-5, "Not a valid Bech32 or Base58 encoding", node.getaddressinfo, INVALID_ADDRESS) + assert_raises_rpc_error(-5, "Invalid or unsupported Segwit (Bech32) or Base58 encoding.", node.getaddressinfo, BECH32_INVALID_PREFIX) + assert_raises_rpc_error(-5, "Invalid or unsupported Base58-encoded address.", node.getaddressinfo, BASE58_INVALID_PREFIX) + assert_raises_rpc_error(-5, "Invalid or unsupported Segwit (Bech32) or Base58 encoding.", node.getaddressinfo, INVALID_ADDRESS) def run_test(self): self.test_validateaddress() diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index 58a80e37a2..0fc0c0df8b 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -4,7 +4,6 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the Partially Signed Transaction RPCs. """ - from decimal import Decimal from itertools import product @@ -27,6 +26,7 @@ from test_framework.psbt import ( PSBT_IN_SHA256, PSBT_IN_HASH160, PSBT_IN_HASH256, + PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_WITNESS_UTXO, PSBT_OUT_TAP_TREE, ) @@ -59,13 +59,16 @@ class PSBTTest(BitcoinTestFramework): ["-walletrbf=0", "-changetype=legacy"], [] ] + # whitelist peers to speed up tx relay / mempool sync + for args in self.extra_args: + args.append("-whitelist=noban@127.0.0.1") self.supports_cli = False def skip_test_if_missing_module(self): self.skip_if_no_wallet() - # TODO: Re-enable this test with segwit v1 def test_utxo_conversion(self): + self.log.info("Check that non-witness UTXOs are removed for segwit v1+ inputs") mining_node = self.nodes[2] offline_node = self.nodes[0] online_node = self.nodes[1] @@ -77,34 +80,41 @@ class PSBTTest(BitcoinTestFramework): # Create watchonly on online_node online_node.createwallet(wallet_name='wonline', disable_private_keys=True) wonline = online_node.get_wallet_rpc('wonline') - w2 = online_node.get_wallet_rpc('') + w2 = online_node.get_wallet_rpc(self.default_wallet_name) # Mine a transaction that credits the offline address - offline_addr = offline_node.getnewaddress(address_type="p2sh-segwit") - online_addr = w2.getnewaddress(address_type="p2sh-segwit") + offline_addr = offline_node.getnewaddress(address_type="bech32m") + online_addr = w2.getnewaddress(address_type="bech32m") wonline.importaddress(offline_addr, "", False) - mining_node.sendtoaddress(address=offline_addr, amount=1.0) - self.generate(mining_node, nblocks=1) + mining_wallet = mining_node.get_wallet_rpc(self.default_wallet_name) + mining_wallet.sendtoaddress(address=offline_addr, amount=1.0) + self.generate(mining_node, nblocks=1, sync_fun=lambda: self.sync_all([online_node, mining_node])) - # Construct an unsigned PSBT on the online node (who doesn't know the output is Segwit, so will include a non-witness UTXO) + # Construct an unsigned PSBT on the online node utxos = wonline.listunspent(addresses=[offline_addr]) raw = wonline.createrawtransaction([{"txid":utxos[0]["txid"], "vout":utxos[0]["vout"]}],[{online_addr:0.9999}]) psbt = wonline.walletprocesspsbt(online_node.converttopsbt(raw))["psbt"] - assert "non_witness_utxo" in mining_node.decodepsbt(psbt)["inputs"][0] + assert not "not_witness_utxo" in mining_node.decodepsbt(psbt)["inputs"][0] + + # add non-witness UTXO manually + psbt_new = PSBT.from_base64(psbt) + prev_tx = wonline.gettransaction(utxos[0]["txid"])["hex"] + psbt_new.i[0].map[PSBT_IN_NON_WITNESS_UTXO] = bytes.fromhex(prev_tx) + assert "non_witness_utxo" in mining_node.decodepsbt(psbt_new.to_base64())["inputs"][0] - # Have the offline node sign the PSBT (which will update the UTXO to segwit) - signed_psbt = offline_node.walletprocesspsbt(psbt)["psbt"] - assert "witness_utxo" in mining_node.decodepsbt(signed_psbt)["inputs"][0] + # Have the offline node sign the PSBT (which will remove the non-witness UTXO) + signed_psbt = offline_node.walletprocesspsbt(psbt_new.to_base64())["psbt"] + assert not "non_witness_utxo" in mining_node.decodepsbt(signed_psbt)["inputs"][0] # Make sure we can mine the resulting transaction txid = mining_node.sendrawtransaction(mining_node.finalizepsbt(signed_psbt)["hex"]) - self.generate(mining_node, 1) + self.generate(mining_node, nblocks=1, sync_fun=lambda: self.sync_all([online_node, mining_node])) assert_equal(online_node.gettxout(txid,0)["confirmations"], 1) wonline.unloadwallet() # Reconnect - self.connect_nodes(0, 1) + self.connect_nodes(1, 0) self.connect_nodes(0, 2) def test_input_confs_control(self): @@ -571,8 +581,8 @@ class PSBTTest(BitcoinTestFramework): for i, signer in enumerate(signers): self.nodes[2].unloadwallet("wallet{}".format(i)) - # TODO: Re-enable this for segwit v1 - # self.test_utxo_conversion() + if self.options.descriptors: + self.test_utxo_conversion() self.test_input_confs_control() @@ -931,6 +941,9 @@ class PSBTTest(BitcoinTestFramework): assert_equal(self.nodes[0].finalizepsbt(psbt.to_base64()), {'hex': '0200000001dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd0000000000000000000100000000000000000000000000', 'complete': True}) + self.log.info("Test we don't crash when making a 0-value funded transaction at 0 fee without forcing an input selection") + assert_raises_rpc_error(-4, "Transaction requires one destination of non-0 value, a non-0 feerate, or a pre-selected input", self.nodes[0].walletcreatefundedpsbt, [], [{"data": "deadbeef"}], 0, {"fee_rate": "0"}) + if __name__ == '__main__': PSBTTest().main() diff --git a/test/functional/test_framework/p2p.py b/test/functional/test_framework/p2p.py index c5768177bd..2433e52671 100755 --- a/test/functional/test_framework/p2p.py +++ b/test/functional/test_framework/p2p.py @@ -464,7 +464,7 @@ class P2PInterface(P2PConnection): def wait_for_connect(self, timeout=60): test_function = lambda: self.is_connected - wait_until_helper(test_function, timeout=timeout, lock=p2p_lock) + self.wait_until(test_function, timeout=timeout, check_connected=False) def wait_for_disconnect(self, timeout=60): test_function = lambda: not self.is_connected diff --git a/test/functional/test_framework/psbt.py b/test/functional/test_framework/psbt.py index 3a5b4ec74d..1eff4a250e 100644 --- a/test/functional/test_framework/psbt.py +++ b/test/functional/test_framework/psbt.py @@ -105,8 +105,8 @@ class PSBT: def deserialize(self, f): assert f.read(5) == b"psbt\xff" self.g = from_binary(PSBTMap, f) - assert 0 in self.g.map - self.tx = from_binary(CTransaction, self.g.map[0]) + assert PSBT_GLOBAL_UNSIGNED_TX in self.g.map + self.tx = from_binary(CTransaction, self.g.map[PSBT_GLOBAL_UNSIGNED_TX]) self.i = [from_binary(PSBTMap, f) for _ in self.tx.vin] self.o = [from_binary(PSBTMap, f) for _ in self.tx.vout] return self @@ -115,8 +115,8 @@ class PSBT: assert isinstance(self.g, PSBTMap) assert isinstance(self.i, list) and all(isinstance(x, PSBTMap) for x in self.i) assert isinstance(self.o, list) and all(isinstance(x, PSBTMap) for x in self.o) - assert 0 in self.g.map - tx = from_binary(CTransaction, self.g.map[0]) + assert PSBT_GLOBAL_UNSIGNED_TX in self.g.map + tx = from_binary(CTransaction, self.g.map[PSBT_GLOBAL_UNSIGNED_TX]) assert len(tx.vin) == len(self.i) assert len(tx.vout) == len(self.o) @@ -130,7 +130,7 @@ class PSBT: for m in self.i + self.o: m.map.clear() - self.g = PSBTMap(map={0: self.g.map[0]}) + self.g = PSBTMap(map={PSBT_GLOBAL_UNSIGNED_TX: self.g.map[PSBT_GLOBAL_UNSIGNED_TX]}) def to_base64(self): return base64.b64encode(self.serialize()).decode("utf8") diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 9620951a16..66a23b443c 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -120,8 +120,6 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): self.disable_autoconnect = True self.set_test_params() assert self.wallet_names is None or len(self.wallet_names) <= self.num_nodes - if self.options.timeout_factor == 0 : - self.options.timeout_factor = 99999 self.rpc_timeout = int(self.rpc_timeout * self.options.timeout_factor) # optionally, increase timeout by a factor def main(self): @@ -193,7 +191,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): help="run nodes under the valgrind memory error detector: expect at least a ~10x slowdown. valgrind 3.14 or later required. Forces --nosandbox.") parser.add_argument("--randomseed", type=int, help="set a random seed for deterministically reproducing a previous test run") - parser.add_argument('--timeout-factor', dest="timeout_factor", type=float, default=1.0, help='adjust test timeouts by a factor. Setting it to 0 disables all timeouts') + parser.add_argument("--timeout-factor", dest="timeout_factor", type=float, help="adjust test timeouts by a factor. Setting it to 0 disables all timeouts") self.add_options(parser) # Running TestShell in a Jupyter notebook causes an additional -f argument @@ -201,6 +199,9 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): # source: https://stackoverflow.com/questions/48796169/how-to-fix-ipykernel-launcher-py-error-unrecognized-arguments-in-jupyter/56349168#56349168 parser.add_argument("-f", "--fff", help="a dummy argument to fool ipython", default="1") self.options = parser.parse_args() + if self.options.timeout_factor == 0: + self.options.timeout_factor = 99999 + self.options.timeout_factor = self.options.timeout_factor or (4 if self.options.valgrind else 1) self.options.previous_releases_path = previous_releases_path config = configparser.ConfigParser() diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index 882f82e0f2..56abe5f26a 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -469,7 +469,7 @@ class TestNode(): return if time.time() >= time_end: - print_log = " - " + "\n - ".join(log.splitlines()) + print_log = " - " + "\n - ".join(log.decode("utf8", errors="replace").splitlines()) break # No sleep here because we want to detect the message fragment as fast as diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 1d560706c2..bcedc0c9af 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -195,6 +195,7 @@ BASE_SCRIPTS = [ 'interface_http.py', 'interface_rpc.py', 'interface_usdt_coinselection.py', + 'interface_usdt_mempool.py', 'interface_usdt_net.py', 'interface_usdt_utxocache.py', 'interface_usdt_validation.py', @@ -323,6 +324,7 @@ BASE_SCRIPTS = [ 'mempool_compatibility.py', 'mempool_accept_wtxid.py', 'mempool_dust.py', + 'mempool_sigoplimit.py', 'rpc_deriveaddresses.py', 'rpc_deriveaddresses.py --usecli', 'p2p_ping.py', diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py index 52022a2eee..53ac01686a 100755 --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -632,7 +632,7 @@ class WalletTest(BitcoinTestFramework): assert_equal(total_txs, len(self.nodes[0].listtransactions("*", 99999))) # Test getaddressinfo on external address. Note that these addresses are taken from disablewallet.py - assert_raises_rpc_error(-5, "Invalid prefix for Base58-encoded address", self.nodes[0].getaddressinfo, "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy") + assert_raises_rpc_error(-5, "Invalid or unsupported Base58-encoded address.", self.nodes[0].getaddressinfo, "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy") address_info = self.nodes[0].getaddressinfo("mneYUmWYsuk7kySiURxCi3AGxrAqZxLgPZ") assert_equal(address_info['address'], "mneYUmWYsuk7kySiURxCi3AGxrAqZxLgPZ") assert_equal(address_info["scriptPubKey"], "76a9144e3854046c7bd1594ac904e4793b6a45b36dea0988ac") diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py index e66eb2c289..4f2db2018a 100755 --- a/test/functional/wallet_importdescriptors.py +++ b/test/functional/wallet_importdescriptors.py @@ -15,6 +15,9 @@ variants. - `test_address()` is called to call getaddressinfo for an address on node1 and test the values returned.""" +import concurrent.futures + +from test_framework.authproxy import JSONRPCException from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.descriptors import descsum_create @@ -687,11 +690,25 @@ class ImportDescriptorsTest(BitcoinTestFramework): descriptor["timestamp"] = 0 descriptor["next_index"] = 0 - batch = [] - batch.append(encrypted_wallet.walletpassphrase.get_request("passphrase", 3)) - batch.append(encrypted_wallet.importdescriptors.get_request([descriptor])) + encrypted_wallet.walletpassphrase("passphrase", 99999) + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as thread: + with self.nodes[0].assert_debug_log(expected_msgs=["Rescan started from block 0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206... (slow variant inspecting all blocks)"], timeout=5): + importing = thread.submit(encrypted_wallet.importdescriptors, requests=[descriptor]) + + # Set the passphrase timeout to 1 to test that the wallet remains unlocked during the rescan + self.nodes[0].cli("-rpcwallet=encrypted_wallet").walletpassphrase("passphrase", 1) + + try: + self.nodes[0].cli("-rpcwallet=encrypted_wallet").walletlock() + except JSONRPCException as e: + assert e.error["code"] == -4 and "Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before locking the wallet." in e.error["message"] + + try: + self.nodes[0].cli("-rpcwallet=encrypted_wallet").walletpassphrasechange("passphrase", "newpassphrase") + except JSONRPCException as e: + assert e.error["code"] == -4 and "Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before changing the passphrase." in e.error["message"] - encrypted_wallet.batch(batch) + assert_equal(importing.result(), [{"success": True}]) assert_equal(temp_wallet.getbalance(), encrypted_wallet.getbalance()) diff --git a/test/functional/wallet_listdescriptors.py b/test/functional/wallet_listdescriptors.py index fb2156bda1..c5479089c6 100755 --- a/test/functional/wallet_listdescriptors.py +++ b/test/functional/wallet_listdescriptors.py @@ -54,7 +54,7 @@ class ListDescriptorsTest(BitcoinTestFramework): assert_equal(4, len([d for d in result['descriptors'] if d['internal']])) for item in result['descriptors']: assert item['desc'] != '' - assert item['next'] == 0 + assert item['next_index'] == 0 assert item['range'] == [0, 0] assert item['timestamp'] is not None @@ -78,7 +78,8 @@ class ListDescriptorsTest(BitcoinTestFramework): 'timestamp': TIME_GENESIS_BLOCK, 'active': False, 'range': [0, 0], - 'next': 0}, + 'next': 0, + 'next_index': 0}, ], } assert_equal(expected, wallet.listdescriptors()) @@ -92,7 +93,8 @@ class ListDescriptorsTest(BitcoinTestFramework): 'timestamp': TIME_GENESIS_BLOCK, 'active': False, 'range': [0, 0], - 'next': 0}, + 'next': 0, + 'next_index': 0}, ], } assert_equal(expected_private, wallet.listdescriptors(True)) diff --git a/test/functional/wallet_transactiontime_rescan.py b/test/functional/wallet_transactiontime_rescan.py index 904013cdef..ea99992084 100755 --- a/test/functional/wallet_transactiontime_rescan.py +++ b/test/functional/wallet_transactiontime_rescan.py @@ -5,8 +5,10 @@ """Test transaction time during old block rescanning """ +import concurrent.futures import time +from test_framework.authproxy import JSONRPCException from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( @@ -196,14 +198,27 @@ class TransactionTimeRescanTest(BitcoinTestFramework): minernode.createwallet("encrypted_wallet", blank=True, passphrase="passphrase", descriptors=False) encrypted_wallet = minernode.get_wallet_rpc("encrypted_wallet") - encrypted_wallet.walletpassphrase("passphrase", 1) + encrypted_wallet.walletpassphrase("passphrase", 99999) encrypted_wallet.sethdseed(seed=hd_seed) - batch = [] - batch.append(encrypted_wallet.walletpassphrase.get_request("passphrase", 3)) - batch.append(encrypted_wallet.rescanblockchain.get_request()) + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as thread: + with minernode.assert_debug_log(expected_msgs=["Rescan started from block 0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206... (slow variant inspecting all blocks)"], timeout=5): + rescanning = thread.submit(encrypted_wallet.rescanblockchain) - encrypted_wallet.batch(batch) + # set the passphrase timeout to 1 to test that the wallet remains unlocked during the rescan + minernode.cli("-rpcwallet=encrypted_wallet").walletpassphrase("passphrase", 1) + + try: + minernode.cli("-rpcwallet=encrypted_wallet").walletlock() + except JSONRPCException as e: + assert e.error["code"] == -4 and "Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before locking the wallet." in e.error["message"] + + try: + minernode.cli("-rpcwallet=encrypted_wallet").walletpassphrasechange("passphrase", "newpassphrase") + except JSONRPCException as e: + assert e.error["code"] == -4 and "Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before changing the passphrase." in e.error["message"] + + assert_equal(rescanning.result(), {"start_height": 0, "stop_height": 803}) assert_equal(encrypted_wallet.getbalance(), temp_wallet.getbalance()) |