diff options
Diffstat (limited to 'test')
89 files changed, 2094 insertions, 849 deletions
diff --git a/test/README.md b/test/README.md index acd68d8d8f..c9e15c4968 100644 --- a/test/README.md +++ b/test/README.md @@ -275,12 +275,15 @@ Use the `-v` option for verbose output. #### Dependencies -| Lint test | Dependency | Version [used by CI](../ci/lint/04_install.sh) | Installation -|-----------|:----------:|:-------------------------------------------:|-------------- -| [`lint-python.sh`](lint/lint-python.sh) | [flake8](https://gitlab.com/pycqa/flake8) | [3.8.3](https://github.com/bitcoin/bitcoin/pull/19348) | `pip3 install flake8==3.8.3` -| [`lint-python.sh`](lint/lint-python.sh) | [mypy](https://github.com/python/mypy) | [0.781](https://github.com/bitcoin/bitcoin/pull/19348) | `pip3 install mypy==0.781` -| [`lint-shell.sh`](lint/lint-shell.sh) | [ShellCheck](https://github.com/koalaman/shellcheck) | [0.7.2](https://github.com/bitcoin/bitcoin/pull/21749) | [details...](https://github.com/koalaman/shellcheck#installing) -| [`lint-spelling.sh`](lint/lint-spelling.sh) | [codespell](https://github.com/codespell-project/codespell) | [2.0.0](https://github.com/bitcoin/bitcoin/pull/20817) | `pip3 install codespell==2.0.0` +| Lint test | Dependency | +|-----------|:----------:| +| [`lint-python.sh`](lint/lint-python.sh) | [flake8](https://gitlab.com/pycqa/flake8) +| [`lint-python.sh`](lint/lint-python.sh) | [mypy](https://github.com/python/mypy) +| [`lint-python.sh`](lint/lint-python.sh) | [pyzmq](https://github.com/zeromq/pyzmq) +| [`lint-shell.sh`](lint/lint-shell.sh) | [ShellCheck](https://github.com/koalaman/shellcheck) +| [`lint-spelling.sh`](lint/lint-spelling.sh) | [codespell](https://github.com/codespell-project/codespell) + +In use versions and install instructions are available in the [CI setup](../ci/lint/04_install.sh). Please be aware that on Linux distributions all dependencies are usually available as packages, but could be outdated. diff --git a/test/config.ini.in b/test/config.ini.in index db80bba6f1..8bcba1b39c 100644 --- a/test/config.ini.in +++ b/test/config.ini.in @@ -24,3 +24,4 @@ RPCAUTH=@abs_top_srcdir@/share/rpcauth/rpcauth.py @ENABLE_FUZZ_TRUE@ENABLE_FUZZ=true @ENABLE_ZMQ_TRUE@ENABLE_ZMQ=true @ENABLE_EXTERNAL_SIGNER_TRUE@ENABLE_EXTERNAL_SIGNER=true +@ENABLE_SYSCALL_SANDBOX_TRUE@ENABLE_SYSCALL_SANDBOX=true diff --git a/test/functional/README.md b/test/functional/README.md index d830ba0334..926810cf03 100644 --- a/test/functional/README.md +++ b/test/functional/README.md @@ -188,5 +188,5 @@ perf report -i /path/to/datadir/send-big-msgs.perf.data.xxxx --stdio | c++filt | #### See also: - [Installing perf](https://askubuntu.com/q/50145) -- [Perf examples](http://www.brendangregg.com/perf.html) +- [Perf examples](https://www.brendangregg.com/perf.html) - [Hotspot](https://github.com/KDAB/hotspot): a GUI for perf output analysis diff --git a/test/functional/combine_logs.py b/test/functional/combine_logs.py index 00f2833f55..71dfb4c01a 100755 --- a/test/functional/combine_logs.py +++ b/test/functional/combine_logs.py @@ -188,7 +188,7 @@ def print_logs_plain(log_events, colors): def print_logs_html(log_events): """Renders the iterator of log events into html.""" try: - import jinja2 + import jinja2 #type:ignore except ImportError: print("jinja2 not found. Try `pip install jinja2`") sys.exit(1) diff --git a/test/functional/data/__init__.py b/test/functional/data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/test/functional/data/__init__.py diff --git a/test/functional/feature_addrman.py b/test/functional/feature_addrman.py index ee421c89b5..93d50c1369 100755 --- a/test/functional/feature_addrman.py +++ b/test/functional/feature_addrman.py @@ -5,6 +5,7 @@ """Test addrman functionality""" import os +import re import struct from test_framework.messages import ser_uint256, hash256 @@ -14,22 +15,31 @@ from test_framework.test_node import ErrorMatch from test_framework.util import assert_equal -def serialize_addrman(*, format=1, lowest_compatible=3): +def serialize_addrman( + *, + format=1, + lowest_compatible=3, + net_magic="regtest", + bucket_key=1, + len_new=None, + len_tried=None, + mock_checksum=None, +): new = [] tried = [] INCOMPATIBILITY_BASE = 32 - r = MAGIC_BYTES["regtest"] + r = MAGIC_BYTES[net_magic] r += struct.pack("B", format) r += struct.pack("B", INCOMPATIBILITY_BASE + lowest_compatible) - r += ser_uint256(1) - r += struct.pack("i", len(new)) - r += struct.pack("i", len(tried)) + r += ser_uint256(bucket_key) + r += struct.pack("i", len_new or len(new)) + r += struct.pack("i", len_tried or len(tried)) ADDRMAN_NEW_BUCKET_COUNT = 1 << 10 r += struct.pack("i", ADDRMAN_NEW_BUCKET_COUNT ^ (1 << 30)) for _ in range(ADDRMAN_NEW_BUCKET_COUNT): r += struct.pack("i", 0) checksum = hash256(r) - r += checksum + r += mock_checksum or checksum return r @@ -47,7 +57,7 @@ class AddrmanTest(BitcoinTestFramework): init_error = lambda reason: ( f"Error: Invalid or corrupt peers.dat \\({reason}\\). If you believe this " f"is a bug, please report it to {self.config['environment']['PACKAGE_BUGREPORT']}. " - f'As a workaround, you can move the file \\("{peers_dat}"\\) out of the way \\(rename, ' + f'As a workaround, you can move the file \\("{re.escape(peers_dat)}"\\) out of the way \\(rename, ' "move, or delete\\) to have a new one created on the next start." ) @@ -70,7 +80,7 @@ class AddrmanTest(BitcoinTestFramework): match=ErrorMatch.FULL_REGEX, ) - self.log.info("Check that corrupt addrman cannot be read") + self.log.info("Check that corrupt addrman cannot be read (EOF)") self.stop_node(0) with open(peers_dat, "wb") as f: f.write(serialize_addrman()[:-1]) @@ -79,6 +89,46 @@ class AddrmanTest(BitcoinTestFramework): match=ErrorMatch.FULL_REGEX, ) + self.log.info("Check that corrupt addrman cannot be read (magic)") + self.stop_node(0) + write_addrman(peers_dat, net_magic="signet") + self.nodes[0].assert_start_raises_init_error( + expected_msg=init_error("Invalid network magic number"), + match=ErrorMatch.FULL_REGEX, + ) + + self.log.info("Check that corrupt addrman cannot be read (checksum)") + self.stop_node(0) + write_addrman(peers_dat, mock_checksum=b"ab" * 32) + self.nodes[0].assert_start_raises_init_error( + expected_msg=init_error("Checksum mismatch, data corrupted"), + match=ErrorMatch.FULL_REGEX, + ) + + self.log.info("Check that corrupt addrman cannot be read (len_tried)") + self.stop_node(0) + write_addrman(peers_dat, len_tried=-1) + self.nodes[0].assert_start_raises_init_error( + expected_msg=init_error("Corrupt AddrMan serialization: nTried=-1, should be in \\[0, 16384\\]:.*"), + match=ErrorMatch.FULL_REGEX, + ) + + self.log.info("Check that corrupt addrman cannot be read (len_new)") + self.stop_node(0) + write_addrman(peers_dat, len_new=-1) + self.nodes[0].assert_start_raises_init_error( + expected_msg=init_error("Corrupt AddrMan serialization: nNew=-1, should be in \\[0, 65536\\]:.*"), + match=ErrorMatch.FULL_REGEX, + ) + + self.log.info("Check that corrupt addrman cannot be read (failed check)") + self.stop_node(0) + write_addrman(peers_dat, bucket_key=0) + self.nodes[0].assert_start_raises_init_error( + expected_msg=init_error("Corrupt data. Consistency check failed with code -16: .*"), + match=ErrorMatch.FULL_REGEX, + ) + self.log.info("Check that missing addrman is recreated") self.stop_node(0) os.remove(peers_dat) diff --git a/test/functional/feature_asmap.py b/test/functional/feature_asmap.py index 704dd6126b..debd87962f 100755 --- a/test/functional/feature_asmap.py +++ b/test/functional/feature_asmap.py @@ -14,9 +14,11 @@ Verify node behaviour and debug log when launching bitcoind in these cases: 4. `bitcoind -asmap/-asmap=` with no file specified, using the default asmap -5. `bitcoind -asmap` with no file specified and a missing default asmap file +5. `bitcoind -asmap` restart with an addrman containing new and tried entries -6. `bitcoind -asmap` with an empty (unparsable) default asmap file +6. `bitcoind -asmap` with no file specified and a missing default asmap file + +7. `bitcoind -asmap` with an empty (unparsable) default asmap file The tests are order-independent. @@ -37,6 +39,12 @@ def expected_messages(filename): class AsmapTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 + self.extra_args = [["-checkaddrman=1"]] # Do addrman checks on all operations. + + def fill_addrman(self, node_id): + """Add 1 tried address to the addrman, followed by 1 new address.""" + for addr, tried in [[0, True], [1, False]]: + self.nodes[node_id].addpeeraddress(address=f"101.{addr}.0.0", tried=tried, port=8333) def test_without_asmap_arg(self): self.log.info('Test bitcoind with no -asmap arg passed') @@ -72,6 +80,22 @@ class AsmapTest(BitcoinTestFramework): self.start_node(0, [arg]) os.remove(self.default_asmap) + def test_asmap_interaction_with_addrman_containing_entries(self): + self.log.info("Test bitcoind -asmap restart with addrman containing new and tried entries") + self.stop_node(0) + shutil.copyfile(self.asmap_raw, self.default_asmap) + self.start_node(0, ["-asmap", "-checkaddrman=1"]) + self.fill_addrman(node_id=0) + self.restart_node(0, ["-asmap", "-checkaddrman=1"]) + with self.node.assert_debug_log( + expected_msgs=[ + "Addrman checks started: new 1, tried 1, total 2", + "Addrman checks completed successfully", + ] + ): + self.node.getnodeaddresses() # getnodeaddresses re-runs the addrman checks + os.remove(self.default_asmap) + def test_default_asmap_with_missing_file(self): self.log.info('Test bitcoind -asmap with missing default map file') self.stop_node(0) @@ -97,6 +121,7 @@ class AsmapTest(BitcoinTestFramework): self.test_asmap_with_absolute_path() self.test_asmap_with_relative_path() self.test_default_asmap() + self.test_asmap_interaction_with_addrman_containing_entries() self.test_default_asmap_with_missing_file() self.test_empty_asmap() diff --git a/test/functional/feature_bip68_sequence.py b/test/functional/feature_bip68_sequence.py index 09cda8444a..99ac1b5884 100755 --- a/test/functional/feature_bip68_sequence.py +++ b/test/functional/feature_bip68_sequence.py @@ -24,7 +24,6 @@ from test_framework.util import ( assert_equal, assert_greater_than, assert_raises_rpc_error, - satoshi_round, softfork_active, ) from test_framework.script_util import DUMMY_P2WPKH_SCRIPT @@ -41,8 +40,14 @@ class BIP68Test(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.extra_args = [ - ["-acceptnonstdtxn=1"], - ["-acceptnonstdtxn=0"], + [ + '-testactivationheight=csv@432', + "-acceptnonstdtxn=1", + ], + [ + '-testactivationheight=csv@432', + "-acceptnonstdtxn=0", + ], ] def skip_test_if_missing_module(self): @@ -88,7 +93,7 @@ class BIP68Test(BitcoinTestFramework): utxo = utxos[0] tx1 = CTransaction() - value = int(satoshi_round(utxo["amount"] - self.relayfee)*COIN) + value = int((utxo["amount"] - self.relayfee) * COIN) # Check that the disable flag disables relative locktime. # If sequence locks were used, this would require 1 block for the diff --git a/test/functional/feature_block.py b/test/functional/feature_block.py index 777787ed32..b06ea8542b 100755 --- a/test/functional/feature_block.py +++ b/test/functional/feature_block.py @@ -82,7 +82,10 @@ class FullBlockTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True - self.extra_args = [['-acceptnonstdtxn=1']] # This is a consensus block test, we don't care about tx policy + self.extra_args = [[ + '-acceptnonstdtxn=1', # This is a consensus block test, we don't care about tx policy + '-testactivationheight=bip34@2', + ]] def run_test(self): node = self.nodes[0] # convenience reference to the node diff --git a/test/functional/feature_cltv.py b/test/functional/feature_cltv.py index 2c3ef9b88b..3dc858f5d2 100755 --- a/test/functional/feature_cltv.py +++ b/test/functional/feature_cltv.py @@ -8,7 +8,6 @@ Test that the CHECKLOCKTIMEVERIFY soft-fork activates. """ from test_framework.blocktools import ( - CLTV_HEIGHT, create_block, create_coinbase, ) @@ -76,10 +75,14 @@ def cltv_validate(tx, height): cltv_modify_tx(tx, prepend_scriptsig=scheme[0], nsequence=scheme[1], nlocktime=scheme[2]) +CLTV_HEIGHT = 111 + + class BIP65Test(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.extra_args = [[ + f'-testactivationheight=cltv@{CLTV_HEIGHT}', '-whitelist=noban@127.0.0.1', '-par=1', # Use only one script thread to get the exact reject reason for testing '-acceptnonstdtxn=1', # cltv_invalidate is nonstandard diff --git a/test/functional/feature_coinstatsindex.py b/test/functional/feature_coinstatsindex.py index 146e776b07..c592d7bd69 100755 --- a/test/functional/feature_coinstatsindex.py +++ b/test/functional/feature_coinstatsindex.py @@ -164,7 +164,7 @@ class CoinStatsIndexTest(BitcoinTestFramework): # Generate and send another tx with an OP_RETURN output (which is unspendable) tx2 = CTransaction() tx2.vin.append(CTxIn(COutPoint(int(tx1_txid, 16), n), b'')) - tx2.vout.append(CTxOut(int(20.99 * COIN), CScript([OP_RETURN] + [OP_FALSE]*30))) + tx2.vout.append(CTxOut(int(Decimal('20.99') * COIN), CScript([OP_RETURN] + [OP_FALSE]*30))) tx2_hex = self.nodes[0].signrawtransactionwithwallet(tx2.serialize().hex())['hex'] self.nodes[0].sendrawtransaction(tx2_hex) @@ -175,16 +175,16 @@ class CoinStatsIndexTest(BitcoinTestFramework): for hash_option in index_hash_options: # Check all amounts were registered correctly res6 = index_node.gettxoutsetinfo(hash_option, 108) - assert_equal(res6['total_unspendable_amount'], Decimal('70.98999999')) + assert_equal(res6['total_unspendable_amount'], Decimal('70.99000000')) assert_equal(res6['block_info'], { - 'unspendable': Decimal('20.98999999'), + 'unspendable': Decimal('20.99000000'), 'prevout_spent': 111, 'new_outputs_ex_coinbase': Decimal('89.99993620'), - 'coinbase': Decimal('50.01006381'), + 'coinbase': Decimal('50.01006380'), 'unspendables': { 'genesis_block': 0, 'bip30': 0, - 'scripts': Decimal('20.98999999'), + 'scripts': Decimal('20.99000000'), 'unclaimed_rewards': 0 } }) @@ -206,7 +206,7 @@ class CoinStatsIndexTest(BitcoinTestFramework): for hash_option in index_hash_options: res7 = index_node.gettxoutsetinfo(hash_option, 109) - assert_equal(res7['total_unspendable_amount'], Decimal('80.98999999')) + assert_equal(res7['total_unspendable_amount'], Decimal('80.99000000')) assert_equal(res7['block_info'], { 'unspendable': 10, 'prevout_spent': 0, diff --git a/test/functional/feature_csv_activation.py b/test/functional/feature_csv_activation.py index d2b3fe45d1..5255b13bd1 100755 --- a/test/functional/feature_csv_activation.py +++ b/test/functional/feature_csv_activation.py @@ -41,7 +41,6 @@ from itertools import product import time from test_framework.blocktools import ( - CSV_ACTIVATION_HEIGHT, create_block, create_coinbase, ) @@ -89,12 +88,16 @@ def all_rlt_txs(txs): return [tx['tx'] for tx in txs] +CSV_ACTIVATION_HEIGHT = 432 + + class BIP68_112_113Test(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True self.extra_args = [[ '-whitelist=noban@127.0.0.1', + f'-testactivationheight=csv@{CSV_ACTIVATION_HEIGHT}', '-par=1', # Use only one script thread to get the exact reject reason for testing ]] self.supports_cli = False diff --git a/test/functional/feature_dersig.py b/test/functional/feature_dersig.py index 595d26611a..28aff1f2f9 100755 --- a/test/functional/feature_dersig.py +++ b/test/functional/feature_dersig.py @@ -8,7 +8,6 @@ Test the DERSIG soft-fork activation on regtest. """ from test_framework.blocktools import ( - DERSIG_HEIGHT, create_block, create_coinbase, ) @@ -42,10 +41,14 @@ def unDERify(tx): tx.vin[0].scriptSig = CScript(newscript) +DERSIG_HEIGHT = 102 + + class BIP66Test(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.extra_args = [[ + f'-testactivationheight=dersig@{DERSIG_HEIGHT}', '-whitelist=noban@127.0.0.1', '-par=1', # Use only one script thread to get the exact log msg for testing ]] @@ -83,7 +86,6 @@ class BIP66Test(BitcoinTestFramework): tip = self.nodes[0].getbestblockhash() block_time = self.nodes[0].getblockheader(tip)['mediantime'] + 1 block = create_block(int(tip, 16), create_coinbase(DERSIG_HEIGHT - 1), block_time) - block.nVersion = 2 block.vtx.append(spendtx) block.hashMerkleRoot = block.calc_merkle_root() block.rehash() @@ -110,7 +112,7 @@ class BIP66Test(BitcoinTestFramework): peer.sync_with_ping() self.log.info("Test that transactions with non-DER signatures cannot appear in a block") - block.nVersion = 3 + block.nVersion = 4 spendtx = self.create_tx(self.coinbase_txids[1]) unDERify(spendtx) @@ -139,7 +141,7 @@ class BIP66Test(BitcoinTestFramework): assert_equal(int(self.nodes[0].getbestblockhash(), 16), tip) peer.sync_with_ping() - self.log.info("Test that a version 3 block with a DERSIG-compliant transaction is accepted") + self.log.info("Test that a block with a DERSIG-compliant transaction is accepted") block.vtx[1] = self.create_tx(self.coinbase_txids[1]) block.hashMerkleRoot = block.calc_merkle_root() block.rehash() diff --git a/test/functional/feature_fee_estimation.py b/test/functional/feature_fee_estimation.py index c4610f98bd..ac00db8ff0 100755 --- a/test/functional/feature_fee_estimation.py +++ b/test/functional/feature_fee_estimation.py @@ -4,6 +4,7 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test fee estimation code.""" from decimal import Decimal +import os import random from test_framework.messages import ( @@ -132,9 +133,13 @@ def check_smart_estimates(node, fees_seen): delta = 1.0e-6 # account for rounding error last_feerate = float(max(fees_seen)) all_smart_estimates = [node.estimatesmartfee(i) for i in range(1, 26)] + mempoolMinFee = node.getmempoolinfo()['mempoolminfee'] + minRelaytxFee = node.getmempoolinfo()['minrelaytxfee'] for i, e in enumerate(all_smart_estimates): # estimate is for i+1 feerate = float(e["feerate"]) assert_greater_than(feerate, 0) + assert_greater_than_or_equal(feerate, float(mempoolMinFee)) + assert_greater_than_or_equal(feerate, float(minRelaytxFee)) if feerate + delta < min(fees_seen) or feerate - delta > max(fees_seen): raise AssertionError(f"Estimated fee ({feerate}) out of range ({min(fees_seen)},{max(fees_seen)})") @@ -151,6 +156,21 @@ def check_estimates(node, fees_seen): check_raw_estimates(node, fees_seen) check_smart_estimates(node, fees_seen) + +def send_tx(node, utxo, feerate): + """Broadcast a 1in-1out transaction with a specific input and feerate (sat/vb).""" + overhead, op, scriptsig, nseq, value, spk = 10, 36, 5, 4, 8, 24 + tx_size = overhead + op + scriptsig + nseq + value + spk + fee = tx_size * feerate + + tx = CTransaction() + tx.vin = [CTxIn(COutPoint(int(utxo["txid"], 16), utxo["vout"]), SCRIPT_SIG[utxo["vout"]])] + tx.vout = [CTxOut(int(utxo["amount"] * COIN) - fee, P2SH_1)] + txid = node.sendrawtransaction(tx.serialize().hex()) + + return txid + + class EstimateFeeTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 3 @@ -208,20 +228,16 @@ class EstimateFeeTest(BitcoinTestFramework): newmem.append(utx) self.memutxo = newmem - def run_test(self): - self.log.info("This test is time consuming, please be patient") - self.log.info("Splitting inputs so we can generate tx's") - - # Start node0 - self.start_node(0) + def initial_split(self, node): + """Split two coinbase UTxOs into many small coins""" self.txouts = [] self.txouts2 = [] # Split a coinbase into two transaction puzzle outputs - split_inputs(self.nodes[0], self.nodes[0].listunspent(0), self.txouts, True) + split_inputs(node, node.listunspent(0), self.txouts, True) # Mine - while len(self.nodes[0].getrawmempool()) > 0: - self.generate(self.nodes[0], 1) + while len(node.getrawmempool()) > 0: + self.generate(node, 1) # Repeatedly split those 2 outputs, doubling twice for each rep # Use txouts to monitor the available utxo, since these won't be tracked in wallet @@ -229,27 +245,19 @@ class EstimateFeeTest(BitcoinTestFramework): while reps < 5: # Double txouts to txouts2 while len(self.txouts) > 0: - split_inputs(self.nodes[0], self.txouts, self.txouts2) - while len(self.nodes[0].getrawmempool()) > 0: - self.generate(self.nodes[0], 1) + split_inputs(node, self.txouts, self.txouts2) + while len(node.getrawmempool()) > 0: + self.generate(node, 1) # Double txouts2 to txouts while len(self.txouts2) > 0: - split_inputs(self.nodes[0], self.txouts2, self.txouts) - while len(self.nodes[0].getrawmempool()) > 0: - self.generate(self.nodes[0], 1) + split_inputs(node, self.txouts2, self.txouts) + while len(node.getrawmempool()) > 0: + self.generate(node, 1) reps += 1 - self.log.info("Finished splitting") - - # Now we can connect the other nodes, didn't want to connect them earlier - # so the estimates would not be affected by the splitting transactions - self.start_node(1) - self.start_node(2) - self.connect_nodes(1, 0) - self.connect_nodes(0, 2) - self.connect_nodes(2, 1) - - self.sync_all() + def sanity_check_estimates_range(self): + """Populate estimation buckets, assert estimates are in a sane range and + are strictly increasing as the target decreases.""" self.fees_per_kb = [] self.memutxo = [] self.confutxo = self.txouts # Start with the set of confirmed txouts after splitting @@ -275,6 +283,101 @@ class EstimateFeeTest(BitcoinTestFramework): self.log.info("Final estimates after emptying mempools") check_estimates(self.nodes[1], self.fees_per_kb) + def test_feerate_mempoolminfee(self): + high_val = 3*self.nodes[1].estimatesmartfee(1)['feerate'] + self.restart_node(1, extra_args=[f'-minrelaytxfee={high_val}']) + check_estimates(self.nodes[1], self.fees_per_kb) + self.restart_node(1) + + def sanity_check_rbf_estimates(self, utxos): + """During 5 blocks, broadcast low fee transactions. Only 10% of them get + confirmed and the remaining ones get RBF'd with a high fee transaction at + the next block. + The block policy estimator should return the high feerate. + """ + # The broadcaster and block producer + node = self.nodes[0] + miner = self.nodes[1] + # In sat/vb + low_feerate = 1 + high_feerate = 10 + # Cache the utxos of which to replace the spender after it failed to get + # confirmed + utxos_to_respend = [] + txids_to_replace = [] + + assert len(utxos) >= 250 + for _ in range(5): + # Broadcast 45 low fee transactions that will need to be RBF'd + for _ in range(45): + u = utxos.pop(0) + txid = send_tx(node, u, low_feerate) + utxos_to_respend.append(u) + txids_to_replace.append(txid) + # Broadcast 5 low fee transaction which don't need to + for _ in range(5): + send_tx(node, utxos.pop(0), low_feerate) + # Mine the transactions on another node + self.sync_mempools(wait=.1, nodes=[node, miner]) + for txid in txids_to_replace: + miner.prioritisetransaction(txid=txid, fee_delta=-COIN) + self.generate(miner, 1) + self.sync_blocks(wait=.1, nodes=[node, miner]) + # RBF the low-fee transactions + while True: + try: + u = utxos_to_respend.pop(0) + send_tx(node, u, high_feerate) + except IndexError: + break + + # Mine the last replacement txs + self.sync_mempools(wait=.1, nodes=[node, miner]) + self.generate(miner, 1) + self.sync_blocks(wait=.1, nodes=[node, miner]) + + # Only 10% of the transactions were really confirmed with a low feerate, + # the rest needed to be RBF'd. We must return the 90% conf rate feerate. + high_feerate_kvb = Decimal(high_feerate) / COIN * 10**3 + est_feerate = node.estimatesmartfee(2)["feerate"] + assert est_feerate == high_feerate_kvb + + def run_test(self): + self.log.info("This test is time consuming, please be patient") + self.log.info("Splitting inputs so we can generate tx's") + + # Split two coinbases into many small utxos + self.start_node(0) + self.initial_split(self.nodes[0]) + self.log.info("Finished splitting") + + # Now we can connect the other nodes, didn't want to connect them earlier + # so the estimates would not be affected by the splitting transactions + self.start_node(1) + self.start_node(2) + self.connect_nodes(1, 0) + self.connect_nodes(0, 2) + self.connect_nodes(2, 1) + self.sync_all() + + self.log.info("Testing estimates with single transactions.") + self.sanity_check_estimates_range() + + # check that the effective feerate is greater than or equal to the mempoolminfee even for high mempoolminfee + self.log.info("Test fee rate estimation after restarting node with high MempoolMinFee") + self.test_feerate_mempoolminfee() + + self.log.info("Restarting node with fresh estimation") + self.stop_node(0) + fee_dat = os.path.join(self.nodes[0].datadir, self.chain, "fee_estimates.dat") + os.remove(fee_dat) + self.start_node(0) + self.connect_nodes(0, 1) + self.connect_nodes(0, 2) + + self.log.info("Testing estimates with RBF.") + self.sanity_check_rbf_estimates(self.confutxo + self.memutxo) + self.log.info("Testing that fee estimation is disabled in blocksonly.") self.restart_node(0, ["-blocksonly"]) assert_raises_rpc_error(-32603, "Fee estimation disabled", diff --git a/test/functional/feature_filelock.py b/test/functional/feature_filelock.py index e09107802b..0fc654e10a 100755 --- a/test/functional/feature_filelock.py +++ b/test/functional/feature_filelock.py @@ -35,7 +35,7 @@ class FilelockTest(BitcoinTestFramework): wallet_dir = os.path.join(datadir, 'wallets') self.log.info("Check that we can't start a second bitcoind instance using the same wallet") if descriptors: - expected_msg = "Error: SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another bitcoind?" + expected_msg = f"Error: SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another instance of {self.config['environment']['PACKAGE_NAME']}?" else: expected_msg = "Error: Error initializing wallet database environment" self.nodes[1].assert_start_raises_init_error(extra_args=[f'-walletdir={wallet_dir}', f'-wallet={wallet_name}', '-noserver'], expected_msg=expected_msg, match=ErrorMatch.PARTIAL_REGEX) diff --git a/test/functional/feature_notifications.py b/test/functional/feature_notifications.py index 5ef3860867..2a507c75c4 100755 --- a/test/functional/feature_notifications.py +++ b/test/functional/feature_notifications.py @@ -27,6 +27,9 @@ class NotificationsTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True + # The experimental syscall sandbox feature (-sandbox) is not compatible with -alertnotify, + # -blocknotify or -walletnotify (which all invoke execve). + self.disable_syscall_sandbox = True def setup_network(self): self.wallet = ''.join(chr(i) for i in range(FILE_CHAR_START, FILE_CHAR_END) if chr(i) not in FILE_CHARS_DISALLOWED) @@ -42,7 +45,6 @@ class NotificationsTest(BitcoinTestFramework): f"-alertnotify=echo > {os.path.join(self.alertnotify_dir, '%s')}", f"-blocknotify=echo > {os.path.join(self.blocknotify_dir, '%s')}", ], [ - "-rescan", f"-walletnotify=echo %h_%b > {os.path.join(self.walletnotify_dir, notify_outputname('%w', '%s'))}", ]] self.wallet_names = [self.default_wallet_name, self.wallet] @@ -91,16 +93,15 @@ class NotificationsTest(BitcoinTestFramework): # directory content should equal the generated transaction hashes tx_details = list(map(lambda t: (t['txid'], t['blockheight'], t['blockhash']), self.nodes[1].listtransactions("*", block_count))) - self.stop_node(1) self.expect_wallet_notify(tx_details) self.log.info("test -walletnotify after rescan") - # restart node to rescan to force wallet notifications - self.start_node(1) - self.connect_nodes(0, 1) - + # rescan to force wallet notifications + self.nodes[1].rescanblockchain() self.wait_until(lambda: len(os.listdir(self.walletnotify_dir)) == block_count, timeout=10) + self.connect_nodes(0, 1) + # directory content should equal the generated transaction hashes tx_details = list(map(lambda t: (t['txid'], t['blockheight'], t['blockhash']), self.nodes[1].listtransactions("*", block_count))) self.expect_wallet_notify(tx_details) diff --git a/test/functional/feature_nulldummy.py b/test/functional/feature_nulldummy.py index 96984e1e64..217a38050d 100755 --- a/test/functional/feature_nulldummy.py +++ b/test/functional/feature_nulldummy.py @@ -52,7 +52,7 @@ class NULLDUMMYTest(BitcoinTestFramework): # This script tests NULLDUMMY activation, which is part of the 'segwit' deployment, so we go through # normal segwit activation here (and don't use the default always-on behaviour). self.extra_args = [[ - f'-segwitheight={COINBASE_MATURITY + 5}', + f'-testactivationheight=segwit@{COINBASE_MATURITY + 5}', '-addresstype=legacy', '-par=1', # Use only one script thread to get the exact reject reason for testing ]] diff --git a/test/functional/feature_presegwit_node_upgrade.py b/test/functional/feature_presegwit_node_upgrade.py index fd6b8620d4..aac42d4dbf 100755 --- a/test/functional/feature_presegwit_node_upgrade.py +++ b/test/functional/feature_presegwit_node_upgrade.py @@ -16,7 +16,7 @@ class SegwitUpgradeTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 - self.extra_args = [["-segwitheight=10"]] + self.extra_args = [["-testactivationheight=segwit@10"]] def run_test(self): """A pre-segwit node with insufficiently validated blocks needs to redownload blocks""" @@ -37,14 +37,14 @@ class SegwitUpgradeTest(BitcoinTestFramework): # Restarting the node (with segwit activation height set to 5) should result in a shutdown # because the blockchain consists of 3 insufficiently validated blocks per segwit consensus rules. node.assert_start_raises_init_error( - extra_args=["-segwitheight=5"], + extra_args=["-testactivationheight=segwit@5"], expected_msg=": Witness data for blocks after height 5 requires " f"validation. Please restart with -reindex..{os.linesep}" "Please restart with -reindex or -reindex-chainstate to recover.", ) # As directed, the user restarts the node with -reindex - self.start_node(0, extra_args=["-reindex", "-segwitheight=5"]) + self.start_node(0, extra_args=["-reindex", "-testactivationheight=segwit@5"]) # With the segwit consensus rules, the node is able to validate only up to block 4 assert_equal(node.getblockcount(), 4) diff --git a/test/functional/feature_rbf.py b/test/functional/feature_rbf.py index cb7556feb4..420147542e 100755 --- a/test/functional/feature_rbf.py +++ b/test/functional/feature_rbf.py @@ -7,7 +7,6 @@ from copy import deepcopy from decimal import Decimal -from test_framework.blocktools import COINBASE_MATURITY from test_framework.messages import ( BIP125_SEQUENCE_NUMBER, COIN, @@ -18,9 +17,16 @@ from test_framework.messages import ( ) from test_framework.script import CScript, OP_DROP from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal, assert_raises_rpc_error, satoshi_round -from test_framework.script_util import DUMMY_P2WPKH_SCRIPT, DUMMY_2_P2WPKH_SCRIPT +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) +from test_framework.script_util import ( + DUMMY_P2WPKH_SCRIPT, + DUMMY_2_P2WPKH_SCRIPT, +) from test_framework.wallet import MiniWallet +from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE MAX_REPLACEMENT_LIMIT = 100 class ReplaceByFeeTest(BitcoinTestFramework): @@ -38,15 +44,12 @@ class ReplaceByFeeTest(BitcoinTestFramework): ] self.supports_cli = False - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - def run_test(self): self.wallet = MiniWallet(self.nodes[0]) # the pre-mined test framework chain contains coinbase outputs to the # MiniWallet's default address ADDRESS_BCRT1_P2WSH_OP_TRUE in blocks # 76-100 (see method BitcoinTestFramework._initialize_chain()) - self.wallet.scan_blocks(start=76, num=2) + self.wallet.rescan_utxos() self.log.info("Running test simple doublespend...") self.test_simple_doublespend() @@ -89,29 +92,10 @@ class ReplaceByFeeTest(BitcoinTestFramework): def make_utxo(self, node, amount, confirmed=True, scriptPubKey=DUMMY_P2WPKH_SCRIPT): """Create a txout with a given amount and scriptPubKey - Mines coins as needed. - confirmed - txouts created will be confirmed in the blockchain; unconfirmed otherwise. """ - fee = 1 * COIN - while node.getbalance() < satoshi_round((amount + fee) / COIN): - self.generate(node, COINBASE_MATURITY) - - new_addr = node.getnewaddress() - txid = node.sendtoaddress(new_addr, satoshi_round((amount + fee) / COIN)) - tx1 = node.getrawtransaction(txid, 1) - txid = int(txid, 16) - i, _ = next(filter(lambda vout: new_addr == vout[1]['scriptPubKey']['address'], enumerate(tx1['vout']))) - - tx2 = CTransaction() - tx2.vin = [CTxIn(COutPoint(txid, i))] - tx2.vout = [CTxOut(amount, scriptPubKey)] - tx2.rehash() - - signed_tx = node.signrawtransactionwithwallet(tx2.serialize().hex()) - - txid = node.sendrawtransaction(signed_tx['hex'], 0) + txid, n = self.wallet.send_to(from_node=node, scriptPubKey=scriptPubKey, amount=amount) # If requested, ensure txouts are confirmed. if confirmed: @@ -124,7 +108,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): assert new_size < mempool_size mempool_size = new_size - return COutPoint(int(txid, 16), 0) + return COutPoint(int(txid, 16), n) def test_simple_doublespend(self): """Simple doublespend""" @@ -161,14 +145,14 @@ class ReplaceByFeeTest(BitcoinTestFramework): def test_doublespend_chain(self): """Doublespend of a long chain""" - initial_nValue = 50 * COIN + initial_nValue = 5 * COIN tx0_outpoint = self.make_utxo(self.nodes[0], initial_nValue) prevout = tx0_outpoint remaining_value = initial_nValue chain_txids = [] - while remaining_value > 10 * COIN: - remaining_value -= 1 * COIN + while remaining_value > 1 * COIN: + remaining_value -= int(0.1 * COIN) tx = CTransaction() tx.vin = [CTxIn(prevout, nSequence=0)] tx.vout = [CTxOut(remaining_value, CScript([1, OP_DROP] * 15 + [1]))] @@ -178,10 +162,10 @@ class ReplaceByFeeTest(BitcoinTestFramework): prevout = COutPoint(int(txid, 16), 0) # Whether the double-spend is allowed is evaluated by including all - # child fees - 40 BTC - so this attempt is rejected. + # child fees - 4 BTC - so this attempt is rejected. dbl_tx = CTransaction() dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)] - dbl_tx.vout = [CTxOut(initial_nValue - 30 * COIN, DUMMY_P2WPKH_SCRIPT)] + dbl_tx.vout = [CTxOut(initial_nValue - 3 * COIN, DUMMY_P2WPKH_SCRIPT)] dbl_tx_hex = dbl_tx.serialize().hex() # This will raise an exception due to insufficient fee @@ -190,7 +174,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): # Accepted with sufficient fee dbl_tx = CTransaction() dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)] - dbl_tx.vout = [CTxOut(1 * COIN, DUMMY_P2WPKH_SCRIPT)] + dbl_tx.vout = [CTxOut(int(0.1 * COIN), DUMMY_P2WPKH_SCRIPT)] dbl_tx_hex = dbl_tx.serialize().hex() self.nodes[0].sendrawtransaction(dbl_tx_hex, 0) @@ -201,10 +185,10 @@ class ReplaceByFeeTest(BitcoinTestFramework): def test_doublespend_tree(self): """Doublespend of a big tree of transactions""" - initial_nValue = 50 * COIN + initial_nValue = 5 * COIN tx0_outpoint = self.make_utxo(self.nodes[0], initial_nValue) - def branch(prevout, initial_value, max_txs, tree_width=5, fee=0.0001 * COIN, _total_txs=None): + def branch(prevout, initial_value, max_txs, tree_width=5, fee=0.00001 * COIN, _total_txs=None): if _total_txs is None: _total_txs = [0] if _total_txs[0] >= max_txs: @@ -235,7 +219,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): _total_txs=_total_txs): yield x - fee = int(0.0001 * COIN) + fee = int(0.00001 * COIN) n = MAX_REPLACEMENT_LIMIT tree_txs = list(branch(tx0_outpoint, initial_nValue, n, fee=fee)) assert_equal(len(tree_txs), n) @@ -248,10 +232,10 @@ class ReplaceByFeeTest(BitcoinTestFramework): # This will raise an exception due to insufficient fee assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, dbl_tx_hex, 0) - # 1 BTC fee is enough + # 0.1 BTC fee is enough dbl_tx = CTransaction() dbl_tx.vin = [CTxIn(tx0_outpoint, nSequence=0)] - dbl_tx.vout = [CTxOut(initial_nValue - fee * n - 1 * COIN, DUMMY_P2WPKH_SCRIPT)] + dbl_tx.vout = [CTxOut(initial_nValue - fee * n - int(0.1 * COIN), DUMMY_P2WPKH_SCRIPT)] dbl_tx_hex = dbl_tx.serialize().hex() self.nodes[0].sendrawtransaction(dbl_tx_hex, 0) @@ -264,7 +248,7 @@ class ReplaceByFeeTest(BitcoinTestFramework): # Try again, but with more total transactions than the "max txs # double-spent at once" anti-DoS limit. for n in (MAX_REPLACEMENT_LIMIT + 1, MAX_REPLACEMENT_LIMIT * 2): - fee = int(0.0001 * COIN) + fee = int(0.00001 * COIN) tx0_outpoint = self.make_utxo(self.nodes[0], initial_nValue) tree_txs = list(branch(tx0_outpoint, initial_nValue, n, fee=fee)) assert_equal(len(tree_txs), n) @@ -544,9 +528,9 @@ class ReplaceByFeeTest(BitcoinTestFramework): assert tx2b_txid in self.nodes[0].getrawmempool() def test_rpc(self): - us0 = self.nodes[0].listunspent()[0] + us0 = self.wallet.get_utxo() ins = [us0] - outs = {self.nodes[0].getnewaddress(): Decimal(1.0000000)} + outs = {ADDRESS_BCRT1_UNSPENDABLE: Decimal(1.0000000)} rawtx0 = self.nodes[0].createrawtransaction(ins, outs, 0, True) rawtx1 = self.nodes[0].createrawtransaction(ins, outs, 0, False) json0 = self.nodes[0].decoderawtransaction(rawtx0) @@ -554,14 +538,16 @@ class ReplaceByFeeTest(BitcoinTestFramework): assert_equal(json0["vin"][0]["sequence"], 4294967293) assert_equal(json1["vin"][0]["sequence"], 4294967295) - rawtx2 = self.nodes[0].createrawtransaction([], outs) - frawtx2a = self.nodes[0].fundrawtransaction(rawtx2, {"replaceable": True}) - frawtx2b = self.nodes[0].fundrawtransaction(rawtx2, {"replaceable": False}) + if self.is_wallet_compiled(): + self.init_wallet(node=0) + rawtx2 = self.nodes[0].createrawtransaction([], outs) + frawtx2a = self.nodes[0].fundrawtransaction(rawtx2, {"replaceable": True}) + frawtx2b = self.nodes[0].fundrawtransaction(rawtx2, {"replaceable": False}) - json0 = self.nodes[0].decoderawtransaction(frawtx2a['hex']) - json1 = self.nodes[0].decoderawtransaction(frawtx2b['hex']) - assert_equal(json0["vin"][0]["sequence"], 4294967293) - assert_equal(json1["vin"][0]["sequence"], 4294967294) + json0 = self.nodes[0].decoderawtransaction(frawtx2a['hex']) + json1 = self.nodes[0].decoderawtransaction(frawtx2b['hex']) + assert_equal(json0["vin"][0]["sequence"], 4294967293) + assert_equal(json1["vin"][0]["sequence"], 4294967294) def test_no_inherited_signaling(self): confirmed_utxo = self.wallet.get_utxo() diff --git a/test/functional/feature_segwit.py b/test/functional/feature_segwit.py index 2b79b3284c..5abe989e55 100755 --- a/test/functional/feature_segwit.py +++ b/test/functional/feature_segwit.py @@ -17,6 +17,7 @@ from test_framework.blocktools import ( send_to_witness, witness_script, ) +from test_framework.descriptors import descsum_create from test_framework.messages import ( COIN, COutPoint, @@ -31,11 +32,11 @@ from test_framework.script import ( OP_1, OP_2, OP_CHECKMULTISIG, - OP_CHECKSIG, OP_DROP, OP_TRUE, ) from test_framework.script_util import ( + key_to_p2pk_script, key_to_p2pkh_script, key_to_p2wpkh_script, script_to_p2sh_script, @@ -44,22 +45,28 @@ from test_framework.script_util import ( from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + assert_greater_than_or_equal, assert_is_hex_string, assert_raises_rpc_error, try_rpc, ) +from test_framework.wallet_util import ( + get_generate_key, +) NODE_0 = 0 NODE_2 = 2 P2WPKH = 0 P2WSH = 1 + def getutxo(txid): utxo = {} utxo["vout"] = 0 utxo["txid"] = txid return utxo + def find_spendable_utxo(node, min_value): for utxo in node.listunspent(query_options={'minimumAmount': min_value}): if utxo['spendable']: @@ -67,7 +74,9 @@ def find_spendable_utxo(node, min_value): raise AssertionError(f"Unspent output equal or higher than {min_value} not found") -txs_mined = {} # txindex from txid to blockhash + +txs_mined = {} # txindex from txid to blockhash + class SegWitTest(BitcoinTestFramework): def set_test_params(self): @@ -78,18 +87,18 @@ class SegWitTest(BitcoinTestFramework): [ "-acceptnonstdtxn=1", "-rpcserialversion=0", - "-segwitheight=432", + "-testactivationheight=segwit@432", "-addresstype=legacy", ], [ "-acceptnonstdtxn=1", "-rpcserialversion=1", - "-segwitheight=432", + "-testactivationheight=segwit@432", "-addresstype=legacy", ], [ "-acceptnonstdtxn=1", - "-segwitheight=432", + "-testactivationheight=segwit@432", "-addresstype=legacy", ], ] @@ -124,26 +133,52 @@ class SegWitTest(BitcoinTestFramework): self.log.info("Verify sigops are counted in GBT with pre-BIP141 rules before the fork") txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) tmpl = self.nodes[0].getblocktemplate({'rules': ['segwit']}) - assert tmpl['sizelimit'] == 1000000 + assert_equal(tmpl['sizelimit'], 1000000) assert 'weightlimit' not in tmpl - assert tmpl['sigoplimit'] == 20000 - assert tmpl['transactions'][0]['hash'] == txid - assert tmpl['transactions'][0]['sigops'] == 2 + assert_equal(tmpl['sigoplimit'], 20000) + assert_equal(tmpl['transactions'][0]['hash'], txid) + assert_equal(tmpl['transactions'][0]['sigops'], 2) assert '!segwit' not in tmpl['rules'] self.generate(self.nodes[0], 1) # block 162 balance_presetup = self.nodes[0].getbalance() self.pubkey = [] - p2sh_ids = [] # p2sh_ids[NODE][TYPE] is an array of txids that spend to P2WPKH (TYPE=0) or P2WSH (TYPE=1) scripts to an address for NODE embedded in p2sh - wit_ids = [] # wit_ids[NODE][TYPE] is an array of txids that spend to P2WPKH (TYPE=0) or P2WSH (TYPE=1) scripts to an address for NODE via bare witness + p2sh_ids = [] # p2sh_ids[NODE][TYPE] is an array of txids that spend to P2WPKH (TYPE=0) or P2WSH (TYPE=1) scripts to an address for NODE embedded in p2sh + wit_ids = [] # wit_ids[NODE][TYPE] is an array of txids that spend to P2WPKH (TYPE=0) or P2WSH (TYPE=1) scripts to an address for NODE via bare witness for i in range(3): - newaddress = self.nodes[i].getnewaddress() - self.pubkey.append(self.nodes[i].getaddressinfo(newaddress)["pubkey"]) + key = get_generate_key() + self.pubkey.append(key.pubkey) + multiscript = CScript([OP_1, bytes.fromhex(self.pubkey[-1]), OP_1, OP_CHECKMULTISIG]) - p2sh_ms_addr = self.nodes[i].addmultisigaddress(1, [self.pubkey[-1]], '', 'p2sh-segwit')['address'] - bip173_ms_addr = self.nodes[i].addmultisigaddress(1, [self.pubkey[-1]], '', 'bech32')['address'] + p2sh_ms_addr = self.nodes[i].createmultisig(1, [self.pubkey[-1]], 'p2sh-segwit')['address'] + bip173_ms_addr = self.nodes[i].createmultisig(1, [self.pubkey[-1]], 'bech32')['address'] assert_equal(p2sh_ms_addr, script_to_p2sh_p2wsh(multiscript)) assert_equal(bip173_ms_addr, script_to_p2wsh(multiscript)) + + p2sh_ms_desc = descsum_create(f"sh(wsh(multi(1,{key.privkey})))") + bip173_ms_desc = descsum_create(f"wsh(multi(1,{key.privkey}))") + assert_equal(self.nodes[i].deriveaddresses(p2sh_ms_desc)[0], p2sh_ms_addr) + assert_equal(self.nodes[i].deriveaddresses(bip173_ms_desc)[0], bip173_ms_addr) + + sh_wpkh_desc = descsum_create(f"sh(wpkh({key.privkey}))") + wpkh_desc = descsum_create(f"wpkh({key.privkey})") + assert_equal(self.nodes[i].deriveaddresses(sh_wpkh_desc)[0], key.p2sh_p2wpkh_addr) + assert_equal(self.nodes[i].deriveaddresses(wpkh_desc)[0], key.p2wpkh_addr) + + if self.options.descriptors: + res = self.nodes[i].importdescriptors([ + {"desc": p2sh_ms_desc, "timestamp": "now"}, + {"desc": bip173_ms_desc, "timestamp": "now"}, + {"desc": sh_wpkh_desc, "timestamp": "now"}, + {"desc": wpkh_desc, "timestamp": "now"}, + ]) + else: + # The nature of the legacy wallet is that this import results in also adding all of the necessary scripts + res = self.nodes[i].importmulti([ + {"desc": p2sh_ms_desc, "timestamp": "now"}, + ]) + assert all([r["success"] for r in res]) + p2sh_ids.append([]) wit_ids.append([]) for _ in range(2): @@ -215,7 +250,7 @@ class SegWitTest(BitcoinTestFramework): witnesses = coinbase_tx["decoded"]["vin"][0]["txinwitness"] assert_equal(len(witnesses), 1) assert_is_hex_string(witnesses[0]) - assert_equal(witnesses[0], '00'*32) + assert_equal(witnesses[0], '00' * 32) self.log.info("Verify witness txs without witness data are invalid after the fork") self.fail_accept(self.nodes[2], 'non-mandatory-script-verify-flag (Witness program hash mismatch)', wit_ids[NODE_2][P2WPKH][2], sign=False) @@ -231,12 +266,14 @@ class SegWitTest(BitcoinTestFramework): self.log.info("Verify sigops are counted in GBT with BIP141 rules after the fork") txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) + raw_tx = self.nodes[0].getrawtransaction(txid, True) tmpl = self.nodes[0].getblocktemplate({'rules': ['segwit']}) - assert tmpl['sizelimit'] >= 3999577 # actual maximum size is lower due to minimum mandatory non-witness data - assert tmpl['weightlimit'] == 4000000 - assert tmpl['sigoplimit'] == 80000 - assert tmpl['transactions'][0]['txid'] == txid - assert tmpl['transactions'][0]['sigops'] == 8 + assert_greater_than_or_equal(tmpl['sizelimit'], 3999577) # actual maximum size is lower due to minimum mandatory non-witness data + assert_equal(tmpl['weightlimit'], 4000000) + assert_equal(tmpl['sigoplimit'], 80000) + assert_equal(tmpl['transactions'][0]['txid'], txid) + expected_sigops = 9 if 'txinwitness' in raw_tx["vin"][0] else 8 + assert_equal(tmpl['transactions'][0]['sigops'], expected_sigops) assert '!segwit' in tmpl['rules'] self.generate(self.nodes[0], 1) # Mine a block to clear the gbt cache @@ -304,284 +341,285 @@ class SegWitTest(BitcoinTestFramework): # Mine a block to clear the gbt cache again. self.generate(self.nodes[0], 1) - self.log.info("Verify behaviour of importaddress and listunspent") - - # Some public keys to be used later - pubkeys = [ - "0363D44AABD0F1699138239DF2F042C3282C0671CC7A76826A55C8203D90E39242", # cPiM8Ub4heR9NBYmgVzJQiUH1if44GSBGiqaeJySuL2BKxubvgwb - "02D3E626B3E616FC8662B489C123349FECBFC611E778E5BE739B257EAE4721E5BF", # cPpAdHaD6VoYbW78kveN2bsvb45Q7G5PhaPApVUGwvF8VQ9brD97 - "04A47F2CBCEFFA7B9BCDA184E7D5668D3DA6F9079AD41E422FA5FD7B2D458F2538A62F5BD8EC85C2477F39650BD391EA6250207065B2A81DA8B009FC891E898F0E", # 91zqCU5B9sdWxzMt1ca3VzbtVm2YM6Hi5Rxn4UDtxEaN9C9nzXV - "02A47F2CBCEFFA7B9BCDA184E7D5668D3DA6F9079AD41E422FA5FD7B2D458F2538", # cPQFjcVRpAUBG8BA9hzr2yEzHwKoMgLkJZBBtK9vJnvGJgMjzTbd - "036722F784214129FEB9E8129D626324F3F6716555B603FFE8300BBCB882151228", # cQGtcm34xiLjB1v7bkRa4V3aAc9tS2UTuBZ1UnZGeSeNy627fN66 - "0266A8396EE936BF6D99D17920DB21C6C7B1AB14C639D5CD72B300297E416FD2EC", # cTW5mR5M45vHxXkeChZdtSPozrFwFgmEvTNnanCW6wrqwaCZ1X7K - "0450A38BD7F0AC212FEBA77354A9B036A32E0F7C81FC4E0C5ADCA7C549C4505D2522458C2D9AE3CEFD684E039194B72C8A10F9CB9D4764AB26FCC2718D421D3B84", # 92h2XPssjBpsJN5CqSP7v9a7cf2kgDunBC6PDFwJHMACM1rrVBJ - ] - - # Import a compressed key and an uncompressed key, generate some multisig addresses - self.nodes[0].importprivkey("92e6XLo5jVAVwrQKPNTs93oQco8f8sDNBcpv73Dsrs397fQtFQn") - uncompressed_spendable_address = ["mvozP4UwyGD2mGZU4D2eMvMLPB9WkMmMQu"] - self.nodes[0].importprivkey("cNC8eQ5dg3mFAVePDX4ddmPYpPbw41r9bm2jd1nLJT77e6RrzTRR") - compressed_spendable_address = ["mmWQubrDomqpgSYekvsU7HWEVjLFHAakLe"] - assert not self.nodes[0].getaddressinfo(uncompressed_spendable_address[0])['iscompressed'] - assert self.nodes[0].getaddressinfo(compressed_spendable_address[0])['iscompressed'] - - self.nodes[0].importpubkey(pubkeys[0]) - compressed_solvable_address = [key_to_p2pkh(pubkeys[0])] - self.nodes[0].importpubkey(pubkeys[1]) - compressed_solvable_address.append(key_to_p2pkh(pubkeys[1])) - self.nodes[0].importpubkey(pubkeys[2]) - uncompressed_solvable_address = [key_to_p2pkh(pubkeys[2])] - - spendable_anytime = [] # These outputs should be seen anytime after importprivkey and addmultisigaddress - spendable_after_importaddress = [] # These outputs should be seen after importaddress - solvable_after_importaddress = [] # These outputs should be seen after importaddress but not spendable - unsolvable_after_importaddress = [] # These outputs should be unsolvable after importaddress - solvable_anytime = [] # These outputs should be solvable after importpubkey - unseen_anytime = [] # These outputs should never be seen - - uncompressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [uncompressed_spendable_address[0], compressed_spendable_address[0]])['address']) - uncompressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [uncompressed_spendable_address[0], uncompressed_spendable_address[0]])['address']) - compressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], compressed_spendable_address[0]])['address']) - uncompressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], uncompressed_solvable_address[0]])['address']) - compressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], compressed_solvable_address[0]])['address']) - compressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_solvable_address[0], compressed_solvable_address[1]])['address']) - - # Test multisig_without_privkey - # We have 2 public keys without private keys, use addmultisigaddress to add to wallet. - # Money sent to P2SH of multisig of this should only be seen after importaddress with the BASE58 P2SH address. - - multisig_without_privkey_address = self.nodes[0].addmultisigaddress(2, [pubkeys[3], pubkeys[4]])['address'] - script = CScript([OP_2, bytes.fromhex(pubkeys[3]), bytes.fromhex(pubkeys[4]), OP_2, OP_CHECKMULTISIG]) - solvable_after_importaddress.append(script_to_p2sh_script(script)) - - for i in compressed_spendable_address: - v = self.nodes[0].getaddressinfo(i) - if (v['isscript']): - [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) - # p2sh multisig with compressed keys should always be spendable - spendable_anytime.extend([p2sh]) - # bare multisig can be watched and signed, but is not treated as ours - solvable_after_importaddress.extend([bare]) - # P2WSH and P2SH(P2WSH) multisig with compressed keys are spendable after direct importaddress - spendable_after_importaddress.extend([p2wsh, p2sh_p2wsh]) - else: - [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) - # normal P2PKH and P2PK with compressed keys should always be spendable - spendable_anytime.extend([p2pkh, p2pk]) - # P2SH_P2PK, P2SH_P2PKH with compressed keys are spendable after direct importaddress - spendable_after_importaddress.extend([p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh]) - # P2WPKH and P2SH_P2WPKH with compressed keys should always be spendable - spendable_anytime.extend([p2wpkh, p2sh_p2wpkh]) - - for i in uncompressed_spendable_address: - v = self.nodes[0].getaddressinfo(i) - if (v['isscript']): - [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) - # p2sh multisig with uncompressed keys should always be spendable - spendable_anytime.extend([p2sh]) - # bare multisig can be watched and signed, but is not treated as ours - solvable_after_importaddress.extend([bare]) - # P2WSH and P2SH(P2WSH) multisig with uncompressed keys are never seen - unseen_anytime.extend([p2wsh, p2sh_p2wsh]) - else: - [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) - # normal P2PKH and P2PK with uncompressed keys should always be spendable - spendable_anytime.extend([p2pkh, p2pk]) - # P2SH_P2PK and P2SH_P2PKH are spendable after direct importaddress - spendable_after_importaddress.extend([p2sh_p2pk, p2sh_p2pkh]) - # Witness output types with uncompressed keys are never seen - unseen_anytime.extend([p2wpkh, p2sh_p2wpkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh]) - - for i in compressed_solvable_address: - v = self.nodes[0].getaddressinfo(i) - if (v['isscript']): - # Multisig without private is not seen after addmultisigaddress, but seen after importaddress - [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) - solvable_after_importaddress.extend([bare, p2sh, p2wsh, p2sh_p2wsh]) - else: - [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) - # normal P2PKH, P2PK, P2WPKH and P2SH_P2WPKH with compressed keys should always be seen - solvable_anytime.extend([p2pkh, p2pk, p2wpkh, p2sh_p2wpkh]) - # P2SH_P2PK, P2SH_P2PKH with compressed keys are seen after direct importaddress - solvable_after_importaddress.extend([p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh]) - - for i in uncompressed_solvable_address: - v = self.nodes[0].getaddressinfo(i) - if (v['isscript']): - [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) - # Base uncompressed multisig without private is not seen after addmultisigaddress, but seen after importaddress - solvable_after_importaddress.extend([bare, p2sh]) - # P2WSH and P2SH(P2WSH) multisig with uncompressed keys are never seen - unseen_anytime.extend([p2wsh, p2sh_p2wsh]) - else: - [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) - # normal P2PKH and P2PK with uncompressed keys should always be seen - solvable_anytime.extend([p2pkh, p2pk]) - # P2SH_P2PK, P2SH_P2PKH with uncompressed keys are seen after direct importaddress - solvable_after_importaddress.extend([p2sh_p2pk, p2sh_p2pkh]) - # Witness output types with uncompressed keys are never seen - unseen_anytime.extend([p2wpkh, p2sh_p2wpkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh]) - - op1 = CScript([OP_1]) - op0 = CScript([OP_0]) - # 2N7MGY19ti4KDMSzRfPAssP6Pxyuxoi6jLe is the P2SH(P2PKH) version of mjoE3sSrb8ByYEvgnC3Aox86u1CHnfJA4V - unsolvable_address_key = bytes.fromhex("02341AEC7587A51CDE5279E0630A531AEA2615A9F80B17E8D9376327BAEAA59E3D") - unsolvablep2pkh = key_to_p2pkh_script(unsolvable_address_key) - unsolvablep2wshp2pkh = script_to_p2wsh_script(unsolvablep2pkh) - p2shop0 = script_to_p2sh_script(op0) - p2wshop1 = script_to_p2wsh_script(op1) - unsolvable_after_importaddress.append(unsolvablep2pkh) - unsolvable_after_importaddress.append(unsolvablep2wshp2pkh) - unsolvable_after_importaddress.append(op1) # OP_1 will be imported as script - unsolvable_after_importaddress.append(p2wshop1) - unseen_anytime.append(op0) # OP_0 will be imported as P2SH address with no script provided - unsolvable_after_importaddress.append(p2shop0) - - spendable_txid = [] - solvable_txid = [] - spendable_txid.append(self.mine_and_test_listunspent(spendable_anytime, 2)) - solvable_txid.append(self.mine_and_test_listunspent(solvable_anytime, 1)) - self.mine_and_test_listunspent(spendable_after_importaddress + solvable_after_importaddress + unseen_anytime + unsolvable_after_importaddress, 0) - - importlist = [] - for i in compressed_spendable_address + uncompressed_spendable_address + compressed_solvable_address + uncompressed_solvable_address: - v = self.nodes[0].getaddressinfo(i) - if (v['isscript']): - bare = bytes.fromhex(v['hex']) - importlist.append(bare.hex()) - importlist.append(script_to_p2wsh_script(bare).hex()) - else: - pubkey = bytes.fromhex(v['pubkey']) - p2pk = CScript([pubkey, OP_CHECKSIG]) - p2pkh = key_to_p2pkh_script(pubkey) - importlist.append(p2pk.hex()) - importlist.append(p2pkh.hex()) - importlist.append(key_to_p2wpkh_script(pubkey).hex()) - importlist.append(script_to_p2wsh_script(p2pk).hex()) - importlist.append(script_to_p2wsh_script(p2pkh).hex()) - - importlist.append(unsolvablep2pkh.hex()) - importlist.append(unsolvablep2wshp2pkh.hex()) - importlist.append(op1.hex()) - importlist.append(p2wshop1.hex()) - - for i in importlist: - # import all generated addresses. The wallet already has the private keys for some of these, so catch JSON RPC - # exceptions and continue. - try_rpc(-4, "The wallet already contains the private key for this address or script", self.nodes[0].importaddress, i, "", False, True) - - self.nodes[0].importaddress(script_to_p2sh(op0)) # import OP_0 as address only - self.nodes[0].importaddress(multisig_without_privkey_address) # Test multisig_without_privkey - - spendable_txid.append(self.mine_and_test_listunspent(spendable_anytime + spendable_after_importaddress, 2)) - solvable_txid.append(self.mine_and_test_listunspent(solvable_anytime + solvable_after_importaddress, 1)) - self.mine_and_test_listunspent(unsolvable_after_importaddress, 1) - self.mine_and_test_listunspent(unseen_anytime, 0) - - spendable_txid.append(self.mine_and_test_listunspent(spendable_anytime + spendable_after_importaddress, 2)) - solvable_txid.append(self.mine_and_test_listunspent(solvable_anytime + solvable_after_importaddress, 1)) - self.mine_and_test_listunspent(unsolvable_after_importaddress, 1) - self.mine_and_test_listunspent(unseen_anytime, 0) - - # Repeat some tests. This time we don't add witness scripts with importaddress - # Import a compressed key and an uncompressed key, generate some multisig addresses - self.nodes[0].importprivkey("927pw6RW8ZekycnXqBQ2JS5nPyo1yRfGNN8oq74HeddWSpafDJH") - uncompressed_spendable_address = ["mguN2vNSCEUh6rJaXoAVwY3YZwZvEmf5xi"] - self.nodes[0].importprivkey("cMcrXaaUC48ZKpcyydfFo8PxHAjpsYLhdsp6nmtB3E2ER9UUHWnw") - compressed_spendable_address = ["n1UNmpmbVUJ9ytXYXiurmGPQ3TRrXqPWKL"] - - self.nodes[0].importpubkey(pubkeys[5]) - compressed_solvable_address = [key_to_p2pkh(pubkeys[5])] - self.nodes[0].importpubkey(pubkeys[6]) - uncompressed_solvable_address = [key_to_p2pkh(pubkeys[6])] - - unseen_anytime = [] # These outputs should never be seen - solvable_anytime = [] # These outputs should be solvable after importpubkey - unseen_anytime = [] # These outputs should never be seen - - uncompressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [uncompressed_spendable_address[0], compressed_spendable_address[0]])['address']) - uncompressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [uncompressed_spendable_address[0], uncompressed_spendable_address[0]])['address']) - compressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], compressed_spendable_address[0]])['address']) - uncompressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_solvable_address[0], uncompressed_solvable_address[0]])['address']) - compressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], compressed_solvable_address[0]])['address']) - - premature_witaddress = [] - - for i in compressed_spendable_address: - v = self.nodes[0].getaddressinfo(i) - if (v['isscript']): - [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) - premature_witaddress.append(script_to_p2sh(p2wsh)) - else: - [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) - # P2WPKH, P2SH_P2WPKH are always spendable - spendable_anytime.extend([p2wpkh, p2sh_p2wpkh]) - - for i in uncompressed_spendable_address + uncompressed_solvable_address: - v = self.nodes[0].getaddressinfo(i) - if (v['isscript']): - [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) - # P2WSH and P2SH(P2WSH) multisig with uncompressed keys are never seen - unseen_anytime.extend([p2wsh, p2sh_p2wsh]) - else: - [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) - # P2WPKH, P2SH_P2WPKH with uncompressed keys are never seen - unseen_anytime.extend([p2wpkh, p2sh_p2wpkh]) - - for i in compressed_solvable_address: - v = self.nodes[0].getaddressinfo(i) - if (v['isscript']): - [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) - premature_witaddress.append(script_to_p2sh(p2wsh)) - else: - [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) - # P2SH_P2PK, P2SH_P2PKH with compressed keys are always solvable - solvable_anytime.extend([p2wpkh, p2sh_p2wpkh]) - - self.mine_and_test_listunspent(spendable_anytime, 2) - self.mine_and_test_listunspent(solvable_anytime, 1) - self.mine_and_test_listunspent(unseen_anytime, 0) - - # Check that createrawtransaction/decoderawtransaction with non-v0 Bech32 works - v1_addr = program_to_witness(1, [3, 5]) - v1_tx = self.nodes[0].createrawtransaction([getutxo(spendable_txid[0])], {v1_addr: 1}) - v1_decoded = self.nodes[1].decoderawtransaction(v1_tx) - assert_equal(v1_decoded['vout'][0]['scriptPubKey']['address'], v1_addr) - assert_equal(v1_decoded['vout'][0]['scriptPubKey']['hex'], "51020305") - - # Check that spendable outputs are really spendable - self.create_and_mine_tx_from_txids(spendable_txid) - - # import all the private keys so solvable addresses become spendable - self.nodes[0].importprivkey("cPiM8Ub4heR9NBYmgVzJQiUH1if44GSBGiqaeJySuL2BKxubvgwb") - self.nodes[0].importprivkey("cPpAdHaD6VoYbW78kveN2bsvb45Q7G5PhaPApVUGwvF8VQ9brD97") - self.nodes[0].importprivkey("91zqCU5B9sdWxzMt1ca3VzbtVm2YM6Hi5Rxn4UDtxEaN9C9nzXV") - self.nodes[0].importprivkey("cPQFjcVRpAUBG8BA9hzr2yEzHwKoMgLkJZBBtK9vJnvGJgMjzTbd") - self.nodes[0].importprivkey("cQGtcm34xiLjB1v7bkRa4V3aAc9tS2UTuBZ1UnZGeSeNy627fN66") - self.nodes[0].importprivkey("cTW5mR5M45vHxXkeChZdtSPozrFwFgmEvTNnanCW6wrqwaCZ1X7K") - self.create_and_mine_tx_from_txids(solvable_txid) - - # Test that importing native P2WPKH/P2WSH scripts works - for use_p2wsh in [False, True]: - if use_p2wsh: - scriptPubKey = "00203a59f3f56b713fdcf5d1a57357f02c44342cbf306ffe0c4741046837bf90561a" - transaction = "01000000000100e1f505000000002200203a59f3f56b713fdcf5d1a57357f02c44342cbf306ffe0c4741046837bf90561a00000000" - else: - scriptPubKey = "a9142f8c469c2f0084c48e11f998ffbe7efa7549f26d87" - transaction = "01000000000100e1f5050000000017a9142f8c469c2f0084c48e11f998ffbe7efa7549f26d8700000000" - - self.nodes[1].importaddress(scriptPubKey, "", False) - rawtxfund = self.nodes[1].fundrawtransaction(transaction)['hex'] - rawtxfund = self.nodes[1].signrawtransactionwithwallet(rawtxfund)["hex"] - txid = self.nodes[1].sendrawtransaction(rawtxfund) - - assert_equal(self.nodes[1].gettransaction(txid, True)["txid"], txid) - assert_equal(self.nodes[1].listtransactions("*", 1, 0, True)[0]["txid"], txid) - - # Assert it is properly saved - self.restart_node(1) - assert_equal(self.nodes[1].gettransaction(txid, True)["txid"], txid) - assert_equal(self.nodes[1].listtransactions("*", 1, 0, True)[0]["txid"], txid) + if not self.options.descriptors: + self.log.info("Verify behaviour of importaddress and listunspent") + + # Some public keys to be used later + pubkeys = [ + "0363D44AABD0F1699138239DF2F042C3282C0671CC7A76826A55C8203D90E39242", # cPiM8Ub4heR9NBYmgVzJQiUH1if44GSBGiqaeJySuL2BKxubvgwb + "02D3E626B3E616FC8662B489C123349FECBFC611E778E5BE739B257EAE4721E5BF", # cPpAdHaD6VoYbW78kveN2bsvb45Q7G5PhaPApVUGwvF8VQ9brD97 + "04A47F2CBCEFFA7B9BCDA184E7D5668D3DA6F9079AD41E422FA5FD7B2D458F2538A62F5BD8EC85C2477F39650BD391EA6250207065B2A81DA8B009FC891E898F0E", # 91zqCU5B9sdWxzMt1ca3VzbtVm2YM6Hi5Rxn4UDtxEaN9C9nzXV + "02A47F2CBCEFFA7B9BCDA184E7D5668D3DA6F9079AD41E422FA5FD7B2D458F2538", # cPQFjcVRpAUBG8BA9hzr2yEzHwKoMgLkJZBBtK9vJnvGJgMjzTbd + "036722F784214129FEB9E8129D626324F3F6716555B603FFE8300BBCB882151228", # cQGtcm34xiLjB1v7bkRa4V3aAc9tS2UTuBZ1UnZGeSeNy627fN66 + "0266A8396EE936BF6D99D17920DB21C6C7B1AB14C639D5CD72B300297E416FD2EC", # cTW5mR5M45vHxXkeChZdtSPozrFwFgmEvTNnanCW6wrqwaCZ1X7K + "0450A38BD7F0AC212FEBA77354A9B036A32E0F7C81FC4E0C5ADCA7C549C4505D2522458C2D9AE3CEFD684E039194B72C8A10F9CB9D4764AB26FCC2718D421D3B84", # 92h2XPssjBpsJN5CqSP7v9a7cf2kgDunBC6PDFwJHMACM1rrVBJ + ] + + # Import a compressed key and an uncompressed key, generate some multisig addresses + self.nodes[0].importprivkey("92e6XLo5jVAVwrQKPNTs93oQco8f8sDNBcpv73Dsrs397fQtFQn") + uncompressed_spendable_address = ["mvozP4UwyGD2mGZU4D2eMvMLPB9WkMmMQu"] + self.nodes[0].importprivkey("cNC8eQ5dg3mFAVePDX4ddmPYpPbw41r9bm2jd1nLJT77e6RrzTRR") + compressed_spendable_address = ["mmWQubrDomqpgSYekvsU7HWEVjLFHAakLe"] + assert not self.nodes[0].getaddressinfo(uncompressed_spendable_address[0])['iscompressed'] + assert self.nodes[0].getaddressinfo(compressed_spendable_address[0])['iscompressed'] + + self.nodes[0].importpubkey(pubkeys[0]) + compressed_solvable_address = [key_to_p2pkh(pubkeys[0])] + self.nodes[0].importpubkey(pubkeys[1]) + compressed_solvable_address.append(key_to_p2pkh(pubkeys[1])) + self.nodes[0].importpubkey(pubkeys[2]) + uncompressed_solvable_address = [key_to_p2pkh(pubkeys[2])] + + spendable_anytime = [] # These outputs should be seen anytime after importprivkey and addmultisigaddress + spendable_after_importaddress = [] # These outputs should be seen after importaddress + solvable_after_importaddress = [] # These outputs should be seen after importaddress but not spendable + unsolvable_after_importaddress = [] # These outputs should be unsolvable after importaddress + solvable_anytime = [] # These outputs should be solvable after importpubkey + unseen_anytime = [] # These outputs should never be seen + + uncompressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [uncompressed_spendable_address[0], compressed_spendable_address[0]])['address']) + uncompressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [uncompressed_spendable_address[0], uncompressed_spendable_address[0]])['address']) + compressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], compressed_spendable_address[0]])['address']) + uncompressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], uncompressed_solvable_address[0]])['address']) + compressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], compressed_solvable_address[0]])['address']) + compressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_solvable_address[0], compressed_solvable_address[1]])['address']) + + # Test multisig_without_privkey + # We have 2 public keys without private keys, use addmultisigaddress to add to wallet. + # Money sent to P2SH of multisig of this should only be seen after importaddress with the BASE58 P2SH address. + + multisig_without_privkey_address = self.nodes[0].addmultisigaddress(2, [pubkeys[3], pubkeys[4]])['address'] + script = CScript([OP_2, bytes.fromhex(pubkeys[3]), bytes.fromhex(pubkeys[4]), OP_2, OP_CHECKMULTISIG]) + solvable_after_importaddress.append(script_to_p2sh_script(script)) + + for i in compressed_spendable_address: + v = self.nodes[0].getaddressinfo(i) + if v['isscript']: + [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) + # p2sh multisig with compressed keys should always be spendable + spendable_anytime.extend([p2sh]) + # bare multisig can be watched and signed, but is not treated as ours + solvable_after_importaddress.extend([bare]) + # P2WSH and P2SH(P2WSH) multisig with compressed keys are spendable after direct importaddress + spendable_after_importaddress.extend([p2wsh, p2sh_p2wsh]) + else: + [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) + # normal P2PKH and P2PK with compressed keys should always be spendable + spendable_anytime.extend([p2pkh, p2pk]) + # P2SH_P2PK, P2SH_P2PKH with compressed keys are spendable after direct importaddress + spendable_after_importaddress.extend([p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh]) + # P2WPKH and P2SH_P2WPKH with compressed keys should always be spendable + spendable_anytime.extend([p2wpkh, p2sh_p2wpkh]) + + for i in uncompressed_spendable_address: + v = self.nodes[0].getaddressinfo(i) + if v['isscript']: + [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) + # p2sh multisig with uncompressed keys should always be spendable + spendable_anytime.extend([p2sh]) + # bare multisig can be watched and signed, but is not treated as ours + solvable_after_importaddress.extend([bare]) + # P2WSH and P2SH(P2WSH) multisig with uncompressed keys are never seen + unseen_anytime.extend([p2wsh, p2sh_p2wsh]) + else: + [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) + # normal P2PKH and P2PK with uncompressed keys should always be spendable + spendable_anytime.extend([p2pkh, p2pk]) + # P2SH_P2PK and P2SH_P2PKH are spendable after direct importaddress + spendable_after_importaddress.extend([p2sh_p2pk, p2sh_p2pkh]) + # Witness output types with uncompressed keys are never seen + unseen_anytime.extend([p2wpkh, p2sh_p2wpkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh]) + + for i in compressed_solvable_address: + v = self.nodes[0].getaddressinfo(i) + if v['isscript']: + # Multisig without private is not seen after addmultisigaddress, but seen after importaddress + [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) + solvable_after_importaddress.extend([bare, p2sh, p2wsh, p2sh_p2wsh]) + else: + [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) + # normal P2PKH, P2PK, P2WPKH and P2SH_P2WPKH with compressed keys should always be seen + solvable_anytime.extend([p2pkh, p2pk, p2wpkh, p2sh_p2wpkh]) + # P2SH_P2PK, P2SH_P2PKH with compressed keys are seen after direct importaddress + solvable_after_importaddress.extend([p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh]) + + for i in uncompressed_solvable_address: + v = self.nodes[0].getaddressinfo(i) + if v['isscript']: + [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) + # Base uncompressed multisig without private is not seen after addmultisigaddress, but seen after importaddress + solvable_after_importaddress.extend([bare, p2sh]) + # P2WSH and P2SH(P2WSH) multisig with uncompressed keys are never seen + unseen_anytime.extend([p2wsh, p2sh_p2wsh]) + else: + [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) + # normal P2PKH and P2PK with uncompressed keys should always be seen + solvable_anytime.extend([p2pkh, p2pk]) + # P2SH_P2PK, P2SH_P2PKH with uncompressed keys are seen after direct importaddress + solvable_after_importaddress.extend([p2sh_p2pk, p2sh_p2pkh]) + # Witness output types with uncompressed keys are never seen + unseen_anytime.extend([p2wpkh, p2sh_p2wpkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh]) + + op1 = CScript([OP_1]) + op0 = CScript([OP_0]) + # 2N7MGY19ti4KDMSzRfPAssP6Pxyuxoi6jLe is the P2SH(P2PKH) version of mjoE3sSrb8ByYEvgnC3Aox86u1CHnfJA4V + unsolvable_address_key = bytes.fromhex("02341AEC7587A51CDE5279E0630A531AEA2615A9F80B17E8D9376327BAEAA59E3D") + unsolvablep2pkh = key_to_p2pkh_script(unsolvable_address_key) + unsolvablep2wshp2pkh = script_to_p2wsh_script(unsolvablep2pkh) + p2shop0 = script_to_p2sh_script(op0) + p2wshop1 = script_to_p2wsh_script(op1) + unsolvable_after_importaddress.append(unsolvablep2pkh) + unsolvable_after_importaddress.append(unsolvablep2wshp2pkh) + unsolvable_after_importaddress.append(op1) # OP_1 will be imported as script + unsolvable_after_importaddress.append(p2wshop1) + unseen_anytime.append(op0) # OP_0 will be imported as P2SH address with no script provided + unsolvable_after_importaddress.append(p2shop0) + + spendable_txid = [] + solvable_txid = [] + spendable_txid.append(self.mine_and_test_listunspent(spendable_anytime, 2)) + solvable_txid.append(self.mine_and_test_listunspent(solvable_anytime, 1)) + self.mine_and_test_listunspent(spendable_after_importaddress + solvable_after_importaddress + unseen_anytime + unsolvable_after_importaddress, 0) + + importlist = [] + for i in compressed_spendable_address + uncompressed_spendable_address + compressed_solvable_address + uncompressed_solvable_address: + v = self.nodes[0].getaddressinfo(i) + if v['isscript']: + bare = bytes.fromhex(v['hex']) + importlist.append(bare.hex()) + importlist.append(script_to_p2wsh_script(bare).hex()) + else: + pubkey = bytes.fromhex(v['pubkey']) + p2pk = key_to_p2pk_script(pubkey) + p2pkh = key_to_p2pkh_script(pubkey) + importlist.append(p2pk.hex()) + importlist.append(p2pkh.hex()) + importlist.append(key_to_p2wpkh_script(pubkey).hex()) + importlist.append(script_to_p2wsh_script(p2pk).hex()) + importlist.append(script_to_p2wsh_script(p2pkh).hex()) + + importlist.append(unsolvablep2pkh.hex()) + importlist.append(unsolvablep2wshp2pkh.hex()) + importlist.append(op1.hex()) + importlist.append(p2wshop1.hex()) + + for i in importlist: + # import all generated addresses. The wallet already has the private keys for some of these, so catch JSON RPC + # exceptions and continue. + try_rpc(-4, "The wallet already contains the private key for this address or script", self.nodes[0].importaddress, i, "", False, True) + + self.nodes[0].importaddress(script_to_p2sh(op0)) # import OP_0 as address only + self.nodes[0].importaddress(multisig_without_privkey_address) # Test multisig_without_privkey + + spendable_txid.append(self.mine_and_test_listunspent(spendable_anytime + spendable_after_importaddress, 2)) + solvable_txid.append(self.mine_and_test_listunspent(solvable_anytime + solvable_after_importaddress, 1)) + self.mine_and_test_listunspent(unsolvable_after_importaddress, 1) + self.mine_and_test_listunspent(unseen_anytime, 0) + + spendable_txid.append(self.mine_and_test_listunspent(spendable_anytime + spendable_after_importaddress, 2)) + solvable_txid.append(self.mine_and_test_listunspent(solvable_anytime + solvable_after_importaddress, 1)) + self.mine_and_test_listunspent(unsolvable_after_importaddress, 1) + self.mine_and_test_listunspent(unseen_anytime, 0) + + # Repeat some tests. This time we don't add witness scripts with importaddress + # Import a compressed key and an uncompressed key, generate some multisig addresses + self.nodes[0].importprivkey("927pw6RW8ZekycnXqBQ2JS5nPyo1yRfGNN8oq74HeddWSpafDJH") + uncompressed_spendable_address = ["mguN2vNSCEUh6rJaXoAVwY3YZwZvEmf5xi"] + self.nodes[0].importprivkey("cMcrXaaUC48ZKpcyydfFo8PxHAjpsYLhdsp6nmtB3E2ER9UUHWnw") + compressed_spendable_address = ["n1UNmpmbVUJ9ytXYXiurmGPQ3TRrXqPWKL"] + + self.nodes[0].importpubkey(pubkeys[5]) + compressed_solvable_address = [key_to_p2pkh(pubkeys[5])] + self.nodes[0].importpubkey(pubkeys[6]) + uncompressed_solvable_address = [key_to_p2pkh(pubkeys[6])] + + unseen_anytime = [] # These outputs should never be seen + solvable_anytime = [] # These outputs should be solvable after importpubkey + unseen_anytime = [] # These outputs should never be seen + + uncompressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [uncompressed_spendable_address[0], compressed_spendable_address[0]])['address']) + uncompressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [uncompressed_spendable_address[0], uncompressed_spendable_address[0]])['address']) + compressed_spendable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], compressed_spendable_address[0]])['address']) + uncompressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_solvable_address[0], uncompressed_solvable_address[0]])['address']) + compressed_solvable_address.append(self.nodes[0].addmultisigaddress(2, [compressed_spendable_address[0], compressed_solvable_address[0]])['address']) + + premature_witaddress = [] + + for i in compressed_spendable_address: + v = self.nodes[0].getaddressinfo(i) + if v['isscript']: + [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) + premature_witaddress.append(script_to_p2sh(p2wsh)) + else: + [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) + # P2WPKH, P2SH_P2WPKH are always spendable + spendable_anytime.extend([p2wpkh, p2sh_p2wpkh]) + + for i in uncompressed_spendable_address + uncompressed_solvable_address: + v = self.nodes[0].getaddressinfo(i) + if v['isscript']: + [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) + # P2WSH and P2SH(P2WSH) multisig with uncompressed keys are never seen + unseen_anytime.extend([p2wsh, p2sh_p2wsh]) + else: + [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) + # P2WPKH, P2SH_P2WPKH with uncompressed keys are never seen + unseen_anytime.extend([p2wpkh, p2sh_p2wpkh]) + + for i in compressed_solvable_address: + v = self.nodes[0].getaddressinfo(i) + if v['isscript']: + [bare, p2sh, p2wsh, p2sh_p2wsh] = self.p2sh_address_to_script(v) + premature_witaddress.append(script_to_p2sh(p2wsh)) + else: + [p2wpkh, p2sh_p2wpkh, p2pk, p2pkh, p2sh_p2pk, p2sh_p2pkh, p2wsh_p2pk, p2wsh_p2pkh, p2sh_p2wsh_p2pk, p2sh_p2wsh_p2pkh] = self.p2pkh_address_to_script(v) + # P2SH_P2PK, P2SH_P2PKH with compressed keys are always solvable + solvable_anytime.extend([p2wpkh, p2sh_p2wpkh]) + + self.mine_and_test_listunspent(spendable_anytime, 2) + self.mine_and_test_listunspent(solvable_anytime, 1) + self.mine_and_test_listunspent(unseen_anytime, 0) + + # Check that createrawtransaction/decoderawtransaction with non-v0 Bech32 works + v1_addr = program_to_witness(1, [3, 5]) + v1_tx = self.nodes[0].createrawtransaction([getutxo(spendable_txid[0])], {v1_addr: 1}) + v1_decoded = self.nodes[1].decoderawtransaction(v1_tx) + assert_equal(v1_decoded['vout'][0]['scriptPubKey']['address'], v1_addr) + assert_equal(v1_decoded['vout'][0]['scriptPubKey']['hex'], "51020305") + + # Check that spendable outputs are really spendable + self.create_and_mine_tx_from_txids(spendable_txid) + + # import all the private keys so solvable addresses become spendable + self.nodes[0].importprivkey("cPiM8Ub4heR9NBYmgVzJQiUH1if44GSBGiqaeJySuL2BKxubvgwb") + self.nodes[0].importprivkey("cPpAdHaD6VoYbW78kveN2bsvb45Q7G5PhaPApVUGwvF8VQ9brD97") + self.nodes[0].importprivkey("91zqCU5B9sdWxzMt1ca3VzbtVm2YM6Hi5Rxn4UDtxEaN9C9nzXV") + self.nodes[0].importprivkey("cPQFjcVRpAUBG8BA9hzr2yEzHwKoMgLkJZBBtK9vJnvGJgMjzTbd") + self.nodes[0].importprivkey("cQGtcm34xiLjB1v7bkRa4V3aAc9tS2UTuBZ1UnZGeSeNy627fN66") + self.nodes[0].importprivkey("cTW5mR5M45vHxXkeChZdtSPozrFwFgmEvTNnanCW6wrqwaCZ1X7K") + self.create_and_mine_tx_from_txids(solvable_txid) + + # Test that importing native P2WPKH/P2WSH scripts works + for use_p2wsh in [False, True]: + if use_p2wsh: + scriptPubKey = "00203a59f3f56b713fdcf5d1a57357f02c44342cbf306ffe0c4741046837bf90561a" + transaction = "01000000000100e1f505000000002200203a59f3f56b713fdcf5d1a57357f02c44342cbf306ffe0c4741046837bf90561a00000000" + else: + scriptPubKey = "a9142f8c469c2f0084c48e11f998ffbe7efa7549f26d87" + transaction = "01000000000100e1f5050000000017a9142f8c469c2f0084c48e11f998ffbe7efa7549f26d8700000000" + + self.nodes[1].importaddress(scriptPubKey, "", False) + rawtxfund = self.nodes[1].fundrawtransaction(transaction)['hex'] + rawtxfund = self.nodes[1].signrawtransactionwithwallet(rawtxfund)["hex"] + txid = self.nodes[1].sendrawtransaction(rawtxfund) + + assert_equal(self.nodes[1].gettransaction(txid, True)["txid"], txid) + assert_equal(self.nodes[1].listtransactions("*", 1, 0, True)[0]["txid"], txid) + + # Assert it is properly saved + self.restart_node(1) + assert_equal(self.nodes[1].gettransaction(txid, True)["txid"], txid) + assert_equal(self.nodes[1].listtransactions("*", 1, 0, True)[0]["txid"], txid) def mine_and_test_listunspent(self, script_list, ismine): utxo = find_spendable_utxo(self.nodes[0], 50) @@ -597,13 +635,13 @@ class SegWitTest(BitcoinTestFramework): watchcount = 0 spendcount = 0 for i in self.nodes[0].listunspent(): - if (i['txid'] == txid): + if i['txid'] == txid: watchcount += 1 if i['spendable']: spendcount += 1 - if (ismine == 2): + if ismine == 2: assert_equal(spendcount, len(script_list)) - elif (ismine == 1): + elif ismine == 1: assert_equal(watchcount, len(script_list)) assert_equal(spendcount, 0) else: @@ -615,13 +653,13 @@ class SegWitTest(BitcoinTestFramework): p2sh = CScript(bytes.fromhex(v['scriptPubKey'])) p2wsh = script_to_p2wsh_script(bare) p2sh_p2wsh = script_to_p2sh_script(p2wsh) - return([bare, p2sh, p2wsh, p2sh_p2wsh]) + return [bare, p2sh, p2wsh, p2sh_p2wsh] def p2pkh_address_to_script(self, v): pubkey = bytes.fromhex(v['pubkey']) p2wpkh = key_to_p2wpkh_script(pubkey) p2sh_p2wpkh = script_to_p2sh_script(p2wpkh) - p2pk = CScript([pubkey, OP_CHECKSIG]) + p2pk = key_to_p2pk_script(pubkey) p2pkh = CScript(bytes.fromhex(v['scriptPubKey'])) p2sh_p2pk = script_to_p2sh_script(p2pk) p2sh_p2pkh = script_to_p2sh_script(p2pkh) diff --git a/test/functional/feature_syscall_sandbox.py b/test/functional/feature_syscall_sandbox.py new file mode 100755 index 0000000000..caf7f1e7fc --- /dev/null +++ b/test/functional/feature_syscall_sandbox.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 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 bitcoind aborts if a disallowed syscall is used when compiled with the syscall sandbox.""" + +from test_framework.test_framework import BitcoinTestFramework, SkipTest + + +class SyscallSandboxTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + + def skip_test_if_missing_module(self): + if not self.is_syscall_sandbox_compiled(): + raise SkipTest("bitcoind has not been built with syscall sandbox enabled.") + if self.options.nosandbox: + raise SkipTest("--nosandbox passed to test runner.") + + def run_test(self): + disallowed_syscall_terminated_bitcoind = False + expected_log_entry = 'ERROR: The syscall "getgroups" (syscall number 115) is not allowed by the syscall sandbox' + with self.nodes[0].assert_debug_log([expected_log_entry]): + self.log.info("Invoking disallowed syscall") + try: + self.nodes[0].invokedisallowedsyscall() + except ConnectionError: + disallowed_syscall_terminated_bitcoind = True + assert disallowed_syscall_terminated_bitcoind + self.nodes = [] + + +if __name__ == "__main__": + SyscallSandboxTest().main() diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py index c44a48f15f..50a25ee1ef 100755 --- a/test/functional/feature_taproot.py +++ b/test/functional/feature_taproot.py @@ -76,6 +76,7 @@ from test_framework.script import ( taproot_construct, ) from test_framework.script_util import ( + key_to_p2pk_script, key_to_p2wpkh_script, keyhash_to_p2pkh_script, script_to_p2sh_script, @@ -1109,7 +1110,7 @@ def spenders_taproot_active(): for witv0 in [False, True]: for hashtype in VALID_SIGHASHES_ECDSA + [random.randrange(0x04, 0x80), random.randrange(0x84, 0x100)]: standard = (hashtype in VALID_SIGHASHES_ECDSA) and (compressed or not witv0) - add_spender(spenders, "legacy/pk-wrongkey", hashtype=hashtype, p2sh=p2sh, witv0=witv0, standard=standard, script=CScript([pubkey1, OP_CHECKSIG]), **SINGLE_SIG, key=eckey1, failure={"key": eckey2}, sigops_weight=4-3*witv0, **ERR_NO_SUCCESS) + add_spender(spenders, "legacy/pk-wrongkey", hashtype=hashtype, p2sh=p2sh, witv0=witv0, standard=standard, script=key_to_p2pk_script(pubkey1), **SINGLE_SIG, key=eckey1, failure={"key": eckey2}, sigops_weight=4-3*witv0, **ERR_NO_SUCCESS) add_spender(spenders, "legacy/pkh-sighashflip", hashtype=hashtype, p2sh=p2sh, witv0=witv0, standard=standard, pkh=pubkey1, key=eckey1, **SIGHASH_BITFLIP, sigops_weight=4-3*witv0, **ERR_NO_SUCCESS) # Verify that OP_CHECKSIGADD wasn't accidentally added to pre-taproot validation logic. diff --git a/test/functional/feature_versionbits_warning.py b/test/functional/feature_versionbits_warning.py index 311d871d49..d74ef5e088 100755 --- a/test/functional/feature_versionbits_warning.py +++ b/test/functional/feature_versionbits_warning.py @@ -28,6 +28,9 @@ class VersionBitsWarningTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 + # The experimental syscall sandbox feature (-sandbox) is not compatible with -alertnotify + # (which invokes execve). + self.disable_syscall_sandbox = True def setup_network(self): self.alert_filename = os.path.join(self.options.tmpdir, "alert.txt") diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py index 89503adda3..c28186cde7 100755 --- a/test/functional/interface_bitcoin_cli.py +++ b/test/functional/interface_bitcoin_cli.py @@ -57,7 +57,7 @@ def cli_get_info_string_to_dict(cli_get_info_string): if key == 'Wallet' and value == '""': # Set default wallet("") to empty string value = '' - if key == "Proxy" and value == "N/A": + if key == "Proxies" and value == "n/a": # Set N/A to empty string to represent no proxy value = '' cli_get_info[key.strip()] = value.strip() @@ -127,10 +127,17 @@ class TestBitcoinCli(BitcoinTestFramework): assert_equal(int(cli_get_info['Time offset (s)']), network_info['timeoffset']) expected_network_info = f"in {network_info['connections_in']}, out {network_info['connections_out']}, total {network_info['connections']}" assert_equal(cli_get_info["Network"], expected_network_info) - assert_equal(cli_get_info['Proxy'], network_info['networks'][0]['proxy']) + assert_equal(cli_get_info['Proxies'], network_info['networks'][0]['proxy']) assert_equal(Decimal(cli_get_info['Difficulty']), blockchain_info['difficulty']) assert_equal(cli_get_info['Chain'], blockchain_info['chain']) + self.log.info("Test -getinfo and bitcoin-cli return all proxies") + self.restart_node(0, extra_args=["-proxy=127.0.0.1:9050", "-i2psam=127.0.0.1:7656"]) + network_info = self.nodes[0].getnetworkinfo() + cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli() + cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) + assert_equal(cli_get_info["Proxies"], "127.0.0.1:9050 (ipv4, ipv6, onion), 127.0.0.1:7656 (i2p)") + if self.is_wallet_compiled(): self.log.info("Test -getinfo and bitcoin-cli getwalletinfo return expected wallet info") assert_equal(Decimal(cli_get_info['Balance']), BALANCE) diff --git a/test/functional/interface_rest.py b/test/functional/interface_rest.py index e0716fc54a..868bb42604 100755 --- a/test/functional/interface_rest.py +++ b/test/functional/interface_rest.py @@ -279,6 +279,13 @@ class RESTTest (BitcoinTestFramework): json_obj = self.test_rest_request(f"/headers/5/{bb_hash}") assert_equal(len(json_obj), 5) # now we should have 5 header objects + # Test number parsing + for num in ['5a', '-5', '0', '2001', '99999999999999999999999999999999999']: + assert_equal( + bytes(f'Header count out of range: {num}\r\n', 'ascii'), + self.test_rest_request(f"/headers/{num}/{bb_hash}", ret_type=RetType.BYTES, status=400), + ) + self.log.info("Test tx inclusion in the /mempool and /block URIs") # Make 3 tx and mine them on node 1 @@ -311,6 +318,15 @@ class RESTTest (BitcoinTestFramework): if 'coinbase' not in tx['vin'][0]} assert_equal(non_coinbase_txs, set(txs)) + # Verify that the non-coinbase tx has "prevout" key set + for tx_obj in json_obj["tx"]: + for vin in tx_obj["vin"]: + if "coinbase" not in vin: + assert "prevout" in vin + assert_equal(vin["prevout"]["generated"], False) + else: + assert "prevout" not in vin + # Check the same but without tx details json_obj = self.test_rest_request(f"/block/notxdetails/{newblockhash[0]}") for tx in txs: diff --git a/test/functional/interface_zmq.py b/test/functional/interface_zmq.py index 4313b05f88..5a11a62ec4 100755 --- a/test/functional/interface_zmq.py +++ b/test/functional/interface_zmq.py @@ -583,7 +583,7 @@ class ZMQTest (BitcoinTestFramework): ], ipv6=True) # Generate 1 block in nodes[0] - self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE) + self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE) # Should receive the same block hash assert_equal(self.nodes[0].getbestblockhash(), subscribers[0].receive().hex()) diff --git a/test/functional/mempool_package_limits.py b/test/functional/mempool_package_limits.py index 2217628858..89a5c83826 100755 --- a/test/functional/mempool_package_limits.py +++ b/test/functional/mempool_package_limits.py @@ -244,7 +244,7 @@ class MempoolPackageLimitsTest(BitcoinTestFramework): assert_equal(txres["package-error"], "package-mempool-limits") # Clear mempool and check that the package passes now - node.generate(1) + self.generate(node, 1) assert all([res["allowed"] for res in node.testmempoolaccept(rawtxs=package_hex)]) def test_anc_count_limits(self): diff --git a/test/functional/mempool_packages.py b/test/functional/mempool_packages.py index b9344ad6da..ff5e45519f 100755 --- a/test/functional/mempool_packages.py +++ b/test/functional/mempool_packages.py @@ -14,7 +14,6 @@ from test_framework.util import ( assert_equal, assert_raises_rpc_error, chain_transaction, - satoshi_round, ) # default limits @@ -51,20 +50,33 @@ class MempoolPackagesTest(BitcoinTestFramework): txid = utxo[0]['txid'] vout = utxo[0]['vout'] value = utxo[0]['amount'] + assert 'ancestorcount' not in utxo[0] + assert 'ancestorsize' not in utxo[0] + assert 'ancestorfees' not in utxo[0] fee = Decimal("0.0001") # MAX_ANCESTORS transactions off a confirmed tx should be fine chain = [] witness_chain = [] - for _ in range(MAX_ANCESTORS): + ancestor_vsize = 0 + ancestor_fees = Decimal(0) + for i in range(MAX_ANCESTORS): (txid, sent_value) = chain_transaction(self.nodes[0], [txid], [0], value, fee, 1) value = sent_value chain.append(txid) # We need the wtxids to check P2P announcements - fulltx = self.nodes[0].getrawtransaction(txid) - witnesstx = self.nodes[0].decoderawtransaction(fulltx, True) + witnesstx = self.nodes[0].gettransaction(txid=txid, verbose=True)['decoded'] witness_chain.append(witnesstx['hash']) + # Check that listunspent ancestor{count, size, fees} yield the correct results + wallet_unspent = self.nodes[0].listunspent(minconf=0) + this_unspent = next(utxo_info for utxo_info in wallet_unspent if utxo_info['txid'] == txid) + assert_equal(this_unspent['ancestorcount'], i + 1) + ancestor_vsize += self.nodes[0].getrawtransaction(txid=txid, verbose=True)['vsize'] + assert_equal(this_unspent['ancestorsize'], ancestor_vsize) + ancestor_fees -= self.nodes[0].gettransaction(txid=txid)['fee'] + assert_equal(this_unspent['ancestorfees'], ancestor_fees * COIN) + # Wait until mempool transactions have passed initial broadcast (sent inv and received getdata) # Otherwise, getrawmempool may be inconsistent with getmempoolentry if unbroadcast changes in between peer_inv_store.wait_for_broadcast(witness_chain) @@ -77,9 +89,9 @@ class MempoolPackagesTest(BitcoinTestFramework): descendant_fees = 0 descendant_vsize = 0 - ancestor_vsize = sum([mempool[tx]['vsize'] for tx in mempool]) + assert_equal(ancestor_vsize, sum([mempool[tx]['vsize'] for tx in mempool])) ancestor_count = MAX_ANCESTORS - ancestor_fees = sum([mempool[tx]['fee'] for tx in mempool]) + assert_equal(ancestor_fees, sum([mempool[tx]['fee'] for tx in mempool])) descendants = [] ancestors = list(chain) @@ -195,10 +207,10 @@ class MempoolPackagesTest(BitcoinTestFramework): entry = self.nodes[0].getmempoolentry(x) descendant_fees += entry['fee'] if (x == chain[-1]): - assert_equal(entry['modifiedfee'], entry['fee']+satoshi_round(0.00002)) - assert_equal(entry['fees']['modified'], entry['fee']+satoshi_round(0.00002)) + assert_equal(entry['modifiedfee'], entry['fee'] + Decimal("0.00002")) + assert_equal(entry['fees']['modified'], entry['fee'] + Decimal("0.00002")) assert_equal(entry['descendantfees'], descendant_fees * COIN + 2000) - assert_equal(entry['fees']['descendant'], descendant_fees+satoshi_round(0.00002)) + assert_equal(entry['fees']['descendant'], descendant_fees + Decimal("0.00002")) # Check that node1's mempool is as expected (-> custom ancestor limit) mempool0 = self.nodes[0].getrawmempool(False) @@ -294,7 +306,7 @@ class MempoolPackagesTest(BitcoinTestFramework): value = utxo[0]['amount'] vout = utxo[0]['vout'] - send_value = satoshi_round((value - fee)/2) + send_value = (value - fee) / 2 inputs = [ {'txid' : txid, 'vout' : vout} ] outputs = {} for _ in range(2): diff --git a/test/functional/mempool_persist.py b/test/functional/mempool_persist.py index 56f7cbe6a5..71a132dca3 100755 --- a/test/functional/mempool_persist.py +++ b/test/functional/mempool_persist.py @@ -46,6 +46,7 @@ from test_framework.util import ( assert_greater_than_or_equal, assert_raises_rpc_error, ) +from test_framework.wallet import MiniWallet class MempoolPersistTest(BitcoinTestFramework): @@ -53,15 +54,26 @@ class MempoolPersistTest(BitcoinTestFramework): self.num_nodes = 3 self.extra_args = [[], ["-persistmempool=0"], []] - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - def run_test(self): + self.mini_wallet = MiniWallet(self.nodes[2]) + self.mini_wallet.rescan_utxos() + if self.is_sqlite_compiled(): + self.nodes[2].createwallet( + wallet_name="watch", + descriptors=True, + disable_private_keys=True, + load_on_startup=False, + ) + wallet_watch = self.nodes[2].get_wallet_rpc("watch") + assert_equal([{'success': True}], wallet_watch.importdescriptors([{'desc': self.mini_wallet.get_descriptor(), 'timestamp': 0}])) + self.log.debug("Send 5 transactions from node2 (to its own address)") tx_creation_time_lower = int(time.time()) for _ in range(5): - last_txid = self.nodes[2].sendtoaddress(self.nodes[2].getnewaddress(), Decimal("10")) - node2_balance = self.nodes[2].getbalance() + last_txid = self.mini_wallet.send_self_transfer(from_node=self.nodes[2])["txid"] + if self.is_sqlite_compiled(): + self.nodes[2].syncwithvalidationinterfacequeue() # Flush mempool to wallet + node2_balance = wallet_watch.getbalance() self.sync_all() tx_creation_time_higher = int(time.time()) @@ -82,16 +94,16 @@ class MempoolPersistTest(BitcoinTestFramework): assert_equal(total_fee_old, self.nodes[0].getmempoolinfo()['total_fee']) assert_equal(total_fee_old, sum(v['fees']['base'] for k, v in self.nodes[0].getrawmempool(verbose=True).items())) - tx_creation_time = self.nodes[0].getmempoolentry(txid=last_txid)['time'] + last_entry = self.nodes[0].getmempoolentry(txid=last_txid) + tx_creation_time = last_entry['time'] assert_greater_than_or_equal(tx_creation_time, tx_creation_time_lower) assert_greater_than_or_equal(tx_creation_time_higher, tx_creation_time) # disconnect nodes & make a txn that remains in the unbroadcast set. self.disconnect_nodes(0, 1) - assert(len(self.nodes[0].getpeerinfo()) == 0) - assert(len(self.nodes[0].p2ps) == 0) - self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), Decimal("12")) - self.connect_nodes(0, 2) + assert_equal(len(self.nodes[0].getpeerinfo()), 0) + assert_equal(len(self.nodes[0].p2ps), 0) + self.mini_wallet.send_self_transfer(from_node=self.nodes[0]) self.log.debug("Stop-start the nodes. Verify that node0 has the transactions in its mempool and node1 does not. Verify that node2 calculates its balance correctly after loading wallet transactions.") self.stop_nodes() @@ -111,17 +123,19 @@ class MempoolPersistTest(BitcoinTestFramework): fees = self.nodes[0].getmempoolentry(txid=last_txid)['fees'] assert_equal(fees['base'] + Decimal('0.00001000'), fees['modified']) - self.log.debug('Verify time is loaded correctly') - assert_equal(tx_creation_time, self.nodes[0].getmempoolentry(txid=last_txid)['time']) + self.log.debug('Verify all fields are loaded correctly') + assert_equal(last_entry, self.nodes[0].getmempoolentry(txid=last_txid)) # Verify accounting of mempool transactions after restart is correct - self.nodes[2].syncwithvalidationinterfacequeue() # Flush mempool to wallet - assert_equal(node2_balance, self.nodes[2].getbalance()) + if self.is_sqlite_compiled(): + self.nodes[2].loadwallet("watch") + wallet_watch = self.nodes[2].get_wallet_rpc("watch") + self.nodes[2].syncwithvalidationinterfacequeue() # Flush mempool to wallet + assert_equal(node2_balance, wallet_watch.getbalance()) - # start node0 with wallet disabled so wallet transactions don't get resubmitted self.log.debug("Stop-start node0 with -persistmempool=0. Verify that it doesn't load its mempool.dat file.") self.stop_nodes() - self.start_node(0, extra_args=["-persistmempool=0", "-disablewallet"]) + self.start_node(0, extra_args=["-persistmempool=0"]) assert self.nodes[0].getmempoolinfo()["loaded"] assert_equal(len(self.nodes[0].getrawmempool()), 0) @@ -141,7 +155,7 @@ class MempoolPersistTest(BitcoinTestFramework): self.log.debug("Stop nodes, make node1 use mempool.dat from node0. Verify it has 6 transactions") os.rename(mempooldat0, mempooldat1) self.stop_nodes() - self.start_node(1, extra_args=[]) + self.start_node(1, extra_args=["-persistmempool"]) assert self.nodes[1].getmempoolinfo()["loaded"] assert_equal(len(self.nodes[1].getrawmempool()), 6) @@ -164,18 +178,18 @@ class MempoolPersistTest(BitcoinTestFramework): # ensure node0 doesn't have any connections # make a transaction that will remain in the unbroadcast set - assert(len(node0.getpeerinfo()) == 0) - assert(len(node0.p2ps) == 0) - node0.sendtoaddress(self.nodes[1].getnewaddress(), Decimal("12")) + assert_equal(len(node0.getpeerinfo()), 0) + assert_equal(len(node0.p2ps), 0) + self.mini_wallet.send_self_transfer(from_node=node0) # shutdown, then startup with wallet disabled - self.stop_nodes() - self.start_node(0, extra_args=["-disablewallet"]) + self.restart_node(0, extra_args=["-disablewallet"]) # check that txn gets broadcast due to unbroadcast logic conn = node0.add_p2p_connection(P2PTxInvStore()) - node0.mockscheduler(16*60) # 15 min + 1 for buffer + node0.mockscheduler(16 * 60) # 15 min + 1 for buffer self.wait_until(lambda: len(conn.get_invs()) == 1) -if __name__ == '__main__': + +if __name__ == "__main__": MempoolPersistTest().main() diff --git a/test/functional/mempool_reorg.py b/test/functional/mempool_reorg.py index 0ee6af62f6..260b41ef12 100755 --- a/test/functional/mempool_reorg.py +++ b/test/functional/mempool_reorg.py @@ -31,7 +31,7 @@ class MempoolCoinbaseTest(BitcoinTestFramework): self.log.info("Add 4 coinbase utxos to the miniwallet") # Block 76 contains the first spendable coinbase txs. first_block = 76 - wallet.scan_blocks(start=first_block, num=4) + wallet.rescan_utxos() # Three scenarios for re-orging coinbase spends in the memory pool: # 1. Direct coinbase spend : spend_1 diff --git a/test/functional/mempool_spend_coinbase.py b/test/functional/mempool_spend_coinbase.py index e97595ed86..4e1dd80ba7 100755 --- a/test/functional/mempool_spend_coinbase.py +++ b/test/functional/mempool_spend_coinbase.py @@ -28,14 +28,14 @@ class MempoolSpendCoinbaseTest(BitcoinTestFramework): chain_height = 198 self.nodes[0].invalidateblock(self.nodes[0].getblockhash(chain_height + 1)) assert_equal(chain_height, self.nodes[0].getblockcount()) + wallet.rescan_utxos() # Coinbase at height chain_height-100+1 ok in mempool, should # get mined. Coinbase at height chain_height-100+2 is # too immature to spend. - wallet.scan_blocks(start=chain_height - 100 + 1, num=1) - utxo_mature = wallet.get_utxo() - wallet.scan_blocks(start=chain_height - 100 + 2, num=1) - utxo_immature = wallet.get_utxo() + coinbase_txid = lambda h: self.nodes[0].getblock(self.nodes[0].getblockhash(h))['tx'][0] + utxo_mature = wallet.get_utxo(txid=coinbase_txid(chain_height - 100 + 1)) + utxo_immature = wallet.get_utxo(txid=coinbase_txid(chain_height - 100 + 2)) spend_mature_id = wallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=utxo_mature)["txid"] diff --git a/test/functional/mining_prioritisetransaction.py b/test/functional/mining_prioritisetransaction.py index da85ee54be..35274d3500 100755 --- a/test/functional/mining_prioritisetransaction.py +++ b/test/functional/mining_prioritisetransaction.py @@ -13,7 +13,7 @@ from test_framework.util import assert_equal, assert_raises_rpc_error, create_co class PrioritiseTransactionTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True - self.num_nodes = 2 + self.num_nodes = 1 self.extra_args = [[ "-printpriority=1", "-acceptnonstdtxn=1", diff --git a/test/functional/p2p_addr_relay.py b/test/functional/p2p_addr_relay.py index e93698b08e..15b90fa61f 100755 --- a/test/functional/p2p_addr_relay.py +++ b/test/functional/p2p_addr_relay.py @@ -6,22 +6,22 @@ Test addr relay """ +import random +import time + from test_framework.messages import ( CAddress, - NODE_NETWORK, - NODE_WITNESS, msg_addr, msg_getaddr, - msg_verack + msg_verack, ) from test_framework.p2p import ( P2PInterface, p2p_lock, + P2P_SERVICES, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_greater_than -import random -import time class AddrReceiver(P2PInterface): @@ -96,7 +96,7 @@ class AddrTest(BitcoinTestFramework): for i in range(num): addr = CAddress() addr.time = self.mocktime + i - addr.nServices = NODE_NETWORK | NODE_WITNESS + addr.nServices = P2P_SERVICES addr.ip = f"123.123.123.{self.counter % 256}" addr.port = 8333 + i addrs.append(addr) @@ -111,7 +111,7 @@ class AddrTest(BitcoinTestFramework): for i in range(num): addr = CAddress() addr.time = self.mocktime + i - addr.nServices = NODE_NETWORK | NODE_WITNESS + addr.nServices = P2P_SERVICES addr.ip = f"{random.randrange(128,169)}.{random.randrange(1,255)}.{random.randrange(1,255)}.{random.randrange(1,255)}" addr.port = 8333 addrs.append(addr) diff --git a/test/functional/p2p_addrfetch.py b/test/functional/p2p_addrfetch.py index 2a0f6432a9..25efd50040 100755 --- a/test/functional/p2p_addrfetch.py +++ b/test/functional/p2p_addrfetch.py @@ -8,14 +8,21 @@ Test p2p addr-fetch connections import time -from test_framework.messages import msg_addr, CAddress, NODE_NETWORK, NODE_WITNESS -from test_framework.p2p import P2PInterface, p2p_lock +from test_framework.messages import ( + CAddress, + msg_addr, +) +from test_framework.p2p import ( + P2PInterface, + p2p_lock, + P2P_SERVICES, +) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal ADDR = CAddress() ADDR.time = int(time.time()) -ADDR.nServices = NODE_NETWORK | NODE_WITNESS +ADDR.nServices = P2P_SERVICES ADDR.ip = "192.0.0.8" ADDR.port = 18444 diff --git a/test/functional/p2p_addrv2_relay.py b/test/functional/p2p_addrv2_relay.py index 32c1d42b1c..3833c58680 100755 --- a/test/functional/p2p_addrv2_relay.py +++ b/test/functional/p2p_addrv2_relay.py @@ -11,10 +11,11 @@ import time from test_framework.messages import ( CAddress, msg_addrv2, - NODE_NETWORK, - NODE_WITNESS, ) -from test_framework.p2p import P2PInterface +from test_framework.p2p import ( + P2PInterface, + P2P_SERVICES, +) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal @@ -24,7 +25,7 @@ ADDRS = [] for i in range(10): addr = CAddress() addr.time = int(time.time()) + i - addr.nServices = NODE_NETWORK | NODE_WITNESS + addr.nServices = P2P_SERVICES # Add one I2P address at an arbitrary position. if i == 5: addr.net = addr.NET_I2P diff --git a/test/functional/p2p_compactblocks_blocksonly.py b/test/functional/p2p_compactblocks_blocksonly.py new file mode 100755 index 0000000000..5f01fa4dfe --- /dev/null +++ b/test/functional/p2p_compactblocks_blocksonly.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021-2021 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 that a node in blocksonly mode does not request compact blocks.""" + +from test_framework.messages import ( + MSG_BLOCK, + MSG_CMPCT_BLOCK, + MSG_WITNESS_FLAG, + CBlock, + CBlockHeader, + CInv, + from_hex, + msg_block, + msg_getdata, + msg_headers, + msg_sendcmpct, +) +from test_framework.p2p import P2PInterface +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + + +class P2PCompactBlocksBlocksOnly(BitcoinTestFramework): + def set_test_params(self): + self.extra_args = [["-blocksonly"], [], [], []] + self.num_nodes = 4 + + def setup_network(self): + self.setup_nodes() + # Start network with everyone disconnected + self.sync_all() + + def build_block_on_tip(self): + blockhash = self.generate(self.nodes[2], 1)[0] + block_hex = self.nodes[2].getblock(blockhash=blockhash, verbosity=0) + block = from_hex(CBlock(), block_hex) + block.rehash() + return block + + def run_test(self): + # Nodes will only request hb compact blocks mode when they're out of IBD + for node in self.nodes: + assert not node.getblockchaininfo()['initialblockdownload'] + + p2p_conn_blocksonly = self.nodes[0].add_p2p_connection(P2PInterface()) + p2p_conn_high_bw = self.nodes[1].add_p2p_connection(P2PInterface()) + p2p_conn_low_bw = self.nodes[3].add_p2p_connection(P2PInterface()) + for conn in [p2p_conn_blocksonly, p2p_conn_high_bw, p2p_conn_low_bw]: + assert_equal(conn.message_count['sendcmpct'], 2) + conn.send_and_ping(msg_sendcmpct(announce=False, version=2)) + + # Nodes: + # 0 -> blocksonly + # 1 -> high bandwidth + # 2 -> miner + # 3 -> low bandwidth + # + # Topology: + # p2p_conn_blocksonly ---> node0 + # p2p_conn_high_bw ---> node1 + # p2p_conn_low_bw ---> node3 + # node2 (no connections) + # + # node2 produces blocks that are passed to the rest of the nodes + # through the respective p2p connections. + + self.log.info("Test that -blocksonly nodes do not select peers for BIP152 high bandwidth mode") + + block0 = self.build_block_on_tip() + + # A -blocksonly node should not request BIP152 high bandwidth mode upon + # receiving a new valid block at the tip. + p2p_conn_blocksonly.send_and_ping(msg_block(block0)) + assert_equal(int(self.nodes[0].getbestblockhash(), 16), block0.sha256) + assert_equal(p2p_conn_blocksonly.message_count['sendcmpct'], 2) + assert_equal(p2p_conn_blocksonly.last_message['sendcmpct'].announce, False) + + # A normal node participating in transaction relay should request BIP152 + # high bandwidth mode upon receiving a new valid block at the tip. + p2p_conn_high_bw.send_and_ping(msg_block(block0)) + assert_equal(int(self.nodes[1].getbestblockhash(), 16), block0.sha256) + p2p_conn_high_bw.wait_until(lambda: p2p_conn_high_bw.message_count['sendcmpct'] == 3) + assert_equal(p2p_conn_high_bw.last_message['sendcmpct'].announce, True) + + # Don't send a block from the p2p_conn_low_bw so the low bandwidth node + # doesn't select it for BIP152 high bandwidth relay. + self.nodes[3].submitblock(block0.serialize().hex()) + + self.log.info("Test that -blocksonly nodes send getdata(BLOCK) instead" + " of getdata(CMPCT) in BIP152 low bandwidth mode") + + block1 = self.build_block_on_tip() + + p2p_conn_blocksonly.send_message(msg_headers(headers=[CBlockHeader(block1)])) + p2p_conn_blocksonly.sync_send_with_ping() + assert_equal(p2p_conn_blocksonly.last_message['getdata'].inv, [CInv(MSG_BLOCK | MSG_WITNESS_FLAG, block1.sha256)]) + + p2p_conn_high_bw.send_message(msg_headers(headers=[CBlockHeader(block1)])) + p2p_conn_high_bw.sync_send_with_ping() + assert_equal(p2p_conn_high_bw.last_message['getdata'].inv, [CInv(MSG_CMPCT_BLOCK, block1.sha256)]) + + self.log.info("Test that getdata(CMPCT) is still sent on BIP152 low bandwidth connections" + " when no -blocksonly nodes are involved") + + p2p_conn_low_bw.send_and_ping(msg_headers(headers=[CBlockHeader(block1)])) + p2p_conn_low_bw.sync_with_ping() + assert_equal(p2p_conn_low_bw.last_message['getdata'].inv, [CInv(MSG_CMPCT_BLOCK, block1.sha256)]) + + self.log.info("Test that -blocksonly nodes still serve compact blocks") + + def test_for_cmpctblock(block): + if 'cmpctblock' not in p2p_conn_blocksonly.last_message: + return False + return p2p_conn_blocksonly.last_message['cmpctblock'].header_and_shortids.header.rehash() == block.sha256 + + p2p_conn_blocksonly.send_message(msg_getdata([CInv(MSG_CMPCT_BLOCK, block0.sha256)])) + p2p_conn_blocksonly.wait_until(lambda: test_for_cmpctblock(block0)) + + # Request BIP152 high bandwidth mode from the -blocksonly node. + p2p_conn_blocksonly.send_and_ping(msg_sendcmpct(announce=True, version=2)) + + block2 = self.build_block_on_tip() + self.nodes[0].submitblock(block1.serialize().hex()) + self.nodes[0].submitblock(block2.serialize().hex()) + p2p_conn_blocksonly.wait_until(lambda: test_for_cmpctblock(block2)) + +if __name__ == '__main__': + P2PCompactBlocksBlocksOnly().main() diff --git a/test/functional/p2p_filter.py b/test/functional/p2p_filter.py index a040665fba..0d8c298bea 100755 --- a/test/functional/p2p_filter.py +++ b/test/functional/p2p_filter.py @@ -8,6 +8,7 @@ Test BIP 37 from test_framework.messages import ( CInv, + COIN, MAX_BLOOM_FILTER_SIZE, MAX_BLOOM_HASH_FUNCS, MSG_BLOCK, @@ -28,11 +29,15 @@ from test_framework.p2p import ( ) from test_framework.script import MAX_SCRIPT_ELEMENT_SIZE from test_framework.test_framework import BitcoinTestFramework +from test_framework.wallet import ( + MiniWallet, + random_p2wpkh, +) class P2PBloomFilter(P2PInterface): # This is a P2SH watch-only wallet - watch_script_pubkey = 'a914ffffffffffffffffffffffffffffffffffffffff87' + watch_script_pubkey = bytes.fromhex('a914ffffffffffffffffffffffffffffffffffffffff87') # The initial filter (n=10, fp=0.000001) with just the above scriptPubKey added watch_filter_init = msg_filterload( data= @@ -93,8 +98,9 @@ class FilterTest(BitcoinTestFramework): '-whitelist=noban@127.0.0.1', # immediate tx relay ]] - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() + def generatetoscriptpubkey(self, scriptpubkey): + """Helper to generate a single block to the given scriptPubKey.""" + return self.generatetodescriptor(self.nodes[0], 1, f'raw({scriptpubkey.hex()})')[0] def test_size_limits(self, filter_peer): self.log.info('Check that too large filter is rejected') @@ -130,8 +136,7 @@ class FilterTest(BitcoinTestFramework): filter_peer = P2PBloomFilter() self.log.debug("Create a tx relevant to the peer before connecting") - filter_address = self.nodes[0].decodescript(filter_peer.watch_script_pubkey)['address'] - txid = self.nodes[0].sendtoaddress(filter_address, 90) + txid, _ = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=filter_peer.watch_script_pubkey, amount=9 * COIN) self.log.debug("Send a mempool msg after connecting and check that the tx is received") self.nodes[0].add_p2p_connection(filter_peer) @@ -142,8 +147,7 @@ class FilterTest(BitcoinTestFramework): 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") filter_peer.tx_received = False - filter_address = self.nodes[0].decodescript(filter_peer.watch_script_pubkey)['address'] - self.nodes[0].sendtoaddress(filter_address, 90) + self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=filter_peer.watch_script_pubkey, amount=9 * COIN) # Sync to make sure the reason filter_peer doesn't receive the tx is not p2p delays filter_peer.sync_with_ping() assert not filter_peer.tx_received @@ -156,45 +160,44 @@ class FilterTest(BitcoinTestFramework): filter_peer.send_and_ping(filter_peer.watch_filter_init) # If fRelay is not already True, sending filterload sets it to True assert self.nodes[0].getpeerinfo()[0]['relaytxes'] - filter_address = self.nodes[0].decodescript(filter_peer.watch_script_pubkey)['address'] self.log.info('Check that we receive merkleblock and tx if the filter matches a tx in a block') - block_hash = self.generatetoaddress(self.nodes[0], 1, filter_address)[0] + block_hash = self.generatetoscriptpubkey(filter_peer.watch_script_pubkey) txid = self.nodes[0].getblock(block_hash)['tx'][0] filter_peer.wait_for_merkleblock(block_hash) filter_peer.wait_for_tx(txid) self.log.info('Check that we only receive a merkleblock if the filter does not match a tx in a block') filter_peer.tx_received = False - block_hash = self.generatetoaddress(self.nodes[0], 1, self.nodes[0].getnewaddress())[0] + block_hash = self.generatetoscriptpubkey(random_p2wpkh()) filter_peer.wait_for_merkleblock(block_hash) assert not filter_peer.tx_received self.log.info('Check that we not receive a tx if the filter does not match a mempool tx') filter_peer.merkleblock_received = False filter_peer.tx_received = False - self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 90) + self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=random_p2wpkh(), amount=7 * COIN) filter_peer.sync_send_with_ping() assert not filter_peer.merkleblock_received assert not filter_peer.tx_received self.log.info('Check that we receive a tx if the filter matches a mempool tx') filter_peer.merkleblock_received = False - txid = self.nodes[0].sendtoaddress(filter_address, 90) + txid, _ = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=filter_peer.watch_script_pubkey, amount=9 * COIN) filter_peer.wait_for_tx(txid) assert not filter_peer.merkleblock_received self.log.info('Check that after deleting filter all txs get relayed again') filter_peer.send_and_ping(msg_filterclear()) for _ in range(5): - txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 7) + txid, _ = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=random_p2wpkh(), amount=7 * COIN) filter_peer.wait_for_tx(txid) self.log.info('Check that request for filtered blocks is ignored if no filter is set') filter_peer.merkleblock_received = False filter_peer.tx_received = False with self.nodes[0].assert_debug_log(expected_msgs=['received getdata']): - block_hash = self.generatetoaddress(self.nodes[0], 1, self.nodes[0].getnewaddress())[0] + block_hash = self.generatetoscriptpubkey(random_p2wpkh()) filter_peer.wait_for_inv([CInv(MSG_BLOCK, int(block_hash, 16))]) filter_peer.sync_with_ping() assert not filter_peer.merkleblock_received @@ -210,6 +213,9 @@ class FilterTest(BitcoinTestFramework): self.nodes[0].disconnect_p2ps() def run_test(self): + self.wallet = MiniWallet(self.nodes[0]) + self.wallet.rescan_utxos() + filter_peer = self.nodes[0].add_p2p_connection(P2PBloomFilter()) self.log.info('Test filter size limits') self.test_size_limits(filter_peer) diff --git a/test/functional/p2p_invalid_messages.py b/test/functional/p2p_invalid_messages.py index f3b80abb59..82c7e94c59 100755 --- a/test/functional/p2p_invalid_messages.py +++ b/test/functional/p2p_invalid_messages.py @@ -209,7 +209,7 @@ class InvalidMessagesTest(BitcoinTestFramework): self.test_addrv2('unrecognized network', [ 'received: addrv2 (25 bytes)', - 'IP 9.9.9.9 mapped', + '9.9.9.9:8333 mapped', 'Added 1 addresses', ], bytes.fromhex( diff --git a/test/functional/p2p_ping.py b/test/functional/p2p_ping.py index 888e986fba..d67e97acf7 100755 --- a/test/functional/p2p_ping.py +++ b/test/functional/p2p_ping.py @@ -30,11 +30,16 @@ class NodeNoPong(P2PInterface): pass +TIMEOUT_INTERVAL = 20 * 60 + + class PingPongTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 - self.extra_args = [['-peertimeout=3']] + # Set the peer connection timeout low. It does not matter for this + # test, as long as it is less than TIMEOUT_INTERVAL. + self.extra_args = [['-peertimeout=1']] def check_peer_info(self, *, pingtime, minping, pingwait): stats = self.nodes[0].getpeerinfo()[0] @@ -110,8 +115,11 @@ class PingPongTest(BitcoinTestFramework): self.nodes[0].ping() no_pong_node.wait_until(lambda: 'ping' in no_pong_node.last_message) with self.nodes[0].assert_debug_log(['ping timeout: 1201.000000s']): - self.mock_forward(20 * 60 + 1) - time.sleep(4) # peertimeout + 1 + self.mock_forward(TIMEOUT_INTERVAL // 2) + # Check that sending a ping does not prevent the disconnect + no_pong_node.sync_with_ping() + self.mock_forward(TIMEOUT_INTERVAL // 2 + 1) + no_pong_node.wait_for_disconnect() if __name__ == '__main__': diff --git a/test/functional/p2p_segwit.py b/test/functional/p2p_segwit.py index a71f736bd6..5195c9fd04 100755 --- a/test/functional/p2p_segwit.py +++ b/test/functional/p2p_segwit.py @@ -8,7 +8,12 @@ import random import struct import time -from test_framework.blocktools import create_block, create_coinbase, add_witness_commitment, WITNESS_COMMITMENT_HEADER +from test_framework.blocktools import ( + WITNESS_COMMITMENT_HEADER, + add_witness_commitment, + create_block, + create_coinbase, +) from test_framework.key import ECKey from test_framework.messages import ( BIP125_SEQUENCE_NUMBER, @@ -43,6 +48,7 @@ from test_framework.messages import ( from test_framework.p2p import ( P2PInterface, p2p_lock, + P2P_SERVICES, ) from test_framework.script import ( CScript, @@ -71,6 +77,7 @@ from test_framework.script import ( hash160, ) from test_framework.script_util import ( + key_to_p2pk_script, key_to_p2wpkh_script, keyhash_to_p2pkh_script, script_to_p2sh_script, @@ -83,10 +90,6 @@ from test_framework.util import ( assert_raises_rpc_error, ) -# The versionbit bit used to signal activation of SegWit -VB_WITNESS_BIT = 1 -VB_TOP_BITS = 0x20000000 - MAX_SIGOP_COST = 80000 SEGWIT_HEIGHT = 120 @@ -196,8 +199,8 @@ class SegWitTest(BitcoinTestFramework): self.num_nodes = 2 # This test tests SegWit both pre and post-activation, so use the normal BIP9 activation. self.extra_args = [ - ["-acceptnonstdtxn=1", "-segwitheight={}".format(SEGWIT_HEIGHT), "-whitelist=noban@127.0.0.1"], - ["-acceptnonstdtxn=0", "-segwitheight={}".format(SEGWIT_HEIGHT)], + ["-acceptnonstdtxn=1", f"-testactivationheight=segwit@{SEGWIT_HEIGHT}", "-whitelist=noban@127.0.0.1", "-par=1"], + ["-acceptnonstdtxn=0", f"-testactivationheight=segwit@{SEGWIT_HEIGHT}"], ] self.supports_cli = False @@ -206,13 +209,13 @@ class SegWitTest(BitcoinTestFramework): # Helper functions - def build_next_block(self, version=4): + def build_next_block(self): """Build a block on top of node0's tip.""" tip = self.nodes[0].getbestblockhash() height = self.nodes[0].getblockcount() + 1 block_time = self.nodes[0].getblockheader(tip)["mediantime"] + 1 block = create_block(int(tip, 16), create_coinbase(height), block_time) - block.nVersion = version + block.nVersion = 4 block.rehash() return block @@ -224,14 +227,14 @@ class SegWitTest(BitcoinTestFramework): def run_test(self): # Setup the p2p connections - # self.test_node sets NODE_WITNESS|NODE_NETWORK - self.test_node = self.nodes[0].add_p2p_connection(TestP2PConn(), services=NODE_NETWORK | NODE_WITNESS) + # self.test_node sets P2P_SERVICES, i.e. NODE_WITNESS | NODE_NETWORK + self.test_node = self.nodes[0].add_p2p_connection(TestP2PConn(), services=P2P_SERVICES) # self.old_node sets only NODE_NETWORK self.old_node = self.nodes[0].add_p2p_connection(TestP2PConn(), services=NODE_NETWORK) # self.std_node is for testing node1 (fRequireStandard=true) - self.std_node = self.nodes[1].add_p2p_connection(TestP2PConn(), services=NODE_NETWORK | NODE_WITNESS) + self.std_node = self.nodes[1].add_p2p_connection(TestP2PConn(), services=P2P_SERVICES) # self.std_wtx_node is for testing node1 with wtxid relay - self.std_wtx_node = self.nodes[1].add_p2p_connection(TestP2PConn(wtxidrelay=True), services=NODE_NETWORK | NODE_WITNESS) + self.std_wtx_node = self.nodes[1].add_p2p_connection(TestP2PConn(wtxidrelay=True), services=P2P_SERVICES) assert self.test_node.nServices & NODE_WITNESS != 0 @@ -298,7 +301,7 @@ class SegWitTest(BitcoinTestFramework): # Mine a block with an anyone-can-spend coinbase, # let it mature, then try to spend it. - block = self.build_next_block(version=1) + block = self.build_next_block() block.solve() self.test_node.send_and_ping(msg_no_witness_block(block)) # make sure the block was processed txid = block.vtx[0].sha256 @@ -336,8 +339,8 @@ class SegWitTest(BitcoinTestFramework): tx.rehash() assert tx.sha256 != tx.calc_sha256(with_witness=True) - # Construct a segwit-signaling block that includes the transaction. - block = self.build_next_block(version=(VB_TOP_BITS | (1 << VB_WITNESS_BIT))) + # Construct a block that includes the transaction. + block = self.build_next_block() self.update_witness_block_with_transactions(block, [tx]) # Sending witness data before activation is not allowed (anti-spam # rule). @@ -364,7 +367,7 @@ class SegWitTest(BitcoinTestFramework): # test_node has set NODE_WITNESS, so all getdata requests should be for # witness blocks. # Test announcing a block via inv results in a getdata, and that - # announcing a version 4 or random VB block with a header results in a getdata + # announcing a block with a header results in a getdata block1 = self.build_next_block() block1.solve() @@ -372,19 +375,13 @@ class SegWitTest(BitcoinTestFramework): assert self.test_node.last_message["getdata"].inv[0].type == blocktype test_witness_block(self.nodes[0], self.test_node, block1, True) - block2 = self.build_next_block(version=4) + block2 = self.build_next_block() block2.solve() self.test_node.announce_block_and_wait_for_getdata(block2, use_header=True) assert self.test_node.last_message["getdata"].inv[0].type == blocktype test_witness_block(self.nodes[0], self.test_node, block2, True) - block3 = self.build_next_block(version=(VB_TOP_BITS | (1 << 15))) - block3.solve() - self.test_node.announce_block_and_wait_for_getdata(block3, use_header=True) - assert self.test_node.last_message["getdata"].inv[0].type == blocktype - test_witness_block(self.nodes[0], self.test_node, block3, True) - # Check that we can getdata for witness blocks or regular blocks, # and the right thing happens. if not self.segwit_active: @@ -429,7 +426,7 @@ class SegWitTest(BitcoinTestFramework): assert_equal(rpc_details["weight"], block.get_weight()) # Upgraded node should not ask for blocks from unupgraded - block4 = self.build_next_block(version=4) + block4 = self.build_next_block() block4.solve() self.old_node.getdataset = set() @@ -513,8 +510,8 @@ class SegWitTest(BitcoinTestFramework): # 'block-validation-failed' (if script check threads > 1) or # 'non-mandatory-script-verify-flag (Witness program was passed an # empty witness)' (otherwise). - # TODO: support multiple acceptable reject reasons. - test_witness_block(self.nodes[0], self.test_node, block, accepted=False, with_witness=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, with_witness=False, + reason='non-mandatory-script-verify-flag (Witness program was passed an empty witness)') self.utxo.pop(0) self.utxo.append(UTXO(txid, 2, value)) @@ -794,7 +791,7 @@ class SegWitTest(BitcoinTestFramework): block_3.rehash() block_3.solve() - test_witness_block(self.nodes[0], self.test_node, block_3, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block_3, accepted=False, reason='bad-witness-merkle-match') # Add a different commitment with different nonce, but in the # right location, and with some funds burned(!). @@ -860,7 +857,7 @@ class SegWitTest(BitcoinTestFramework): # Change the nonce -- should not cause the block to be permanently # failed block.vtx[0].wit.vtxinwit[0].scriptWitness.stack = [ser_uint256(1)] - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, reason='bad-witness-merkle-match') # Changing the witness reserved value doesn't change the block hash block.vtx[0].wit.vtxinwit[0].scriptWitness.stack = [ser_uint256(0)] @@ -869,7 +866,7 @@ class SegWitTest(BitcoinTestFramework): @subtest # type: ignore def test_witness_block_size(self): # TODO: Test that non-witness carrying blocks can't exceed 1MB - # Skipping this test for now; this is covered in p2p-fullblocktest.py + # Skipping this test for now; this is covered in feature_block.py # Test that witness-bearing blocks are limited at ceil(base + wit/4) <= 1MB. block = self.build_next_block() @@ -925,7 +922,7 @@ class SegWitTest(BitcoinTestFramework): # limit assert len(block.serialize()) > 2 * 1024 * 1024 - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, reason='bad-blk-weight') # Now resize the second transaction to make the block fit. cur_length = len(block.vtx[-1].wit.vtxinwit[0].scriptWitness.stack[0]) @@ -997,7 +994,8 @@ class SegWitTest(BitcoinTestFramework): self.update_witness_block_with_transactions(block, [tx]) # Extra witness data should not be allowed. - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Witness provided for non-witness script)') # Try extra signature data. Ok if we're not spending a witness output. block.vtx[1].wit.vtxinwit = [] @@ -1022,7 +1020,8 @@ class SegWitTest(BitcoinTestFramework): self.update_witness_block_with_transactions(block, [tx2]) # This has extra witness data, so it should fail. - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Stack size must be exactly one after execution)') # Now get rid of the extra witness, but add extra scriptSig data tx2.vin[0].scriptSig = CScript([OP_TRUE]) @@ -1034,7 +1033,8 @@ class SegWitTest(BitcoinTestFramework): block.solve() # This has extra signature data for a witness input, so it should fail. - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Witness requires empty scriptSig)') # Now get rid of the extra scriptsig on the witness input, and verify # success (even with extra scriptsig data in the non-witness input) @@ -1072,7 +1072,8 @@ class SegWitTest(BitcoinTestFramework): tx2.rehash() self.update_witness_block_with_transactions(block, [tx, tx2]) - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Push value size limit exceeded)') # Now reduce the length of the stack element tx2.wit.vtxinwit[0].scriptWitness.stack[0] = b'a' * (MAX_SCRIPT_ELEMENT_SIZE) @@ -1112,7 +1113,8 @@ class SegWitTest(BitcoinTestFramework): self.update_witness_block_with_transactions(block, [tx, tx2]) - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Script is too big)') # Try again with one less byte in the witness script witness_script = CScript([b'a' * MAX_SCRIPT_ELEMENT_SIZE] * 19 + [OP_DROP] * 62 + [OP_TRUE]) @@ -1184,7 +1186,7 @@ class SegWitTest(BitcoinTestFramework): block = self.build_next_block() self.update_witness_block_with_transactions(block, [tx2]) - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, reason='bad-txnmrklroot') # Now try using a too short vtxinwit tx2.wit.vtxinwit.pop() @@ -1192,6 +1194,8 @@ class SegWitTest(BitcoinTestFramework): block.vtx = [block.vtx[0]] self.update_witness_block_with_transactions(block, [tx2]) + # This block doesn't result in a specific reject reason, but an iostream exception: + # "Exception 'CDataStream::read(): end of data: unspecified iostream_category error' (...) caught" test_witness_block(self.nodes[0], self.test_node, block, accepted=False) # Now make one of the intermediate witnesses be incorrect @@ -1201,7 +1205,8 @@ class SegWitTest(BitcoinTestFramework): block.vtx = [block.vtx[0]] self.update_witness_block_with_transactions(block, [tx2]) - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Operation not valid with the current stack size)') # Fix the broken witness and the block should be accepted. tx2.wit.vtxinwit[5].scriptWitness.stack = [b'a', witness_script] @@ -1423,7 +1428,7 @@ class SegWitTest(BitcoinTestFramework): self.sync_blocks() block2 = self.build_next_block() self.update_witness_block_with_transactions(block2, [spend_tx]) - test_witness_block(self.nodes[0], self.test_node, block2, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block2, accepted=False, reason='bad-txns-premature-spend-of-coinbase') # Advancing one more block should allow the spend. self.generate(self.nodes[0], 1) @@ -1464,7 +1469,7 @@ class SegWitTest(BitcoinTestFramework): # Now try to spend it. Send it to a P2WSH output, which we'll # use in the next test. - witness_script = CScript([pubkey, CScriptOp(OP_CHECKSIG)]) + witness_script = key_to_p2pk_script(pubkey) script_wsh = script_to_p2wsh_script(witness_script) tx2 = CTransaction() @@ -1542,7 +1547,7 @@ class SegWitTest(BitcoinTestFramework): key.generate() pubkey = key.get_pubkey().get_bytes() - witness_script = CScript([pubkey, CScriptOp(OP_CHECKSIG)]) + witness_script = key_to_p2pk_script(pubkey) script_pubkey = script_to_p2wsh_script(witness_script) # First create a witness output for use in the tests. @@ -1572,13 +1577,17 @@ class SegWitTest(BitcoinTestFramework): # Too-large input value sign_p2pk_witness_input(witness_script, tx, 0, hashtype, prev_utxo.nValue + 1, key) self.update_witness_block_with_transactions(block, [tx]) - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Script evaluated without error ' + 'but finished with a false/empty top stack element') # Too-small input value sign_p2pk_witness_input(witness_script, tx, 0, hashtype, prev_utxo.nValue - 1, key) block.vtx.pop() # remove last tx self.update_witness_block_with_transactions(block, [tx]) - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Script evaluated without error ' + 'but finished with a false/empty top stack element') # Now try correct value sign_p2pk_witness_input(witness_script, tx, 0, hashtype, prev_utxo.nValue, key) @@ -1680,7 +1689,8 @@ class SegWitTest(BitcoinTestFramework): tx2.vin[0].scriptSig = CScript([signature, pubkey]) block = self.build_next_block() self.update_witness_block_with_transactions(block, [tx, tx2]) - test_witness_block(self.nodes[0], self.test_node, block, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block, accepted=False, + reason='non-mandatory-script-verify-flag (Witness requires empty scriptSig)') # Move the signature to the witness. block.vtx.pop() @@ -1926,7 +1936,7 @@ class SegWitTest(BitcoinTestFramework): block_2 = self.build_next_block() self.update_witness_block_with_transactions(block_2, [tx2]) - test_witness_block(self.nodes[0], self.test_node, block_2, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block_2, accepted=False, reason='bad-blk-sigops') # Try dropping the last input in tx2, and add an output that has # too many sigops (contributing to legacy sigop count). @@ -1939,7 +1949,7 @@ class SegWitTest(BitcoinTestFramework): tx2.rehash() block_3 = self.build_next_block() self.update_witness_block_with_transactions(block_3, [tx2]) - test_witness_block(self.nodes[0], self.test_node, block_3, accepted=False) + test_witness_block(self.nodes[0], self.test_node, block_3, accepted=False, reason='bad-blk-sigops') # If we drop the last checksig in this output, the tx should succeed. block_4 = self.build_next_block() @@ -2017,8 +2027,8 @@ class SegWitTest(BitcoinTestFramework): @subtest # type: ignore def test_wtxid_relay(self): # Use brand new nodes to avoid contamination from earlier tests - self.wtx_node = self.nodes[0].add_p2p_connection(TestP2PConn(wtxidrelay=True), services=NODE_NETWORK | NODE_WITNESS) - self.tx_node = self.nodes[0].add_p2p_connection(TestP2PConn(wtxidrelay=False), services=NODE_NETWORK | NODE_WITNESS) + self.wtx_node = self.nodes[0].add_p2p_connection(TestP2PConn(wtxidrelay=True), services=P2P_SERVICES) + self.tx_node = self.nodes[0].add_p2p_connection(TestP2PConn(wtxidrelay=False), services=P2P_SERVICES) # Check wtxidrelay feature negotiation message through connecting a new peer def received_wtxidrelay(): diff --git a/test/functional/rpc_addresses_deprecation.py b/test/functional/rpc_addresses_deprecation.py deleted file mode 100755 index e566fb0aa7..0000000000 --- a/test/functional/rpc_addresses_deprecation.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2020 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 deprecation of reqSigs and addresses RPC fields.""" - -from test_framework.messages import ( - tx_from_hex, -) -from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import ( - assert_equal, -) - - -class AddressesDeprecationTest(BitcoinTestFramework): - def set_test_params(self): - self.num_nodes = 2 - self.extra_args = [[], ["-deprecatedrpc=addresses"]] - - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - - def run_test(self): - self.test_addresses_deprecation() - - def test_addresses_deprecation(self): - node = self.nodes[0] - coin = node.listunspent().pop() - - inputs = [{'txid': coin['txid'], 'vout': coin['vout']}] - outputs = {node.getnewaddress(): 0.99} - raw = node.createrawtransaction(inputs, outputs) - signed = node.signrawtransactionwithwallet(raw)['hex'] - - # This transaction is derived from test/util/data/txcreatemultisig1.json - tx = tx_from_hex(signed) - tx.vout[0].scriptPubKey = bytes.fromhex("522102a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff39721021ac43c7ff740014c3b33737ede99c967e4764553d1b2b83db77c83b8715fa72d2102df2089105c77f266fa11a9d33f05c735234075f2e8780824c6b709415f9fb48553ae") - tx_signed = node.signrawtransactionwithwallet(tx.serialize().hex())['hex'] - txid = node.sendrawtransaction(hexstring=tx_signed, maxfeerate=0) - - self.log.info("Test RPCResult scriptPubKey no longer returns the fields addresses or reqSigs by default") - hash = self.generateblock(node, output=node.getnewaddress(), transactions=[txid])['hash'] - # Ensure both nodes have the newly generated block on disk. - self.sync_blocks() - script_pub_key = node.getblock(blockhash=hash, verbose=2)['tx'][-1]['vout'][0]['scriptPubKey'] - assert 'addresses' not in script_pub_key and 'reqSigs' not in script_pub_key - - self.log.info("Test RPCResult scriptPubKey returns the addresses field with -deprecatedrpc=addresses") - script_pub_key = self.nodes[1].getblock(blockhash=hash, verbose=2)['tx'][-1]['vout'][0]['scriptPubKey'] - assert_equal(script_pub_key['addresses'], ['mvKDK6D54HU8wQumJBLHY95eq5iHFqXSBz', 'mv3rHCQSwKp2BLSuMHD8uCS32LW5xiNAA5', 'mirrsyhAQYzo5CwVhcaYJKwUJu1WJRCRJe']) - assert_equal(script_pub_key['reqSigs'], 2) - - -if __name__ == "__main__": - AddressesDeprecationTest().main() diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index e13de4395b..eea9ee26cb 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -27,8 +27,6 @@ import subprocess from test_framework.address import ADDRESS_BCRT1_P2WSH_OP_TRUE from test_framework.blocktools import ( - CLTV_HEIGHT, - DERSIG_HEIGHT, create_block, create_coinbase, TIME_GENESIS_BLOCK, @@ -129,7 +127,29 @@ class BlockchainTest(BitcoinTestFramework): # should have exact keys assert_equal(sorted(res.keys()), keys) - self.restart_node(0, ['-stopatheight=207', '-prune=550']) + self.stop_node(0) + self.nodes[0].assert_start_raises_init_error( + extra_args=['-testactivationheight=name@2'], + expected_msg='Error: Invalid name (name@2) for -testactivationheight=name@height.', + ) + self.nodes[0].assert_start_raises_init_error( + extra_args=['-testactivationheight=bip34@-2'], + expected_msg='Error: Invalid height value (bip34@-2) for -testactivationheight=name@height.', + ) + self.nodes[0].assert_start_raises_init_error( + extra_args=['-testactivationheight='], + expected_msg='Error: Invalid format () for -testactivationheight=name@height.', + ) + self.start_node(0, extra_args=[ + '-stopatheight=207', + '-prune=550', + '-testactivationheight=bip34@2', + '-testactivationheight=dersig@3', + '-testactivationheight=cltv@4', + '-testactivationheight=csv@5', + '-testactivationheight=segwit@6', + ]) + res = self.nodes[0].getblockchaininfo() # result should have these additional pruning keys if prune=550 assert_equal(sorted(res.keys()), sorted(['pruneheight', 'automatic_pruning', 'prune_target_size'] + keys)) @@ -143,10 +163,10 @@ class BlockchainTest(BitcoinTestFramework): assert_equal(res['softforks'], { 'bip34': {'type': 'buried', 'active': True, 'height': 2}, - 'bip66': {'type': 'buried', 'active': True, 'height': DERSIG_HEIGHT}, - 'bip65': {'type': 'buried', 'active': True, 'height': CLTV_HEIGHT}, - 'csv': {'type': 'buried', 'active': False, 'height': 432}, - 'segwit': {'type': 'buried', 'active': True, 'height': 0}, + 'bip66': {'type': 'buried', 'active': True, 'height': 3}, + 'bip65': {'type': 'buried', 'active': True, 'height': 4}, + 'csv': {'type': 'buried', 'active': True, 'height': 5}, + 'segwit': {'type': 'buried', 'active': True, 'height': 6}, 'testdummy': { 'type': 'bip9', 'bip9': { @@ -406,7 +426,7 @@ class BlockchainTest(BitcoinTestFramework): node = self.nodes[0] miniwallet = MiniWallet(node) - miniwallet.scan_blocks(num=5) + miniwallet.rescan_utxos() fee_per_byte = Decimal('0.00000010') fee_per_kb = 1000 * fee_per_byte @@ -414,17 +434,55 @@ class BlockchainTest(BitcoinTestFramework): miniwallet.send_self_transfer(fee_rate=fee_per_kb, from_node=node) blockhash = self.generate(node, 1)[0] - self.log.info("Test getblock with verbosity 1 doesn't include fee") - block = node.getblock(blockhash, 1) - assert 'fee' not in block['tx'][1] - - self.log.info('Test getblock with verbosity 2 includes expected fee') - block = node.getblock(blockhash, 2) - tx = block['tx'][1] - assert 'fee' in tx - assert_equal(tx['fee'], tx['vsize'] * fee_per_byte) - - self.log.info("Test getblock with verbosity 2 still works with pruned Undo data") + def assert_fee_not_in_block(verbosity): + block = node.getblock(blockhash, verbosity) + assert 'fee' not in block['tx'][1] + + def assert_fee_in_block(verbosity): + block = node.getblock(blockhash, verbosity) + tx = block['tx'][1] + assert 'fee' in tx + assert_equal(tx['fee'], tx['vsize'] * fee_per_byte) + + def assert_vin_contains_prevout(verbosity): + block = node.getblock(blockhash, verbosity) + tx = block["tx"][1] + total_vin = Decimal("0.00000000") + total_vout = Decimal("0.00000000") + for vin in tx["vin"]: + assert "prevout" in vin + assert_equal(set(vin["prevout"].keys()), set(("value", "height", "generated", "scriptPubKey"))) + assert_equal(vin["prevout"]["generated"], True) + total_vin += vin["prevout"]["value"] + for vout in tx["vout"]: + total_vout += vout["value"] + assert_equal(total_vin, total_vout + tx["fee"]) + + def assert_vin_does_not_contain_prevout(verbosity): + block = node.getblock(blockhash, verbosity) + tx = block["tx"][1] + if isinstance(tx, str): + # In verbosity level 1, only the transaction hashes are written + pass + else: + for vin in tx["vin"]: + assert "prevout" not in vin + + self.log.info("Test that getblock with verbosity 1 doesn't include fee") + assert_fee_not_in_block(1) + + self.log.info('Test that getblock with verbosity 2 and 3 includes expected fee') + assert_fee_in_block(2) + assert_fee_in_block(3) + + self.log.info("Test that getblock with verbosity 1 and 2 does not include prevout") + assert_vin_does_not_contain_prevout(1) + assert_vin_does_not_contain_prevout(2) + + self.log.info("Test that getblock with verbosity 3 includes prevout") + assert_vin_contains_prevout(3) + + self.log.info("Test that getblock with verbosity 2 and 3 still works with pruned Undo data") datadir = get_datadir_path(self.options.tmpdir, 0) self.log.info("Test getblock with invalid verbosity type returns proper error message") @@ -438,8 +496,10 @@ class BlockchainTest(BitcoinTestFramework): # Move instead of deleting so we can restore chain state afterwards move_block_file('rev00000.dat', 'rev_wrong') - block = node.getblock(blockhash, 2) - assert 'fee' not in block['tx'][1] + assert_fee_not_in_block(2) + assert_fee_not_in_block(3) + assert_vin_does_not_contain_prevout(2) + assert_vin_does_not_contain_prevout(3) # Restore chain state move_block_file('rev_wrong', 'rev00000.dat') diff --git a/test/functional/rpc_fundrawtransaction.py b/test/functional/rpc_fundrawtransaction.py index 56312dc6e5..b0e46c6ca7 100755 --- a/test/functional/rpc_fundrawtransaction.py +++ b/test/functional/rpc_fundrawtransaction.py @@ -8,6 +8,7 @@ from decimal import Decimal from itertools import product from test_framework.descriptors import descsum_create +from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_approx, @@ -19,6 +20,7 @@ from test_framework.util import ( count_bytes, find_vout_for_address, ) +from test_framework.wallet_util import bytes_to_wif def get_unspent(listunspent, amount): @@ -47,7 +49,40 @@ class RawTransactionsTest(BitcoinTestFramework): self.connect_nodes(0, 2) self.connect_nodes(0, 3) + def lock_outputs_type(self, wallet, outputtype): + """ + Only allow UTXOs of the given type + """ + if outputtype in ["legacy", "p2pkh", "pkh"]: + prefixes = ["pkh(", "sh(multi("] + elif outputtype in ["p2sh-segwit", "sh_wpkh"]: + prefixes = ["sh(wpkh(", "sh(wsh("] + elif outputtype in ["bech32", "wpkh"]: + prefixes = ["wpkh(", "wsh("] + else: + assert False, f"Unknown output type {outputtype}" + + to_lock = [] + for utxo in wallet.listunspent(): + if "desc" in utxo: + for prefix in prefixes: + if utxo["desc"].startswith(prefix): + to_lock.append({"txid": utxo["txid"], "vout": utxo["vout"]}) + wallet.lockunspent(False, to_lock) + + def unlock_utxos(self, wallet): + """ + Unlock all UTXOs except the watchonly one + """ + to_keep = [] + if self.watchonly_txid is not None and self.watchonly_vout is not None: + to_keep.append({"txid": self.watchonly_txid, "vout": self.watchonly_vout}) + wallet.lockunspent(True) + wallet.lockunspent(False, to_keep) + def run_test(self): + self.watchonly_txid = None + self.watchonly_vout = None self.log.info("Connect nodes, set fees, generate blocks, and sync") self.min_relay_tx_fee = self.nodes[0].getnetworkinfo()['relayfee'] # This test is not meant to test fee estimation and we'd like @@ -99,6 +134,7 @@ class RawTransactionsTest(BitcoinTestFramework): self.test_subtract_fee_with_presets() self.test_transaction_too_large() self.test_include_unsafe() + self.test_external_inputs() self.test_22670() def test_change_position(self): @@ -373,6 +409,7 @@ class RawTransactionsTest(BitcoinTestFramework): def test_fee_p2pkh(self): """Compare fee of a standard pubkeyhash transaction.""" self.log.info("Test fundrawtxn p2pkh fee") + self.lock_outputs_type(self.nodes[0], "p2pkh") inputs = [] outputs = {self.nodes[1].getnewaddress():1.1} rawtx = self.nodes[0].createrawtransaction(inputs, outputs) @@ -386,9 +423,12 @@ class RawTransactionsTest(BitcoinTestFramework): feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) assert feeDelta >= 0 and feeDelta <= self.fee_tolerance + self.unlock_utxos(self.nodes[0]) + def test_fee_p2pkh_multi_out(self): """Compare fee of a standard pubkeyhash transaction with multiple outputs.""" self.log.info("Test fundrawtxn p2pkh fee with multiple outputs") + self.lock_outputs_type(self.nodes[0], "p2pkh") inputs = [] outputs = { self.nodes[1].getnewaddress():1.1, @@ -409,8 +449,11 @@ class RawTransactionsTest(BitcoinTestFramework): feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) assert feeDelta >= 0 and feeDelta <= self.fee_tolerance + self.unlock_utxos(self.nodes[0]) + def test_fee_p2sh(self): """Compare fee of a 2-of-2 multisig p2sh transaction.""" + self.lock_outputs_type(self.nodes[0], "p2pkh") # Create 2-of-2 addr. addr1 = self.nodes[1].getnewaddress() addr2 = self.nodes[1].getnewaddress() @@ -433,9 +476,12 @@ class RawTransactionsTest(BitcoinTestFramework): feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) assert feeDelta >= 0 and feeDelta <= self.fee_tolerance + self.unlock_utxos(self.nodes[0]) + def test_fee_4of5(self): """Compare fee of a standard pubkeyhash transaction.""" self.log.info("Test fundrawtxn fee with 4-of-5 addresses") + self.lock_outputs_type(self.nodes[0], "p2pkh") # Create 4-of-5 addr. addr1 = self.nodes[1].getnewaddress() @@ -474,6 +520,8 @@ class RawTransactionsTest(BitcoinTestFramework): feeDelta = Decimal(fundedTx['fee']) - Decimal(signedFee) assert feeDelta >= 0 and feeDelta <= self.fee_tolerance + self.unlock_utxos(self.nodes[0]) + def test_spend_2of2(self): """Spend a 2-of-2 multisig transaction over fundraw.""" self.log.info("Test fundpsbt spending 2-of-2 multisig") @@ -542,15 +590,18 @@ class RawTransactionsTest(BitcoinTestFramework): # Drain the keypool. self.nodes[1].getnewaddress() self.nodes[1].getrawchangeaddress() - inputs = [] - outputs = {self.nodes[0].getnewaddress():1.19999500} + + # Choose 2 inputs + inputs = self.nodes[1].listunspent()[0:2] + value = sum(inp["amount"] for inp in inputs) - Decimal("0.00000500") # Pay a 500 sat fee + outputs = {self.nodes[0].getnewaddress():value} rawtx = self.nodes[1].createrawtransaction(inputs, outputs) # fund a transaction that does not require a new key for the change output self.nodes[1].fundrawtransaction(rawtx) # fund a transaction that requires a new key for the change output # creating the key must be impossible because the wallet is locked - outputs = {self.nodes[0].getnewaddress():1.1} + outputs = {self.nodes[0].getnewaddress():value - Decimal("0.1")} rawtx = self.nodes[1].createrawtransaction(inputs, outputs) assert_raises_rpc_error(-4, "Transaction needs a change address, but we can't generate it.", self.nodes[1].fundrawtransaction, rawtx) @@ -935,6 +986,57 @@ class RawTransactionsTest(BitcoinTestFramework): wallet.sendmany("", outputs) self.generate(self.nodes[0], 10) assert_raises_rpc_error(-4, "Transaction too large", recipient.fundrawtransaction, rawtx) + self.nodes[0].unloadwallet("large") + + def test_external_inputs(self): + self.log.info("Test funding with external inputs") + + eckey = ECKey() + eckey.generate() + privkey = bytes_to_wif(eckey.get_bytes()) + + self.nodes[2].createwallet("extfund") + wallet = self.nodes[2].get_wallet_rpc("extfund") + + # Make a weird but signable script. sh(pkh()) descriptor accomplishes this + desc = descsum_create("sh(pkh({}))".format(privkey)) + if self.options.descriptors: + res = self.nodes[0].importdescriptors([{"desc": desc, "timestamp": "now"}]) + else: + res = self.nodes[0].importmulti([{"desc": desc, "timestamp": "now"}]) + assert res[0]["success"] + addr = self.nodes[0].deriveaddresses(desc)[0] + addr_info = self.nodes[0].getaddressinfo(addr) + + self.nodes[0].sendtoaddress(addr, 10) + self.nodes[0].sendtoaddress(wallet.getnewaddress(), 10) + self.generate(self.nodes[0], 6) + self.sync_all() + ext_utxo = self.nodes[0].listunspent(addresses=[addr])[0] + + # An external input without solving data should result in an error + raw_tx = wallet.createrawtransaction([ext_utxo], {self.nodes[0].getnewaddress(): 15}) + assert_raises_rpc_error(-4, "Insufficient funds", wallet.fundrawtransaction, raw_tx) + + # Error conditions + assert_raises_rpc_error(-5, "'not a pubkey' is not hex", wallet.fundrawtransaction, raw_tx, {"solving_data": {"pubkeys":["not a pubkey"]}}) + assert_raises_rpc_error(-5, "'01234567890a0b0c0d0e0f' is not a valid public key", wallet.fundrawtransaction, raw_tx, {"solving_data": {"pubkeys":["01234567890a0b0c0d0e0f"]}}) + assert_raises_rpc_error(-5, "'not a script' is not hex", wallet.fundrawtransaction, raw_tx, {"solving_data": {"scripts":["not a script"]}}) + assert_raises_rpc_error(-8, "Unable to parse descriptor 'not a descriptor'", wallet.fundrawtransaction, raw_tx, {"solving_data": {"descriptors":["not a descriptor"]}}) + + # But funding should work when the solving data is provided + funded_tx = wallet.fundrawtransaction(raw_tx, {"solving_data": {"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]}}) + signed_tx = wallet.signrawtransactionwithwallet(funded_tx['hex']) + assert not signed_tx['complete'] + signed_tx = self.nodes[0].signrawtransactionwithwallet(signed_tx['hex']) + assert signed_tx['complete'] + + funded_tx = wallet.fundrawtransaction(raw_tx, {"solving_data": {"descriptors": [desc]}}) + signed_tx = wallet.signrawtransactionwithwallet(funded_tx['hex']) + assert not signed_tx['complete'] + signed_tx = self.nodes[0].signrawtransactionwithwallet(signed_tx['hex']) + assert signed_tx['complete'] + self.nodes[2].unloadwallet("extfund") def test_include_unsafe(self): self.log.info("Test fundrawtxn with unsafe inputs") @@ -944,31 +1046,32 @@ class RawTransactionsTest(BitcoinTestFramework): # We receive unconfirmed funds from external keys (unsafe outputs). addr = wallet.getnewaddress() - txid1 = self.nodes[2].sendtoaddress(addr, 6) - txid2 = self.nodes[2].sendtoaddress(addr, 4) - self.sync_all() - vout1 = find_vout_for_address(wallet, txid1, addr) - vout2 = find_vout_for_address(wallet, txid2, addr) + inputs = [] + for i in range(0, 2): + txid = self.nodes[2].sendtoaddress(addr, 5) + self.sync_mempools() + vout = find_vout_for_address(wallet, txid, addr) + inputs.append((txid, vout)) # Unsafe inputs are ignored by default. - rawtx = wallet.createrawtransaction([], [{self.nodes[2].getnewaddress(): 5}]) + rawtx = wallet.createrawtransaction([], [{self.nodes[2].getnewaddress(): 7.5}]) assert_raises_rpc_error(-4, "Insufficient funds", wallet.fundrawtransaction, rawtx) # But we can opt-in to use them for funding. fundedtx = wallet.fundrawtransaction(rawtx, {"include_unsafe": True}) tx_dec = wallet.decoderawtransaction(fundedtx['hex']) - assert any([txin['txid'] == txid1 and txin['vout'] == vout1 for txin in tx_dec['vin']]) + assert all((txin["txid"], txin["vout"]) in inputs for txin in tx_dec["vin"]) signedtx = wallet.signrawtransactionwithwallet(fundedtx['hex']) - wallet.sendrawtransaction(signedtx['hex']) + assert wallet.testmempoolaccept([signedtx['hex']])[0]["allowed"] # And we can also use them once they're confirmed. self.generate(self.nodes[0], 1) - rawtx = wallet.createrawtransaction([], [{self.nodes[2].getnewaddress(): 3}]) - fundedtx = wallet.fundrawtransaction(rawtx, {"include_unsafe": True}) + fundedtx = wallet.fundrawtransaction(rawtx, {"include_unsafe": False}) tx_dec = wallet.decoderawtransaction(fundedtx['hex']) - assert any([txin['txid'] == txid2 and txin['vout'] == vout2 for txin in tx_dec['vin']]) + assert all((txin["txid"], txin["vout"]) in inputs for txin in tx_dec["vin"]) signedtx = wallet.signrawtransactionwithwallet(fundedtx['hex']) - wallet.sendrawtransaction(signedtx['hex']) + assert wallet.testmempoolaccept([signedtx['hex']])[0]["allowed"] + self.nodes[0].unloadwallet("unsafe") def test_22670(self): # In issue #22670, it was observed that ApproximateBestSubset may diff --git a/test/functional/rpc_invalid_address_message.py b/test/functional/rpc_invalid_address_message.py index e362642f0f..085f6582b5 100755 --- a/test/functional/rpc_invalid_address_message.py +++ b/test/functional/rpc_invalid_address_message.py @@ -29,9 +29,6 @@ class InvalidAddressErrorMessageTest(BitcoinTestFramework): self.setup_clean_chain = True self.num_nodes = 1 - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() - def test_validateaddress(self): node = self.nodes[0] @@ -60,6 +57,10 @@ class InvalidAddressErrorMessageTest(BitcoinTestFramework): assert info['isvalid'] assert 'error' not in info + info = node.validateaddress(BECH32_INVALID_VERSION) + assert not info['isvalid'] + assert_equal(info['error'], 'Invalid Bech32 address witness version') + # Base58 info = node.validateaddress(BASE58_INVALID_PREFIX) assert not info['isvalid'] @@ -87,7 +88,10 @@ class InvalidAddressErrorMessageTest(BitcoinTestFramework): def run_test(self): self.test_validateaddress() - self.test_getaddressinfo() + + if self.is_wallet_compiled(): + self.init_wallet(node=0) + self.test_getaddressinfo() if __name__ == '__main__': diff --git a/test/functional/rpc_misc.py b/test/functional/rpc_misc.py index 13f33c321f..e32e562bce 100755 --- a/test/functional/rpc_misc.py +++ b/test/functional/rpc_misc.py @@ -57,7 +57,7 @@ class RpcMiscTest(BitcoinTestFramework): self.log.info("test logging rpc and help") # Test logging RPC returns the expected number of logging categories. - assert_equal(len(node.logging()), 25) + assert_equal(len(node.logging()), 27) # Test toggling a logging category on/off/on with the logging RPC. assert_equal(node.logging()['qt'], True) diff --git a/test/functional/rpc_net.py b/test/functional/rpc_net.py index aa53e354a3..0f3bbce54c 100755 --- a/test/functional/rpc_net.py +++ b/test/functional/rpc_net.py @@ -12,11 +12,10 @@ from itertools import product import time from test_framework.blocktools import COINBASE_MATURITY -from test_framework.p2p import P2PInterface import test_framework.messages -from test_framework.messages import ( - NODE_NETWORK, - NODE_WITNESS, +from test_framework.p2p import ( + P2PInterface, + P2P_SERVICES, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( @@ -189,7 +188,6 @@ class NetTest(BitcoinTestFramework): def test_getnodeaddresses(self): self.log.info("Test getnodeaddresses") self.nodes[0].add_p2p_connection(P2PInterface()) - services = NODE_NETWORK | NODE_WITNESS # Add an IPv6 address to the address manager. ipv6_addr = "1233:3432:2434:2343:3234:2345:6546:4534" @@ -217,7 +215,7 @@ class NetTest(BitcoinTestFramework): assert_greater_than(10000, len(node_addresses)) for a in node_addresses: assert_greater_than(a["time"], 1527811200) # 1st June 2018 - assert_equal(a["services"], services) + assert_equal(a["services"], P2P_SERVICES) assert a["address"] in imported_addrs assert_equal(a["port"], 8333) assert_equal(a["network"], "ipv4") @@ -228,7 +226,7 @@ class NetTest(BitcoinTestFramework): assert_equal(res[0]["address"], ipv6_addr) assert_equal(res[0]["network"], "ipv6") assert_equal(res[0]["port"], 8333) - assert_equal(res[0]["services"], services) + assert_equal(res[0]["services"], P2P_SERVICES) # Test for the absence of onion and I2P addresses. for network in ["onion", "i2p"]: @@ -239,7 +237,16 @@ class NetTest(BitcoinTestFramework): assert_raises_rpc_error(-8, "Network not recognized: Foo", self.nodes[0].getnodeaddresses, 1, "Foo") def test_addpeeraddress(self): + """RPC addpeeraddress sets the source address equal to the destination address. + If an address with the same /16 as an existing new entry is passed, it will be + placed in the same new bucket and have a 1/64 chance of the bucket positions + colliding (depending on the value of nKey in the addrman), in which case the + new address won't be added. The probability of collision can be reduced to + 1/2^16 = 1/65536 by using an address from a different /16. We avoid this here + by first testing adding a tried table entry before testing adding a new table one. + """ self.log.info("Test addpeeraddress") + self.restart_node(1, ["-checkaddrman=1"]) node = self.nodes[1] self.log.debug("Test that addpeerinfo is a hidden RPC") @@ -251,17 +258,25 @@ class NetTest(BitcoinTestFramework): assert_equal(node.addpeeraddress(address="", port=8333), {"success": False}) assert_equal(node.getnodeaddresses(count=0), []) - self.log.debug("Test that adding a valid address succeeds") - assert_equal(node.addpeeraddress(address="1.2.3.4", port=8333), {"success": True}) - addrs = node.getnodeaddresses(count=0) - assert_equal(len(addrs), 1) - assert_equal(addrs[0]["address"], "1.2.3.4") - assert_equal(addrs[0]["port"], 8333) - - self.log.debug("Test that adding the same address again when already present fails") - assert_equal(node.addpeeraddress(address="1.2.3.4", port=8333), {"success": False}) + self.log.debug("Test that adding a valid address to the tried table succeeds") + assert_equal(node.addpeeraddress(address="1.2.3.4", tried=True, port=8333), {"success": True}) + with node.assert_debug_log(expected_msgs=["Addrman checks started: new 0, tried 1, total 1"]): + addrs = node.getnodeaddresses(count=0) # getnodeaddresses re-runs the addrman checks + assert_equal(len(addrs), 1) + assert_equal(addrs[0]["address"], "1.2.3.4") + assert_equal(addrs[0]["port"], 8333) + + self.log.debug("Test that adding an already-present tried address to the new and tried tables fails") + for value in [True, False]: + assert_equal(node.addpeeraddress(address="1.2.3.4", tried=value, port=8333), {"success": False}) assert_equal(len(node.getnodeaddresses(count=0)), 1) + self.log.debug("Test that adding a second address, this time to the new table, succeeds") + assert_equal(node.addpeeraddress(address="2.0.0.0", port=8333), {"success": True}) + with node.assert_debug_log(expected_msgs=["Addrman checks started: new 1, tried 1, total 2"]): + addrs = node.getnodeaddresses(count=0) # getnodeaddresses re-runs the addrman checks + assert_equal(len(addrs), 2) + if __name__ == '__main__': NetTest().main() diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index 2b1892c121..b132ac3d31 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -8,6 +8,8 @@ from decimal import Decimal from itertools import product +from test_framework.descriptors import descsum_create +from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_approx, @@ -16,6 +18,7 @@ from test_framework.util import ( assert_raises_rpc_error, find_output, ) +from test_framework.wallet_util import bytes_to_wif import json import os @@ -108,6 +111,16 @@ class PSBTTest(BitcoinTestFramework): psbtx = self.nodes[1].walletprocesspsbt(psbtx1)['psbt'] assert_equal(psbtx1, psbtx) + # Node 0 should not be able to sign the transaction with the wallet is locked + self.nodes[0].encryptwallet("password") + assert_raises_rpc_error(-13, "Please enter the wallet passphrase with walletpassphrase first", self.nodes[0].walletprocesspsbt, psbtx) + + # Node 0 should be able to process without signing though + unsigned_tx = self.nodes[0].walletprocesspsbt(psbtx, False) + assert_equal(unsigned_tx['complete'], False) + + self.nodes[0].walletpassphrase(passphrase="password", timeout=1000000) + # Sign the transaction and send signed_tx = self.nodes[0].walletprocesspsbt(psbtx)['psbt'] final_tx = self.nodes[0].finalizepsbt(signed_tx)['hex'] @@ -598,5 +611,43 @@ class PSBTTest(BitcoinTestFramework): assert_raises_rpc_error(-25, 'Inputs missing or spent', self.nodes[0].walletprocesspsbt, 'cHNidP8BAJoCAAAAAkvEW8NnDtdNtDpsmze+Ht2LH35IJcKv00jKAlUs21RrAwAAAAD/////S8Rbw2cO1020OmybN74e3Ysffkglwq/TSMoCVSzbVGsBAAAAAP7///8CwLYClQAAAAAWABSNJKzjaUb3uOxixsvh1GGE3fW7zQD5ApUAAAAAFgAUKNw0x8HRctAgmvoevm4u1SbN7XIAAAAAAAEAnQIAAAACczMa321tVHuN4GKWKRncycI22aX3uXgwSFUKM2orjRsBAAAAAP7///9zMxrfbW1Ue43gYpYpGdzJwjbZpfe5eDBIVQozaiuNGwAAAAAA/v///wIA+QKVAAAAABl2qRT9zXUVA8Ls5iVqynLHe5/vSe1XyYisQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAAAAAQEfQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAA==') + # Test that we can fund psbts with external inputs specified + eckey = ECKey() + eckey.generate() + privkey = bytes_to_wif(eckey.get_bytes()) + + # Make a weird but signable script. sh(pkh()) descriptor accomplishes this + desc = descsum_create("sh(pkh({}))".format(privkey)) + if self.options.descriptors: + res = self.nodes[0].importdescriptors([{"desc": desc, "timestamp": "now"}]) + else: + res = self.nodes[0].importmulti([{"desc": desc, "timestamp": "now"}]) + assert res[0]["success"] + addr = self.nodes[0].deriveaddresses(desc)[0] + addr_info = self.nodes[0].getaddressinfo(addr) + + self.nodes[0].sendtoaddress(addr, 10) + self.generate(self.nodes[0], 6) + self.sync_all() + ext_utxo = self.nodes[0].listunspent(addresses=[addr])[0] + + # An external input without solving data should result in an error + assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[1].walletcreatefundedpsbt, [ext_utxo], {self.nodes[0].getnewaddress(): 10 + ext_utxo['amount']}, 0, {'add_inputs': True}) + + # But funding should work when the solving data is provided + psbt = self.nodes[1].walletcreatefundedpsbt([ext_utxo], {self.nodes[0].getnewaddress(): 15}, 0, {'add_inputs': True, "solving_data": {"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]}}) + signed = self.nodes[1].walletprocesspsbt(psbt['psbt']) + assert not signed['complete'] + signed = self.nodes[0].walletprocesspsbt(signed['psbt']) + assert signed['complete'] + self.nodes[0].finalizepsbt(signed['psbt']) + + psbt = self.nodes[1].walletcreatefundedpsbt([ext_utxo], {self.nodes[0].getnewaddress(): 15}, 0, {'add_inputs': True, "solving_data":{"descriptors": [desc]}}) + signed = self.nodes[1].walletprocesspsbt(psbt['psbt']) + assert not signed['complete'] + signed = self.nodes[0].walletprocesspsbt(signed['psbt']) + assert signed['complete'] + self.nodes[0].finalizepsbt(signed['psbt']) + if __name__ == '__main__': PSBTTest().main() diff --git a/test/functional/rpc_signer.py b/test/functional/rpc_signer.py index 9e963eba57..5c3722ef8f 100755 --- a/test/functional/rpc_signer.py +++ b/test/functional/rpc_signer.py @@ -27,6 +27,9 @@ class RPCSignerTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 4 + # The experimental syscall sandbox feature (-sandbox) is not compatible with -signer (which + # invokes execve). + self.disable_syscall_sandbox = True self.extra_args = [ [], diff --git a/test/functional/rpc_signrawtransaction.py b/test/functional/rpc_signrawtransaction.py index 8f17b29ff4..c519d0c7d1 100755 --- a/test/functional/rpc_signrawtransaction.py +++ b/test/functional/rpc_signrawtransaction.py @@ -6,7 +6,6 @@ from test_framework.blocktools import ( COINBASE_MATURITY, - CSV_ACTIVATION_HEIGHT, ) from test_framework.address import ( script_to_p2sh, @@ -18,7 +17,6 @@ from test_framework.util import ( assert_equal, assert_raises_rpc_error, find_vout_for_address, - generate_to_height, ) from test_framework.messages import ( CTxInWitness, @@ -27,12 +25,12 @@ from test_framework.messages import ( from test_framework.script import ( CScript, OP_CHECKLOCKTIMEVERIFY, - OP_CHECKSIG, OP_CHECKSEQUENCEVERIFY, OP_DROP, OP_TRUE, ) from test_framework.script_util import ( + key_to_p2pk_script, key_to_p2pkh_script, script_to_p2sh_p2wsh_script, script_to_p2wsh_script, @@ -231,7 +229,7 @@ class SignRawTransactionsTest(BitcoinTestFramework): embedded_pubkey = eckey.get_pubkey().get_bytes().hex() witness_script = { 'P2PKH': key_to_p2pkh_script(embedded_pubkey).hex(), - 'P2PK': CScript([bytes.fromhex(embedded_pubkey), OP_CHECKSIG]).hex() + 'P2PK': key_to_p2pk_script(embedded_pubkey).hex() }.get(tx_type, "Invalid tx_type") redeem_script = script_to_p2wsh_script(witness_script).hex() addr = script_to_p2sh(redeem_script) @@ -273,7 +271,6 @@ class SignRawTransactionsTest(BitcoinTestFramework): getcontext().prec = 8 # Make sure CSV is active - generate_to_height(self, self.nodes[0], CSV_ACTIVATION_HEIGHT) assert self.nodes[0].getblockchaininfo()['softforks']['csv']['active'] # Create a P2WSH script with CSV diff --git a/test/functional/test_framework/authproxy.py b/test/functional/test_framework/authproxy.py index 81eb881234..c4ffd1fbf6 100644 --- a/test/functional/test_framework/authproxy.py +++ b/test/functional/test_framework/authproxy.py @@ -113,10 +113,8 @@ class AuthServiceProxy(): self.__conn.request(method, path, postdata, headers) return self._get_response() except OSError as e: - retry = ( - '[WinError 10053] An established connection was aborted by the software in your host machine' in str(e)) # Workaround for a bug on macOS. See https://bugs.python.org/issue33450 - retry = retry or ('[Errno 41] Protocol wrong type for socket' in str(e)) + retry = '[Errno 41] Protocol wrong type for socket' in str(e) if retry: self.__conn.close() self.__conn.request(method, path, postdata, headers) diff --git a/test/functional/test_framework/blocktools.py b/test/functional/test_framework/blocktools.py index 25b36b6a91..85e3c2a383 100644 --- a/test/functional/test_framework/blocktools.py +++ b/test/functional/test_framework/blocktools.py @@ -33,11 +33,11 @@ from .script import ( CScriptOp, OP_1, OP_CHECKMULTISIG, - OP_CHECKSIG, OP_RETURN, OP_TRUE, ) from .script_util import ( + key_to_p2pk_script, key_to_p2wpkh_script, script_to_p2wsh_script, ) @@ -53,11 +53,6 @@ TIME_GENESIS_BLOCK = 1296688602 # Coinbase transaction outputs can only be spent after this number of new blocks (network rule) COINBASE_MATURITY = 100 -# Soft-fork activation heights -DERSIG_HEIGHT = 102 # BIP 66 -CLTV_HEIGHT = 111 # BIP 65 -CSV_ACTIVATION_HEIGHT = 432 - # From BIP141 WITNESS_COMMITMENT_HEADER = b"\xaa\x21\xa9\xed" @@ -139,7 +134,7 @@ def create_coinbase(height, pubkey=None, extra_output_script=None, fees=0, nValu coinbaseoutput.nValue >>= halvings coinbaseoutput.nValue += fees if pubkey is not None: - coinbaseoutput.scriptPubKey = CScript([pubkey, OP_CHECKSIG]) + coinbaseoutput.scriptPubKey = key_to_p2pk_script(pubkey) else: coinbaseoutput.scriptPubKey = CScript([OP_TRUE]) coinbase.vout = [coinbaseoutput] diff --git a/test/functional/test_framework/p2p.py b/test/functional/test_framework/p2p.py index b7d5bd8fab..78c63b57a1 100755 --- a/test/functional/test_framework/p2p.py +++ b/test/functional/test_framework/p2p.py @@ -356,7 +356,7 @@ class P2PInterface(P2PConnection): return create_conn - def peer_accept_connection(self, *args, services=NODE_NETWORK | NODE_WITNESS, **kwargs): + def peer_accept_connection(self, *args, services=P2P_SERVICES, **kwargs): create_conn = super().peer_accept_connection(*args, **kwargs) self.peer_connect_send_version(services) @@ -577,6 +577,8 @@ class NetworkThread(threading.Thread): NetworkThread.listeners = {} NetworkThread.protos = {} + if sys.platform == 'win32': + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) NetworkThread.network_event_loop = asyncio.new_event_loop() def run(self): diff --git a/test/functional/test_framework/script_util.py b/test/functional/test_framework/script_util.py index 5d1d7ea45c..82a9067dd2 100755 --- a/test/functional/test_framework/script_util.py +++ b/test/functional/test_framework/script_util.py @@ -3,7 +3,17 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Useful Script constants and utils.""" -from test_framework.script import CScript, hash160, sha256, OP_0, OP_DUP, OP_HASH160, OP_CHECKSIG, OP_EQUAL, OP_EQUALVERIFY +from test_framework.script import ( + CScript, + OP_0, + OP_CHECKSIG, + OP_DUP, + OP_EQUAL, + OP_EQUALVERIFY, + OP_HASH160, + hash160, + sha256, +) # To prevent a "tx-size-small" policy rule error, a transaction has to have a # non-witness size of at least 82 bytes (MIN_STANDARD_TX_NONWITNESS_SIZE in @@ -25,28 +35,39 @@ from test_framework.script import CScript, hash160, sha256, OP_0, OP_DUP, OP_HAS DUMMY_P2WPKH_SCRIPT = CScript([b'a' * 21]) DUMMY_2_P2WPKH_SCRIPT = CScript([b'b' * 21]) -def keyhash_to_p2pkh_script(hash, main = False): + +def key_to_p2pk_script(key): + key = check_key(key) + return CScript([key, OP_CHECKSIG]) + + +def keyhash_to_p2pkh_script(hash): assert len(hash) == 20 return CScript([OP_DUP, OP_HASH160, hash, OP_EQUALVERIFY, OP_CHECKSIG]) -def scripthash_to_p2sh_script(hash, main = False): + +def scripthash_to_p2sh_script(hash): assert len(hash) == 20 return CScript([OP_HASH160, hash, OP_EQUAL]) -def key_to_p2pkh_script(key, main = False): + +def key_to_p2pkh_script(key): key = check_key(key) - return keyhash_to_p2pkh_script(hash160(key), main) + return keyhash_to_p2pkh_script(hash160(key)) + -def script_to_p2sh_script(script, main = False): +def script_to_p2sh_script(script): script = check_script(script) - return scripthash_to_p2sh_script(hash160(script), main) + return scripthash_to_p2sh_script(hash160(script)) -def key_to_p2sh_p2wpkh_script(key, main = False): + +def key_to_p2sh_p2wpkh_script(key): key = check_key(key) p2shscript = CScript([OP_0, hash160(key)]) - return script_to_p2sh_script(p2shscript, main) + return script_to_p2sh_script(p2shscript) + -def program_to_witness_script(version, program, main = False): +def program_to_witness_script(version, program): if isinstance(program, str): program = bytes.fromhex(program) assert 0 <= version <= 16 @@ -54,29 +75,34 @@ def program_to_witness_script(version, program, main = False): assert version > 0 or len(program) in [20, 32] return CScript([version, program]) -def script_to_p2wsh_script(script, main = False): + +def script_to_p2wsh_script(script): script = check_script(script) - return program_to_witness_script(0, sha256(script), main) + return program_to_witness_script(0, sha256(script)) + -def key_to_p2wpkh_script(key, main = False): +def key_to_p2wpkh_script(key): key = check_key(key) - return program_to_witness_script(0, hash160(key), main) + return program_to_witness_script(0, hash160(key)) -def script_to_p2sh_p2wsh_script(script, main = False): + +def script_to_p2sh_p2wsh_script(script): script = check_script(script) p2shscript = CScript([OP_0, sha256(script)]) - return script_to_p2sh_script(p2shscript, main) + return script_to_p2sh_script(p2shscript) + def check_key(key): if isinstance(key, str): - key = bytes.fromhex(key) # Assuming this is hex string + key = bytes.fromhex(key) # Assuming this is hex string if isinstance(key, bytes) and (len(key) == 33 or len(key) == 65): return key assert False + def check_script(script): if isinstance(script, str): - script = bytes.fromhex(script) # Assuming this is hex string + script = bytes.fromhex(script) # Assuming this is hex string if isinstance(script, bytes) or isinstance(script, CScript): return script assert False diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index f382e0fdb3..ec3561b1f2 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -101,6 +101,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): self.supports_cli = True self.bind_to_localhost_only = True self.parse_args() + self.disable_syscall_sandbox = self.options.nosandbox self.default_wallet_name = "default_wallet" if self.options.descriptors else "" self.wallet_data_filename = "wallet.dat" # Optional list of wallet names that can be set in set_test_params to @@ -159,6 +160,8 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): parser = argparse.ArgumentParser(usage="%(prog)s [options]") parser.add_argument("--nocleanup", dest="nocleanup", default=False, action="store_true", help="Leave bitcoinds and test.* datadir on exit or error") + parser.add_argument("--nosandbox", dest="nosandbox", default=False, action="store_true", + help="Don't use the syscall sandbox") parser.add_argument("--noshutdown", dest="noshutdown", default=False, action="store_true", help="Don't stop bitcoinds after the test execution") parser.add_argument("--cachedir", dest="cachedir", default=os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/../../cache"), @@ -420,12 +423,12 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): def import_deterministic_coinbase_privkeys(self): for i in range(self.num_nodes): - self.init_wallet(i) + self.init_wallet(node=i) - def init_wallet(self, i): - wallet_name = self.default_wallet_name if self.wallet_names is None else self.wallet_names[i] if i < len(self.wallet_names) else False + def init_wallet(self, *, node): + wallet_name = self.default_wallet_name if self.wallet_names is None else self.wallet_names[node] if node < len(self.wallet_names) else False if wallet_name is not False: - n = self.nodes[i] + n = self.nodes[node] if wallet_name is not None: n.createwallet(wallet_name=wallet_name, descriptors=self.options.descriptors, load_on_startup=True) n.importprivkey(privkey=n.get_deterministic_priv_key().key, label='coinbase') @@ -468,6 +471,10 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): extra_args = [[]] * num_nodes if versions is None: versions = [None] * num_nodes + if self.is_syscall_sandbox_compiled() and not self.disable_syscall_sandbox: + for i in range(len(extra_args)): + if versions[i] is None or versions[i] >= 219900: + extra_args[i] = extra_args[i] + ["-sandbox=log-and-abort"] if binary is None: binary = [get_bin_from_version(v, 'bitcoind', self.options.bitcoind) for v in versions] if binary_cli is None: @@ -560,18 +567,19 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): self.nodes[i].process.wait(timeout) def connect_nodes(self, a, b): - def connect_nodes_helper(from_connection, node_num): - ip_port = "127.0.0.1:" + str(p2p_port(node_num)) - from_connection.addnode(ip_port, "onetry") - # poll until version handshake complete to avoid race conditions - # with transaction relaying - # See comments in net_processing: - # * Must have a version message before anything else - # * Must have a verack message before anything else - wait_until_helper(lambda: all(peer['version'] != 0 for peer in from_connection.getpeerinfo())) - wait_until_helper(lambda: all(peer['bytesrecv_per_msg'].pop('verack', 0) == 24 for peer in from_connection.getpeerinfo())) - - connect_nodes_helper(self.nodes[a], b) + from_connection = self.nodes[a] + to_connection = self.nodes[b] + ip_port = "127.0.0.1:" + str(p2p_port(b)) + from_connection.addnode(ip_port, "onetry") + # poll until version handshake complete to avoid race conditions + # with transaction relaying + # See comments in net_processing: + # * Must have a version message before anything else + # * Must have a verack message before anything else + wait_until_helper(lambda: all(peer['version'] != 0 for peer in from_connection.getpeerinfo())) + wait_until_helper(lambda: all(peer['version'] != 0 for peer in to_connection.getpeerinfo())) + wait_until_helper(lambda: all(peer['bytesrecv_per_msg'].pop('verack', 0) == 24 for peer in from_connection.getpeerinfo())) + wait_until_helper(lambda: all(peer['bytesrecv_per_msg'].pop('verack', 0) == 24 for peer in to_connection.getpeerinfo())) def disconnect_nodes(self, a, b): def disconnect_nodes_helper(from_connection, node_num): @@ -620,19 +628,19 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): self.sync_all() def generate(self, generator, *args, **kwargs): - blocks = generator.generate(*args, **kwargs) + blocks = generator.generate(*args, invalid_call=False, **kwargs) return blocks def generateblock(self, generator, *args, **kwargs): - blocks = generator.generateblock(*args, **kwargs) + blocks = generator.generateblock(*args, invalid_call=False, **kwargs) return blocks def generatetoaddress(self, generator, *args, **kwargs): - blocks = generator.generatetoaddress(*args, **kwargs) + blocks = generator.generatetoaddress(*args, invalid_call=False, **kwargs) return blocks def generatetodescriptor(self, generator, *args, **kwargs): - blocks = generator.generatetodescriptor(*args, **kwargs) + blocks = generator.generatetodescriptor(*args, invalid_call=False, **kwargs) return blocks def sync_blocks(self, nodes=None, wait=1, timeout=60): @@ -886,3 +894,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): def is_bdb_compiled(self): """Checks whether the wallet module was compiled with BDB support.""" return self.config["components"].getboolean("USE_BDB") + + def is_syscall_sandbox_compiled(self): + """Checks whether the syscall sandbox was compiled.""" + return self.config["components"].getboolean("ENABLE_SYSCALL_SANDBOX") diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index f9e2cfa2f5..e8ff41a46d 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -297,9 +297,21 @@ class TestNode(): time.sleep(1.0 / poll_per_s) self._raise_assertion_error("Unable to retrieve cookie credentials after {}s".format(self.rpc_timeout)) - def generate(self, nblocks, maxtries=1000000): + def generate(self, nblocks, maxtries=1000000, **kwargs): self.log.debug("TestNode.generate() dispatches `generate` call to `generatetoaddress`") - return self.generatetoaddress(nblocks=nblocks, address=self.get_deterministic_priv_key().address, maxtries=maxtries) + return self.generatetoaddress(nblocks=nblocks, address=self.get_deterministic_priv_key().address, maxtries=maxtries, **kwargs) + + def generateblock(self, *args, invalid_call, **kwargs): + assert not invalid_call + return self.__getattr__('generateblock')(*args, **kwargs) + + def generatetoaddress(self, *args, invalid_call, **kwargs): + assert not invalid_call + return self.__getattr__('generatetoaddress')(*args, **kwargs) + + def generatetodescriptor(self, *args, invalid_call, **kwargs): + assert not invalid_call + return self.__getattr__('generatetodescriptor')(*args, **kwargs) def get_wallet_rpc(self, wallet_name): if self.use_cli: diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index d66499dbcb..9f5bca6884 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -34,13 +34,14 @@ def assert_approx(v, vexp, vspan=0.00001): raise AssertionError("%s > [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan))) -def assert_fee_amount(fee, tx_size, fee_per_kB): - """Assert the fee was in range""" - target_fee = round(tx_size * fee_per_kB / 1000, 8) +def assert_fee_amount(fee, tx_size, feerate_BTC_kvB): + """Assert the fee is in range.""" + feerate_BTC_vB = feerate_BTC_kvB / 1000 + target_fee = satoshi_round(tx_size * feerate_BTC_vB) if fee < target_fee: raise AssertionError("Fee of %s BTC too low! (Should be %s BTC)" % (str(fee), str(target_fee))) # allow the wallet's estimation to be at most 2 bytes off - if fee > (tx_size + 2) * fee_per_kB / 1000: + if fee > (tx_size + 2) * feerate_BTC_vB: raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)" % (str(fee), str(target_fee))) @@ -366,7 +367,7 @@ def write_config(config_path, *, n, chain, extra_config="", disable_autoconnect= f.write("listenonion=0\n") # Increase peertimeout to avoid disconnects while using mocktime. # peertimeout is measured in wall clock time, so setting it to the - # duration of the longest test is sufficient. It can be overriden in + # duration of the longest test is sufficient. It can be overridden in # tests. f.write("peertimeout=999999\n") f.write("printtoconsole=0\n") @@ -560,17 +561,6 @@ def mine_large_block(test_framework, node, utxos=None): test_framework.generate(node, 1) -def generate_to_height(test_framework, node, target_height): - """Generates blocks until a given target block height has been reached. - To prevent timeouts, only up to 200 blocks are generated per RPC call. - Can be used to activate certain soft-forks (e.g. CSV, CLTV).""" - current_height = node.getblockcount() - while current_height < target_height: - nblocks = min(200, target_height - current_height) - current_height += len(test_framework.generate(node, nblocks)) - assert_equal(node.getblockcount(), target_height) - - def find_vout_for_address(node, txid, addr): """ Locate the vout index of the given transaction sending to the diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py index 08086bc0b9..81aad20079 100644 --- a/test/functional/test_framework/wallet.py +++ b/test/functional/test_framework/wallet.py @@ -10,6 +10,7 @@ from enum import Enum from random import choice from typing import Optional from test_framework.address import ADDRESS_BCRT1_P2WSH_OP_TRUE +from test_framework.descriptors import descsum_create from test_framework.key import ECKey from test_framework.messages import ( COIN, @@ -23,15 +24,17 @@ from test_framework.messages import ( from test_framework.script import ( CScript, LegacySignatureHash, - OP_CHECKSIG, OP_TRUE, OP_NOP, SIGHASH_ALL, ) +from test_framework.script_util import ( + key_to_p2pk_script, + key_to_p2wpkh_script, +) from test_framework.util import ( assert_equal, assert_greater_than_or_equal, - satoshi_round, ) DEFAULT_FEE = Decimal("0.0001") @@ -74,7 +77,7 @@ class MiniWallet: self._priv_key = ECKey() self._priv_key.set((1).to_bytes(32, 'big'), True) pub_key = self._priv_key.get_pubkey() - self._scriptPubKey = bytes(CScript([pub_key.get_bytes(), OP_CHECKSIG])) + self._scriptPubKey = key_to_p2pk_script(pub_key.get_bytes()) elif mode == MiniWalletMode.ADDRESS_OP_TRUE: self._address = ADDRESS_BCRT1_P2WSH_OP_TRUE self._scriptPubKey = bytes.fromhex(self._test_node.validateaddress(self._address)['scriptPubKey']) @@ -82,18 +85,11 @@ class MiniWallet: def rescan_utxos(self): """Drop all utxos and rescan the utxo set""" self._utxos = [] - res = self._test_node.scantxoutset(action="start", scanobjects=[f'raw({self._scriptPubKey.hex()})']) + res = self._test_node.scantxoutset(action="start", scanobjects=[self.get_descriptor()]) assert_equal(True, res['success']) for utxo in res['unspents']: self._utxos.append({'txid': utxo['txid'], 'vout': utxo['vout'], 'value': utxo['amount']}) - def scan_blocks(self, *, start=1, num): - """Scan the blocks for self._address outputs and add them to self._utxos""" - for i in range(start, start + num): - block = self._test_node.getblock(blockhash=self._test_node.getblockhash(i), verbosity=2) - for tx in block['tx']: - self.scan_tx(tx) - def scan_tx(self, tx): """Scan the tx for self._scriptPubKey outputs and add them to self._utxos""" for out in tx['vout']: @@ -115,14 +111,17 @@ class MiniWallet: break tx.vin[0].scriptSig = CScript([der_sig + bytes(bytearray([SIGHASH_ALL]))]) - def generate(self, num_blocks): + def generate(self, num_blocks, **kwargs): """Generate blocks with coinbase outputs to the internal address, and append the outputs to the internal list""" - blocks = self._test_node.generatetodescriptor(num_blocks, f'raw({self._scriptPubKey.hex()})') + blocks = self._test_node.generatetodescriptor(num_blocks, self.get_descriptor(), **kwargs) for b in blocks: cb_tx = self._test_node.getblock(blockhash=b, verbosity=2)['tx'][0] self._utxos.append({'txid': cb_tx['txid'], 'vout': 0, 'value': cb_tx['vout'][0]['value']}) return blocks + def get_descriptor(self): + return descsum_create(f'raw({self._scriptPubKey.hex()})') + def get_address(self): return self._address @@ -150,6 +149,25 @@ class MiniWallet: self.sendrawtransaction(from_node=kwargs['from_node'], tx_hex=tx['hex']) return tx + def send_to(self, *, from_node, scriptPubKey, amount, fee=1000): + """ + Create and send a tx with an output to a given scriptPubKey/amount, + plus a change output to our internal address. To keep things simple, a + fixed fee given in Satoshi is used. + + Note that this method fails if there is no single internal utxo + available that can cover the cost for the amount and the fixed fee + (the utxo with the largest value is taken). + + Returns a tuple (txid, n) referring to the created external utxo outpoint. + """ + tx = self.create_self_transfer(from_node=from_node, fee_rate=0, mempool_valid=False)['tx'] + assert_greater_than_or_equal(tx.vout[0].nValue, amount + fee) + tx.vout[0].nValue -= (amount + fee) # change output -> MiniWallet + tx.vout.append(CTxOut(amount, scriptPubKey)) # arbitrary output -> to be returned + txid = self.sendrawtransaction(from_node=from_node, tx_hex=tx.serialize().hex()) + return txid, 1 + def create_self_transfer(self, *, fee_rate=Decimal("0.003"), from_node, utxo_to_spend=None, mempool_valid=True, locktime=0, sequence=0): """Create and return a tx with the specified fee_rate. Fee may be exact or at most one satoshi higher than needed.""" self._utxos = sorted(self._utxos, key=lambda k: k['value']) @@ -158,13 +176,12 @@ class MiniWallet: vsize = Decimal(96) # anyone-can-spend else: vsize = Decimal(168) # P2PK (73 bytes scriptSig + 35 bytes scriptPubKey + 60 bytes other) - send_value = satoshi_round(utxo_to_spend['value'] - fee_rate * (vsize / 1000)) - fee = utxo_to_spend['value'] - send_value + send_value = int(COIN * (utxo_to_spend['value'] - fee_rate * (vsize / 1000))) assert send_value > 0 tx = CTransaction() tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']), nSequence=sequence)] - tx.vout = [CTxOut(int(send_value * COIN), self._scriptPubKey)] + tx.vout = [CTxOut(send_value, self._scriptPubKey)] tx.nLockTime = locktime if not self._address: # raw script @@ -183,12 +200,22 @@ class MiniWallet: assert_equal(mempool_valid, tx_info['allowed']) if mempool_valid: assert_equal(tx_info['vsize'], vsize) - assert_equal(tx_info['fees']['base'], fee) + assert_equal(tx_info['fees']['base'], utxo_to_spend['value'] - Decimal(send_value) / COIN) return {'txid': tx_info['txid'], 'wtxid': tx_info['wtxid'], 'hex': tx_hex, 'tx': tx} def sendrawtransaction(self, *, from_node, tx_hex): - from_node.sendrawtransaction(tx_hex) + txid = from_node.sendrawtransaction(tx_hex) self.scan_tx(from_node.decoderawtransaction(tx_hex)) + return txid + + +def random_p2wpkh(): + """Generate a random P2WPKH scriptPubKey. Can be used when a random destination is needed, + but no compiled wallet is available (e.g. as replacement to the getnewaddress RPC).""" + key = ECKey() + key.generate() + return key_to_p2wpkh_script(key.get_pubkey().get_bytes()) + def make_chain(node, address, privkeys, parent_txid, parent_value, n=0, parent_locking_script=None, fee=DEFAULT_FEE): """Build a transaction that spends parent_txid.vout[n] and produces one output with diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 3792d751de..916cd94b79 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -40,7 +40,7 @@ except UnicodeDecodeError: CROSS = "x " CIRCLE = "o " -if os.name != 'nt' or sys.getwindowsversion() >= (10, 0, 14393): +if os.name != 'nt' or sys.getwindowsversion() >= (10, 0, 14393): #type:ignore if os.name == 'nt': import ctypes kernel32 = ctypes.windll.kernel32 # type: ignore @@ -98,7 +98,9 @@ BASE_SCRIPTS = [ 'rpc_fundrawtransaction.py --legacy-wallet', 'rpc_fundrawtransaction.py --descriptors', 'p2p_compactblocks.py', + 'p2p_compactblocks_blocksonly.py', 'feature_segwit.py --legacy-wallet', + 'feature_segwit.py --descriptors', # vv Tests less than 2m vv 'wallet_basic.py --legacy-wallet', 'wallet_basic.py --descriptors', @@ -170,11 +172,13 @@ BASE_SCRIPTS = [ 'rpc_users.py', 'rpc_whitelist.py', 'feature_proxy.py', + 'feature_syscall_sandbox.py', 'rpc_signrawtransaction.py --legacy-wallet', 'rpc_signrawtransaction.py --descriptors', 'rpc_rawtransaction.py --legacy-wallet', 'rpc_rawtransaction.py --descriptors', 'wallet_groups.py --legacy-wallet', + 'wallet_transactiontime_rescan.py', 'p2p_addrv2_relay.py', 'wallet_groups.py --descriptors', 'p2p_compactblocks_hb.py', @@ -204,6 +208,7 @@ BASE_SCRIPTS = [ 'feature_assumevalid.py', 'example_test.py', 'wallet_txn_doublespend.py --legacy-wallet', + 'wallet_multisig_descriptor_psbt.py', 'wallet_txn_doublespend.py --descriptors', 'feature_backwards_compatibility.py --legacy-wallet', 'feature_backwards_compatibility.py --descriptors', @@ -304,7 +309,6 @@ BASE_SCRIPTS = [ 'feature_presegwit_node_upgrade.py', 'feature_settings.py', 'rpc_getdescriptorinfo.py', - 'rpc_addresses_deprecation.py', 'rpc_help.py', 'feature_help.py', 'feature_shutdown.py', @@ -401,8 +405,9 @@ def main(): for test in tests: script = test.split("/")[-1] script = script + ".py" if ".py" not in script else script - if script in ALL_SCRIPTS: - test_list.append(script) + matching_scripts = [s for s in ALL_SCRIPTS if s.startswith(script)] + if matching_scripts: + test_list.extend(matching_scripts) else: print("{}WARNING!{} Test '{}' not found in full test list.".format(BOLD[1], BOLD[0], test)) elif args.extended: diff --git a/test/functional/tool_wallet.py b/test/functional/tool_wallet.py index 4bf3927879..cebaa02524 100755 --- a/test/functional/tool_wallet.py +++ b/test/functional/tool_wallet.py @@ -31,8 +31,8 @@ class ToolWalletTest(BitcoinTestFramework): def bitcoin_wallet_process(self, *args): binary = self.config["environment"]["BUILDDIR"] + '/src/bitcoin-wallet' + self.config["environment"]["EXEEXT"] default_args = ['-datadir={}'.format(self.nodes[0].datadir), '-chain=%s' % self.chain] - if self.options.descriptors and 'create' in args: - default_args.append('-descriptors') + if not self.options.descriptors and 'create' in args: + default_args.append('-legacy') return subprocess.Popen([binary] + default_args + list(args), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) @@ -193,7 +193,7 @@ class ToolWalletTest(BitcoinTestFramework): locked_dir = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets") error = 'Error initializing wallet database environment "{}"!'.format(locked_dir) if self.options.descriptors: - error = "SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another bitcoind?" + error = f"SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another instance of {self.config['environment']['PACKAGE_NAME']}?" self.assert_raises_tool_error( error, '-wallet=' + self.default_wallet_name, diff --git a/test/functional/wallet_abandonconflict.py b/test/functional/wallet_abandonconflict.py index 6365840041..8f54e50598 100755 --- a/test/functional/wallet_abandonconflict.py +++ b/test/functional/wallet_abandonconflict.py @@ -120,6 +120,14 @@ class AbandonConflictTest(BitcoinTestFramework): assert_equal(newbalance, balance + Decimal("30")) balance = newbalance + self.log.info("Check abandoned transactions in listsinceblock") + listsinceblock = self.nodes[0].listsinceblock() + txAB1_listsinceblock = [d for d in listsinceblock['transactions'] if d['txid'] == txAB1 and d['category'] == 'send'] + for tx in txAB1_listsinceblock: + assert_equal(tx['abandoned'], True) + assert_equal(tx['confirmations'], 0) + assert_equal(tx['trusted'], False) + # Verify that even with a low min relay fee, the tx is not reaccepted from wallet on startup once abandoned self.restart_node(0, extra_args=["-minrelaytxfee=0.00001"]) assert self.nodes[0].getmempoolinfo()['loaded'] @@ -149,6 +157,7 @@ class AbandonConflictTest(BitcoinTestFramework): assert_equal(newbalance, balance - Decimal("24.9996")) balance = newbalance + self.log.info("Test transactions conflicted by a double spend") # Create a double spend of AB1 by spending again from only A's 10 output # Mine double spend from node 1 inputs = [] @@ -163,6 +172,34 @@ class AbandonConflictTest(BitcoinTestFramework): self.connect_nodes(0, 1) self.sync_blocks() + tx_list = self.nodes[0].listtransactions() + + conflicted = [tx for tx in tx_list if tx["confirmations"] < 0] + assert_equal(4, len(conflicted)) + + wallet_conflicts = [tx for tx in conflicted if tx["walletconflicts"]] + assert_equal(2, len(wallet_conflicts)) + + double_spends = [tx for tx in tx_list if tx["walletconflicts"] and tx["confirmations"] > 0] + assert_equal(1, len(double_spends)) + double_spend = double_spends[0] + + # Test the properties of the conflicted transactions, i.e. with confirmations < 0. + for tx in conflicted: + assert_equal(tx["abandoned"], False) + assert_equal(tx["confirmations"], -1) + assert_equal(tx["trusted"], False) + + # Test the properties of the double-spend transaction, i.e. having wallet conflicts and confirmations > 0. + assert_equal(double_spend["abandoned"], False) + assert_equal(double_spend["confirmations"], 1) + assert "trusted" not in double_spend.keys() # "trusted" only returned if tx has 0 or negative confirmations. + + # Test the walletconflicts field of each. + for tx in wallet_conflicts: + assert_equal(double_spend["walletconflicts"], [tx["txid"]]) + assert_equal(tx["walletconflicts"], [double_spend["txid"]]) + # Verify that B and C's 10 BTC outputs are available for spending again because AB1 is now conflicted newbalance = self.nodes[0].getbalance() assert_equal(newbalance, balance + Decimal("20")) @@ -176,7 +213,7 @@ class AbandonConflictTest(BitcoinTestFramework): #assert_equal(newbalance, balance - Decimal("10")) self.log.info("If balance has not declined after invalidateblock then out of mempool wallet tx which is no longer") self.log.info("conflicted has not resumed causing its inputs to be seen as spent. See Issue #7315") - self.log.info(str(balance) + " -> " + str(newbalance) + " ?") + assert_equal(balance, newbalance) if __name__ == '__main__': diff --git a/test/functional/wallet_address_types.py b/test/functional/wallet_address_types.py index bdee22e62b..7a448e8590 100755 --- a/test/functional/wallet_address_types.py +++ b/test/functional/wallet_address_types.py @@ -204,8 +204,7 @@ class AddressTypeTest(BitcoinTestFramework): def test_change_output_type(self, node_sender, destinations, expected_type): txid = self.nodes[node_sender].sendmany(dummy="", amounts=dict.fromkeys(destinations, 0.001)) - raw_tx = self.nodes[node_sender].getrawtransaction(txid) - tx = self.nodes[node_sender].decoderawtransaction(raw_tx) + tx = self.nodes[node_sender].gettransaction(txid=txid, verbose=True)['decoded'] # Make sure the transaction has change: assert_equal(len(tx["vout"]), len(destinations) + 1) diff --git a/test/functional/wallet_backup.py b/test/functional/wallet_backup.py index bc6d6206e5..a07c28c8a4 100755 --- a/test/functional/wallet_backup.py +++ b/test/functional/wallet_backup.py @@ -124,9 +124,9 @@ class WalletBackupTest(BitcoinTestFramework): assert_raises_rpc_error(-8, "Wallet name already exists.", node.restorewallet, wallet_name, wallet_file) def init_three(self): - self.init_wallet(0) - self.init_wallet(1) - self.init_wallet(2) + self.init_wallet(node=0) + self.init_wallet(node=1) + self.init_wallet(node=2) def run_test(self): self.log.info("Generating initial blockchain") diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py index 6372e1acd7..92da54d97c 100755 --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -13,6 +13,7 @@ from test_framework.util import ( assert_equal, assert_fee_amount, assert_raises_rpc_error, + find_vout_for_address, ) from test_framework.wallet_util import test_address @@ -121,13 +122,49 @@ class WalletTest(BitcoinTestFramework): # Exercise locking of unspent outputs unspent_0 = self.nodes[2].listunspent()[0] unspent_0 = {"txid": unspent_0["txid"], "vout": unspent_0["vout"]} + # Trying to unlock an output which isn't locked should error assert_raises_rpc_error(-8, "Invalid parameter, expected locked output", self.nodes[2].lockunspent, True, [unspent_0]) + + # Locking an already-locked output should error self.nodes[2].lockunspent(False, [unspent_0]) assert_raises_rpc_error(-8, "Invalid parameter, output already locked", self.nodes[2].lockunspent, False, [unspent_0]) + + # Restarting the node should clear the lock + self.restart_node(2) + self.nodes[2].lockunspent(False, [unspent_0]) + + # Unloading and reloating the wallet should clear the lock + assert_equal(self.nodes[0].listwallets(), [self.default_wallet_name]) + self.nodes[2].unloadwallet(self.default_wallet_name) + self.nodes[2].loadwallet(self.default_wallet_name) + assert_equal(len(self.nodes[2].listlockunspent()), 0) + + # Locking non-persistently, then re-locking persistently, is allowed + self.nodes[2].lockunspent(False, [unspent_0]) + self.nodes[2].lockunspent(False, [unspent_0], True) + + # Restarting the node with the lock written to the wallet should keep the lock + self.restart_node(2) + assert_raises_rpc_error(-8, "Invalid parameter, output already locked", self.nodes[2].lockunspent, False, [unspent_0]) + + # Unloading and reloading the wallet with a persistent lock should keep the lock + self.nodes[2].unloadwallet(self.default_wallet_name) + self.nodes[2].loadwallet(self.default_wallet_name) + assert_raises_rpc_error(-8, "Invalid parameter, output already locked", self.nodes[2].lockunspent, False, [unspent_0]) + + # Locked outputs should not be used, even if they are the only available funds assert_raises_rpc_error(-6, "Insufficient funds", self.nodes[2].sendtoaddress, self.nodes[2].getnewaddress(), 20) assert_equal([unspent_0], self.nodes[2].listlockunspent()) + + # Unlocking should remove the persistent lock self.nodes[2].lockunspent(True, [unspent_0]) + self.restart_node(2) assert_equal(len(self.nodes[2].listlockunspent()), 0) + + # Reconnect node 2 after restarts + self.connect_nodes(1, 2) + self.connect_nodes(0, 2) + assert_raises_rpc_error(-8, "txid must be of length 64 (not 34, for '0000000000000000000000000000000000')", self.nodes[2].lockunspent, False, [{"txid": "0000000000000000000000000000000000", "vout": 0}]) @@ -427,6 +464,9 @@ class WalletTest(BitcoinTestFramework): # 1. Send some coins to generate new UTXO address_to_import = self.nodes[2].getnewaddress() txid = self.nodes[0].sendtoaddress(address_to_import, 1) + self.sync_mempools(self.nodes[0:3]) + vout = find_vout_for_address(self.nodes[2], txid, address_to_import) + self.nodes[2].lockunspent(False, [{"txid": txid, "vout": vout}]) self.generate(self.nodes[0], 1) self.sync_all(self.nodes[0:3]) @@ -542,23 +582,17 @@ class WalletTest(BitcoinTestFramework): assert label in self.nodes[0].listlabels() self.nodes[0].rpc.ensure_ascii = True # restore to default - # maintenance tests - maintenance = [ - '-rescan', - '-reindex', - ] + # -reindex tests chainlimit = 6 - for m in maintenance: - self.log.info("Test " + m) - self.stop_nodes() - # set lower ancestor limit for later - self.start_node(0, [m, "-limitancestorcount=" + str(chainlimit)]) - self.start_node(1, [m, "-limitancestorcount=" + str(chainlimit)]) - self.start_node(2, [m, "-limitancestorcount=" + str(chainlimit)]) - if m == '-reindex': - # reindex will leave rpc warm up "early"; Wait for it to finish - self.wait_until(lambda: [block_count] * 3 == [self.nodes[i].getblockcount() for i in range(3)]) - assert_equal(balance_nodes, [self.nodes[i].getbalance() for i in range(3)]) + self.log.info("Test -reindex") + self.stop_nodes() + # set lower ancestor limit for later + self.start_node(0, ['-reindex', "-limitancestorcount=" + str(chainlimit)]) + self.start_node(1, ['-reindex', "-limitancestorcount=" + str(chainlimit)]) + self.start_node(2, ['-reindex', "-limitancestorcount=" + str(chainlimit)]) + # reindex will leave rpc warm up "early"; Wait for it to finish + self.wait_until(lambda: [block_count] * 3 == [self.nodes[i].getblockcount() for i in range(3)]) + assert_equal(balance_nodes, [self.nodes[i].getbalance() for i in range(3)]) # Exercise listsinceblock with the last two blocks coinbase_tx_1 = self.nodes[0].listsinceblock(blocks[0]) @@ -632,7 +666,7 @@ class WalletTest(BitcoinTestFramework): self.generate(self.nodes[0], 1) destination = self.nodes[1].getnewaddress() txid = self.nodes[0].sendtoaddress(destination, 0.123) - tx = self.nodes[0].decoderawtransaction(self.nodes[0].gettransaction(txid)['hex']) + tx = self.nodes[0].gettransaction(txid=txid, verbose=True)['decoded'] output_addresses = [vout['scriptPubKey']['address'] for vout in tx["vout"]] assert len(output_addresses) > 1 for address in output_addresses: diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py index a1676fffa5..46a5df4a8e 100755 --- a/test/functional/wallet_bumpfee.py +++ b/test/functional/wallet_bumpfee.py @@ -32,6 +32,8 @@ from test_framework.util import ( assert_greater_than, assert_raises_rpc_error, ) +from test_framework.wallet import MiniWallet + WALLET_PASSPHRASE = "test" WALLET_PASSPHRASE_TIMEOUT = 3600 @@ -265,6 +267,14 @@ def test_bumpfee_with_descendant_fails(self, rbf_node, rbf_node_address, dest_ad tx = rbf_node.signrawtransactionwithwallet(tx) rbf_node.sendrawtransaction(tx["hex"]) assert_raises_rpc_error(-8, "Transaction has descendants in the wallet", rbf_node.bumpfee, parent_id) + + # create tx with descendant in the mempool by using MiniWallet + miniwallet = MiniWallet(rbf_node) + parent_id = spend_one_input(rbf_node, miniwallet.get_address()) + tx = rbf_node.gettransaction(txid=parent_id, verbose=True)['decoded'] + miniwallet.scan_tx(tx) + miniwallet.send_self_transfer(from_node=rbf_node) + assert_raises_rpc_error(-8, "Transaction has descendants in the mempool", rbf_node.bumpfee, parent_id) self.clear_mempool() diff --git a/test/functional/wallet_create_tx.py b/test/functional/wallet_create_tx.py index c8b92ef1bf..00ee08002e 100755 --- a/test/functional/wallet_create_tx.py +++ b/test/functional/wallet_create_tx.py @@ -34,13 +34,13 @@ class CreateTxWalletTest(BitcoinTestFramework): self.log.info('Check that we have some (old) blocks and that anti-fee-sniping is disabled') assert_equal(self.nodes[0].getblockchaininfo()['blocks'], 200) txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) - tx = self.nodes[0].decoderawtransaction(self.nodes[0].gettransaction(txid)['hex']) + tx = self.nodes[0].gettransaction(txid=txid, verbose=True)['decoded'] assert_equal(tx['locktime'], 0) self.log.info('Check that anti-fee-sniping is enabled when we mine a recent block') self.generate(self.nodes[0], 1) txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) - tx = self.nodes[0].decoderawtransaction(self.nodes[0].gettransaction(txid)['hex']) + tx = self.nodes[0].gettransaction(txid=txid, verbose=True)['decoded'] assert 0 < tx['locktime'] <= 201 def test_tx_size_too_large(self): diff --git a/test/functional/wallet_descriptor.py b/test/functional/wallet_descriptor.py index 17a4c79da3..4ec44a8a6c 100755 --- a/test/functional/wallet_descriptor.py +++ b/test/functional/wallet_descriptor.py @@ -84,7 +84,7 @@ class WalletDescriptorTest(BitcoinTestFramework): send_wrpc = self.nodes[0].get_wallet_rpc("desc1") # Generate some coins - self.generatetoaddress(send_wrpc, COINBASE_MATURITY + 1, send_wrpc.getnewaddress()) + self.generatetoaddress(self.nodes[0], COINBASE_MATURITY + 1, send_wrpc.getnewaddress()) # Make transactions self.log.info("Test sending and receiving") diff --git a/test/functional/wallet_hd.py b/test/functional/wallet_hd.py index 74f584f2cd..f54ae89c04 100755 --- a/test/functional/wallet_hd.py +++ b/test/functional/wallet_hd.py @@ -103,7 +103,7 @@ class WalletHDTest(BitcoinTestFramework): self.sync_all() # Needs rescan - self.restart_node(1, extra_args=self.extra_args[1] + ['-rescan']) + self.nodes[1].rescanblockchain() assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1) # Try a RPC based rescan @@ -129,7 +129,7 @@ class WalletHDTest(BitcoinTestFramework): # send a tx and make sure its using the internal chain for the changeoutput txid = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 1) - outs = self.nodes[1].decoderawtransaction(self.nodes[1].gettransaction(txid)['hex'])['vout'] + outs = self.nodes[1].gettransaction(txid=txid, verbose=True)['decoded']['vout'] keypath = "" for out in outs: if out['value'] != 1: diff --git a/test/functional/wallet_import_rescan.py b/test/functional/wallet_import_rescan.py index cbe3e9bfdd..27a2a42dac 100755 --- a/test/functional/wallet_import_rescan.py +++ b/test/functional/wallet_import_rescan.py @@ -99,7 +99,7 @@ class Variant(collections.namedtuple("Variant", "call data address_type rescan p assert_equal(tx["label"], self.label) assert_equal(tx["txid"], txid) assert_equal(tx["confirmations"], 1 + current_height - confirmation_height) - assert_equal("trusted" not in tx, True) + assert "trusted" not in tx address, = [ad for ad in addresses if txid in ad["txids"]] assert_equal(address["address"], self.address["address"]) diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py index d86c3737fe..fc9eac1d74 100755 --- a/test/functional/wallet_importdescriptors.py +++ b/test/functional/wallet_importdescriptors.py @@ -74,7 +74,7 @@ class ImportDescriptorsTest(BitcoinTestFramework): assert_equal(wpriv.getwalletinfo()['keypoolsize'], 0) self.log.info('Mining coins') - self.generatetoaddress(w0, COINBASE_MATURITY + 1, w0.getnewaddress()) + self.generatetoaddress(self.nodes[0], COINBASE_MATURITY + 1, w0.getnewaddress()) # RPC importdescriptors ----------------------------------------------- @@ -405,7 +405,7 @@ class ImportDescriptorsTest(BitcoinTestFramework): solvable=True, ismine=True) txid = w0.sendtoaddress(address, 49.99995540) - self.generatetoaddress(w0, 6, w0.getnewaddress()) + self.generatetoaddress(self.nodes[0], 6, w0.getnewaddress()) self.sync_blocks() tx = wpriv.createrawtransaction([{"txid": txid, "vout": 0}], {w0.getnewaddress(): 49.999}) signed_tx = wpriv.signrawtransactionwithwallet(tx) @@ -454,7 +454,7 @@ class ImportDescriptorsTest(BitcoinTestFramework): self.generate(self.nodes[0], 6) self.sync_all() send_txid = wmulti_priv.sendtoaddress(w0.getnewaddress(), 8) - decoded = wmulti_priv.decoderawtransaction(wmulti_priv.gettransaction(send_txid)['hex']) + decoded = wmulti_priv.gettransaction(txid=send_txid, verbose=True)['decoded'] assert_equal(len(decoded['vin'][0]['txinwitness']), 4) self.generate(self.nodes[0], 6) self.sync_all() @@ -586,7 +586,7 @@ class ImportDescriptorsTest(BitcoinTestFramework): self.sync_all() # It is standard and would relay. txid = wmulti_priv_big.sendtoaddress(w0.getnewaddress(), 9.999) - decoded = wmulti_priv_big.decoderawtransaction(wmulti_priv_big.gettransaction(txid)['hex']) + decoded = wmulti_priv_big.gettransaction(txid=txid, verbose=True)['decoded'] # 20 sigs + dummy + witness script assert_equal(len(decoded['vin'][0]['txinwitness']), 22) @@ -620,12 +620,8 @@ class ImportDescriptorsTest(BitcoinTestFramework): self.generate(self.nodes[0], 6) self.sync_all() # It is standard and would relay. - txid = multi_priv_big.sendtoaddress(w0.getnewaddress(), 10, "", "", - True) - decoded = multi_priv_big.decoderawtransaction( - multi_priv_big.gettransaction(txid)['hex'] - ) - + txid = multi_priv_big.sendtoaddress(w0.getnewaddress(), 10, "", "", True) + decoded = multi_priv_big.gettransaction(txid=txid, verbose=True)['decoded'] self.log.info("Amending multisig with new private keys") self.nodes[1].createwallet(wallet_name="wmulti_priv3", descriptors=True) diff --git a/test/functional/wallet_keypool.py b/test/functional/wallet_keypool.py index c714993234..79235646b0 100755 --- a/test/functional/wallet_keypool.py +++ b/test/functional/wallet_keypool.py @@ -138,6 +138,20 @@ class KeyPoolTest(BitcoinTestFramework): assert_equal(wi['keypoolsize_hd_internal'], 100) assert_equal(wi['keypoolsize'], 100) + if not self.options.descriptors: + # Check that newkeypool entirely flushes the keypool + start_keypath = nodes[0].getaddressinfo(nodes[0].getnewaddress())['hdkeypath'] + start_change_keypath = nodes[0].getaddressinfo(nodes[0].getrawchangeaddress())['hdkeypath'] + # flush keypool and get new addresses + nodes[0].newkeypool() + end_keypath = nodes[0].getaddressinfo(nodes[0].getnewaddress())['hdkeypath'] + end_change_keypath = nodes[0].getaddressinfo(nodes[0].getrawchangeaddress())['hdkeypath'] + # The new keypath index should be 100 more than the old one + new_index = int(start_keypath.rsplit('/', 1)[1][:-1]) + 100 + new_change_index = int(start_change_keypath.rsplit('/', 1)[1][:-1]) + 100 + assert_equal(end_keypath, "m/0'/0'/" + str(new_index) + "'") + assert_equal(end_change_keypath, "m/0'/1'/" + str(new_change_index) + "'") + # create a blank wallet nodes[0].createwallet(wallet_name='w2', blank=True, disable_private_keys=True) w2 = nodes[0].get_wallet_rpc('w2') diff --git a/test/functional/wallet_listdescriptors.py b/test/functional/wallet_listdescriptors.py index 221f5262d9..436bbdcfcc 100755 --- a/test/functional/wallet_listdescriptors.py +++ b/test/functional/wallet_listdescriptors.py @@ -23,7 +23,7 @@ class ListDescriptorsTest(BitcoinTestFramework): self.skip_if_no_sqlite() # do not create any wallet by default - def init_wallet(self, i): + def init_wallet(self, *, node): return def run_test(self): diff --git a/test/functional/wallet_listsinceblock.py b/test/functional/wallet_listsinceblock.py index bd3b29c81c..f4a00a8ec8 100755 --- a/test/functional/wallet_listsinceblock.py +++ b/test/functional/wallet_listsinceblock.py @@ -44,6 +44,14 @@ class ListSinceBlockTest(BitcoinTestFramework): def test_no_blockhash(self): self.log.info("Test no blockhash") txid = self.nodes[2].sendtoaddress(self.nodes[0].getnewaddress(), 1) + self.sync_all() + assert_array_result(self.nodes[0].listtransactions(), {"txid": txid}, { + "category": "receive", + "amount": 1, + "confirmations": 0, + "trusted": False, + }) + blockhash, = self.generate(self.nodes[2], 1) blockheight = self.nodes[2].getblockheader(blockhash)['height'] self.sync_all() @@ -56,6 +64,9 @@ class ListSinceBlockTest(BitcoinTestFramework): "blockheight": blockheight, "confirmations": 1, }) + assert_equal(len(txs), 1) + assert "trusted" not in txs[0] + assert_equal( self.nodes[0].listsinceblock(), {"lastblock": blockhash, diff --git a/test/functional/wallet_listtransactions.py b/test/functional/wallet_listtransactions.py index a14bfe345c..ca6a6ab540 100755 --- a/test/functional/wallet_listtransactions.py +++ b/test/functional/wallet_listtransactions.py @@ -31,10 +31,10 @@ class ListTransactionsTest(BitcoinTestFramework): self.sync_all() assert_array_result(self.nodes[0].listtransactions(), {"txid": txid}, - {"category": "send", "amount": Decimal("-0.1"), "confirmations": 0}) + {"category": "send", "amount": Decimal("-0.1"), "confirmations": 0, "trusted": True}) assert_array_result(self.nodes[1].listtransactions(), {"txid": txid}, - {"category": "receive", "amount": Decimal("0.1"), "confirmations": 0}) + {"category": "receive", "amount": Decimal("0.1"), "confirmations": 0, "trusted": False}) self.log.info("Test confirmations change after mining a block") blockhash = self.generate(self.nodes[0], 1)[0] blockheight = self.nodes[0].getblockheader(blockhash)['height'] @@ -204,6 +204,15 @@ class ListTransactionsTest(BitcoinTestFramework): assert_equal(n.gettransaction(txid_3b)["bip125-replaceable"], "yes") assert_equal(n.gettransaction(txid_4)["bip125-replaceable"], "unknown") + self.log.info("Test bip125-replaceable status with listsinceblock") + for n in self.nodes[0:2]: + txs = {tx['txid']: tx['bip125-replaceable'] for tx in n.listsinceblock()['transactions']} + assert_equal(txs[txid_1], "no") + assert_equal(txs[txid_2], "no") + assert_equal(txs[txid_3], "yes") + assert_equal(txs[txid_3b], "yes") + assert_equal(txs[txid_4], "unknown") + self.log.info("Test mined transactions are no longer bip125-replaceable") self.generate(self.nodes[0], 1) assert txid_3b not in self.nodes[0].getrawmempool() diff --git a/test/functional/wallet_multisig_descriptor_psbt.py b/test/functional/wallet_multisig_descriptor_psbt.py new file mode 100755 index 0000000000..ed855d2525 --- /dev/null +++ b/test/functional/wallet_multisig_descriptor_psbt.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 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 a basic M-of-N multisig setup between multiple people using descriptor wallets and PSBTs, as well as a signing flow. + +This is meant to be documentation as much as functional tests, so it is kept as simple and readable as possible. +""" + +from test_framework.address import base58_to_byte +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_approx, + assert_equal, +) + + +class WalletMultisigDescriptorPSBTTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 3 + self.setup_clean_chain = True + self.wallet_names = [] + self.extra_args = [["-keypool=100"]] * self.num_nodes + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + self.skip_if_no_sqlite() + + @staticmethod + def _get_xpub(wallet): + """Extract the wallet's xpubs using `listdescriptors` and pick the one from the `pkh` descriptor since it's least likely to be accidentally reused (legacy addresses).""" + descriptor = next(filter(lambda d: d["desc"].startswith("pkh"), wallet.listdescriptors()["descriptors"])) + return descriptor["desc"].split("]")[-1].split("/")[0] + + @staticmethod + def _check_psbt(psbt, to, value, multisig): + """Helper function for any of the N participants to check the psbt with decodepsbt and verify it is OK before signing.""" + tx = multisig.decodepsbt(psbt)["tx"] + amount = 0 + for vout in tx["vout"]: + address = vout["scriptPubKey"]["address"] + assert_equal(multisig.getaddressinfo(address)["ischange"], address != to) + if address == to: + amount += vout["value"] + assert_approx(amount, float(value), vspan=0.001) + + def participants_create_multisigs(self, xpubs): + """The multisig is created by importing the following descriptors. The resulting wallet is watch-only and every participant can do this.""" + # some simple validation + assert_equal(len(xpubs), self.N) + # a sanity-check/assertion, this will throw if the base58 checksum of any of the provided xpubs are invalid + for xpub in xpubs: + base58_to_byte(xpub) + + for i, node in enumerate(self.nodes): + node.createwallet(wallet_name=f"{self.name}_{i}", blank=True, descriptors=True, disable_private_keys=True) + multisig = node.get_wallet_rpc(f"{self.name}_{i}") + external = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{f'/0/*,'.join(xpubs)}/0/*))") + internal = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{f'/1/*,'.join(xpubs)}/1/*))") + result = multisig.importdescriptors([ + { # receiving addresses (internal: False) + "desc": external["descriptor"], + "active": True, + "internal": False, + "timestamp": "now", + }, + { # change addresses (internal: True) + "desc": internal["descriptor"], + "active": True, + "internal": True, + "timestamp": "now", + }, + ]) + assert all(r["success"] for r in result) + yield multisig + + def run_test(self): + self.M = 2 + self.N = self.num_nodes + self.name = f"{self.M}_of_{self.N}_multisig" + self.log.info(f"Testing {self.name}...") + + participants = { + # Every participant generates an xpub. The most straightforward way is to create a new descriptor wallet. + # This wallet will be the participant's `signer` for the resulting multisig. Avoid reusing this wallet for any other purpose (for privacy reasons). + "signers": [node.get_wallet_rpc(node.createwallet(wallet_name=f"participant_{self.nodes.index(node)}", descriptors=True)["name"]) for node in self.nodes], + # After participants generate and exchange their xpubs they will each create their own watch-only multisig. + # Note: these multisigs are all the same, this justs highlights that each participant can independently verify everything on their own node. + "multisigs": [] + } + + self.log.info("Generate and exchange xpubs...") + xpubs = [self._get_xpub(signer) for signer in participants["signers"]] + + self.log.info("Every participant imports the following descriptors to create the watch-only multisig...") + participants["multisigs"] = list(self.participants_create_multisigs(xpubs)) + + self.log.info("Check that every participant's multisig generates the same addresses...") + for _ in range(10): # we check that the first 10 generated addresses are the same for all participant's multisigs + receive_addresses = [multisig.getnewaddress() for multisig in participants["multisigs"]] + all(address == receive_addresses[0] for address in receive_addresses) + change_addresses = [multisig.getrawchangeaddress() for multisig in participants["multisigs"]] + all(address == change_addresses[0] for address in change_addresses) + + self.log.info("Get a mature utxo to send to the multisig...") + coordinator_wallet = participants["signers"][0] + self.generatetoaddress(self.nodes[0], 101, coordinator_wallet.getnewaddress()) + + deposit_amount = 6.15 + multisig_receiving_address = participants["multisigs"][0].getnewaddress() + self.log.info("Send funds to the resulting multisig receiving address...") + coordinator_wallet.sendtoaddress(multisig_receiving_address, deposit_amount) + self.generate(self.nodes[0], 1) + self.sync_all() + for participant in participants["multisigs"]: + assert_approx(participant.getbalance(), deposit_amount, vspan=0.001) + + self.log.info("Send a transaction from the multisig!") + to = participants["signers"][self.N - 1].getnewaddress() + value = 1 + self.log.info("First, make a sending transaction, created using `walletcreatefundedpsbt` (anyone can initiate this)...") + psbt = participants["multisigs"][0].walletcreatefundedpsbt(inputs=[], outputs={to: value}, options={"feeRate": 0.00010}) + + psbts = [] + self.log.info("Now at least M users check the psbt with decodepsbt and (if OK) signs it with walletprocesspsbt...") + for m in range(self.M): + signers_multisig = participants["multisigs"][m] + self._check_psbt(psbt["psbt"], to, value, signers_multisig) + signing_wallet = participants["signers"][m] + partially_signed_psbt = signing_wallet.walletprocesspsbt(psbt["psbt"]) + psbts.append(partially_signed_psbt["psbt"]) + + self.log.info("Finally, collect the signed PSBTs with combinepsbt, finalizepsbt, then broadcast the resulting transaction...") + combined = coordinator_wallet.combinepsbt(psbts) + finalized = coordinator_wallet.finalizepsbt(combined) + coordinator_wallet.sendrawtransaction(finalized["hex"]) + + self.log.info("Check that balances are correct after the transaction has been included in a block.") + self.generate(self.nodes[0], 1) + self.sync_all() + assert_approx(participants["multisigs"][0].getbalance(), deposit_amount - value, vspan=0.001) + assert_equal(participants["signers"][self.N - 1].getbalance(), value) + + self.log.info("Send another transaction from the multisig, this time with a daisy chained signing flow (one after another in series)!") + psbt = participants["multisigs"][0].walletcreatefundedpsbt(inputs=[], outputs={to: value}, options={"feeRate": 0.00010}) + for m in range(self.M): + signers_multisig = participants["multisigs"][m] + self._check_psbt(psbt["psbt"], to, value, signers_multisig) + signing_wallet = participants["signers"][m] + psbt = signing_wallet.walletprocesspsbt(psbt["psbt"]) + assert_equal(psbt["complete"], m == self.M - 1) + finalized = coordinator_wallet.finalizepsbt(psbt["psbt"]) + coordinator_wallet.sendrawtransaction(finalized["hex"]) + + self.log.info("Check that balances are correct after the transaction has been included in a block.") + self.generate(self.nodes[0], 1) + self.sync_all() + assert_approx(participants["multisigs"][0].getbalance(), deposit_amount - (value * 2), vspan=0.001) + assert_equal(participants["signers"][self.N - 1].getbalance(), value * 2) + + +if __name__ == "__main__": + WalletMultisigDescriptorPSBTTest().main() diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py index d4768f5043..68ca005649 100755 --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -202,7 +202,7 @@ class MultiWalletTest(BitcoinTestFramework): self.restart_node(0, ['-nowallet', '-walletdir=' + competing_wallet_dir]) self.nodes[0].createwallet(self.default_wallet_name) if self.options.descriptors: - exp_stderr = r"Error: SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another bitcoind?" + exp_stderr = f"Error: SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another instance of {self.config['environment']['PACKAGE_NAME']}?" else: exp_stderr = r"Error: Error initializing wallet database environment \"\S+competing_walletdir\S*\"!" self.nodes[1].assert_start_raises_init_error(['-walletdir=' + competing_wallet_dir], exp_stderr, match=ErrorMatch.PARTIAL_REGEX) @@ -303,7 +303,7 @@ class MultiWalletTest(BitcoinTestFramework): # Fail to load duplicate wallets path = os.path.join(self.options.tmpdir, "node0", "regtest", "wallets", "w1", "wallet.dat") if self.options.descriptors: - assert_raises_rpc_error(-4, "Wallet file verification failed. SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another bitcoind?", self.nodes[0].loadwallet, wallet_names[0]) + assert_raises_rpc_error(-4, f"Wallet file verification failed. SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another instance of {self.config['environment']['PACKAGE_NAME']}?", self.nodes[0].loadwallet, wallet_names[0]) else: assert_raises_rpc_error(-35, "Wallet file verification failed. Refusing to load database. Data file '{}' is already loaded.".format(path), self.nodes[0].loadwallet, wallet_names[0]) diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py index aecdaf821f..c9daeabeb9 100755 --- a/test/functional/wallet_send.py +++ b/test/functional/wallet_send.py @@ -9,6 +9,7 @@ from itertools import product from test_framework.authproxy import JSONRPCException from test_framework.descriptors import descsum_create +from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -16,6 +17,7 @@ from test_framework.util import ( assert_greater_than, assert_raises_rpc_error, ) +from test_framework.wallet_util import bytes_to_wif class WalletSendTest(BitcoinTestFramework): def set_test_params(self): @@ -35,7 +37,7 @@ class WalletSendTest(BitcoinTestFramework): conf_target=None, estimate_mode=None, fee_rate=None, add_to_wallet=None, psbt=None, inputs=None, add_inputs=None, include_unsafe=None, change_address=None, change_position=None, change_type=None, include_watching=None, locktime=None, lock_unspents=None, replaceable=None, subtract_fee_from_outputs=None, - expect_error=None): + expect_error=None, solving_data=None): assert (amount is None) != (data is None) from_balance_before = from_wallet.getbalances()["mine"]["trusted"] @@ -94,6 +96,8 @@ class WalletSendTest(BitcoinTestFramework): options["replaceable"] = replaceable if subtract_fee_from_outputs is not None: options["subtract_fee_from_outputs"] = subtract_fee_from_outputs + if solving_data is not None: + options["solving_data"] = solving_data if len(options.keys()) == 0: options = None @@ -476,6 +480,47 @@ class WalletSendTest(BitcoinTestFramework): res = self.test_send(from_wallet=w5, to_wallet=w0, amount=1, include_unsafe=True) assert res["complete"] + self.log.info("External outputs") + eckey = ECKey() + eckey.generate() + privkey = bytes_to_wif(eckey.get_bytes()) + + self.nodes[1].createwallet("extsend") + ext_wallet = self.nodes[1].get_wallet_rpc("extsend") + self.nodes[1].createwallet("extfund") + ext_fund = self.nodes[1].get_wallet_rpc("extfund") + + # Make a weird but signable script. sh(pkh()) descriptor accomplishes this + desc = descsum_create("sh(pkh({}))".format(privkey)) + if self.options.descriptors: + res = ext_fund.importdescriptors([{"desc": desc, "timestamp": "now"}]) + else: + res = ext_fund.importmulti([{"desc": desc, "timestamp": "now"}]) + assert res[0]["success"] + addr = self.nodes[0].deriveaddresses(desc)[0] + addr_info = ext_fund.getaddressinfo(addr) + + self.nodes[0].sendtoaddress(addr, 10) + self.nodes[0].sendtoaddress(ext_wallet.getnewaddress(), 10) + self.generate(self.nodes[0], 6) + self.sync_all() + ext_utxo = ext_fund.listunspent(addresses=[addr])[0] + + # An external input without solving data should result in an error + self.test_send(from_wallet=ext_wallet, to_wallet=self.nodes[0], amount=15, inputs=[ext_utxo], add_inputs=True, psbt=True, include_watching=True, expect_error=(-4, "Insufficient funds")) + + # But funding should work when the solving data is provided + res = self.test_send(from_wallet=ext_wallet, to_wallet=self.nodes[0], amount=15, inputs=[ext_utxo], add_inputs=True, psbt=True, include_watching=True, solving_data={"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]}) + signed = ext_wallet.walletprocesspsbt(res["psbt"]) + signed = ext_fund.walletprocesspsbt(res["psbt"]) + assert signed["complete"] + self.nodes[0].finalizepsbt(signed["psbt"]) + + res = self.test_send(from_wallet=ext_wallet, to_wallet=self.nodes[0], amount=15, inputs=[ext_utxo], add_inputs=True, psbt=True, include_watching=True, solving_data={"descriptors": [desc]}) + signed = ext_wallet.walletprocesspsbt(res["psbt"]) + signed = ext_fund.walletprocesspsbt(res["psbt"]) + assert signed["complete"] + self.nodes[0].finalizepsbt(signed["psbt"]) if __name__ == '__main__': WalletSendTest().main() diff --git a/test/functional/wallet_signer.py b/test/functional/wallet_signer.py index 7b77755d64..c6c1cc8784 100755 --- a/test/functional/wallet_signer.py +++ b/test/functional/wallet_signer.py @@ -27,6 +27,9 @@ class WalletSignerTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 + # The experimental syscall sandbox feature (-sandbox) is not compatible with -signer (which + # invokes execve). + self.disable_syscall_sandbox = True self.extra_args = [ [], diff --git a/test/functional/wallet_taproot.py b/test/functional/wallet_taproot.py index 4f84dbd125..80c125df97 100755 --- a/test/functional/wallet_taproot.py +++ b/test/functional/wallet_taproot.py @@ -185,7 +185,7 @@ class WalletTaprootTest(BitcoinTestFramework): def setup_network(self): self.setup_nodes() - def init_wallet(self, i): + def init_wallet(self, *, node): pass @staticmethod diff --git a/test/functional/wallet_transactiontime_rescan.py b/test/functional/wallet_transactiontime_rescan.py new file mode 100755 index 0000000000..afa5139da7 --- /dev/null +++ b/test/functional/wallet_transactiontime_rescan.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +# Copyright (c) 2018-2019 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 transaction time during old block rescanning +""" + +import time + +from test_framework.blocktools import COINBASE_MATURITY +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal +) + + +class TransactionTimeRescanTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = False + self.num_nodes = 3 + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def run_test(self): + self.log.info('Prepare nodes and wallet') + + minernode = self.nodes[0] # node used to mine BTC and create transactions + usernode = self.nodes[1] # user node with correct time + restorenode = self.nodes[2] # node used to restore user wallet and check time determination in ComputeSmartTime (wallet.cpp) + + # time constant + cur_time = int(time.time()) + ten_days = 10 * 24 * 60 * 60 + + # synchronize nodes and time + self.sync_all() + minernode.setmocktime(cur_time) + usernode.setmocktime(cur_time) + restorenode.setmocktime(cur_time) + + # prepare miner wallet + minernode.createwallet(wallet_name='default') + miner_wallet = minernode.get_wallet_rpc('default') + m1 = miner_wallet.getnewaddress() + + # prepare the user wallet with 3 watch only addresses + wo1 = usernode.getnewaddress() + wo2 = usernode.getnewaddress() + wo3 = usernode.getnewaddress() + + usernode.createwallet(wallet_name='wo', disable_private_keys=True) + wo_wallet = usernode.get_wallet_rpc('wo') + + wo_wallet.importaddress(wo1) + wo_wallet.importaddress(wo2) + wo_wallet.importaddress(wo3) + + self.log.info('Start transactions') + + # check blockcount + assert_equal(minernode.getblockcount(), 200) + + # generate some btc to create transactions and check blockcount + initial_mine = COINBASE_MATURITY + 1 + self.generatetoaddress(minernode, initial_mine, m1) + assert_equal(minernode.getblockcount(), initial_mine + 200) + + # synchronize nodes and time + self.sync_all() + minernode.setmocktime(cur_time + ten_days) + usernode.setmocktime(cur_time + ten_days) + restorenode.setmocktime(cur_time + ten_days) + # send 10 btc to user's first watch-only address + self.log.info('Send 10 btc to user') + miner_wallet.sendtoaddress(wo1, 10) + + # generate blocks and check blockcount + self.generatetoaddress(minernode, COINBASE_MATURITY, m1) + assert_equal(minernode.getblockcount(), initial_mine + 300) + + # synchronize nodes and time + self.sync_all() + minernode.setmocktime(cur_time + ten_days + ten_days) + usernode.setmocktime(cur_time + ten_days + ten_days) + restorenode.setmocktime(cur_time + ten_days + ten_days) + # send 5 btc to our second watch-only address + self.log.info('Send 5 btc to user') + miner_wallet.sendtoaddress(wo2, 5) + + # generate blocks and check blockcount + self.generatetoaddress(minernode, COINBASE_MATURITY, m1) + assert_equal(minernode.getblockcount(), initial_mine + 400) + + # synchronize nodes and time + self.sync_all() + minernode.setmocktime(cur_time + ten_days + ten_days + ten_days) + usernode.setmocktime(cur_time + ten_days + ten_days + ten_days) + restorenode.setmocktime(cur_time + ten_days + ten_days + ten_days) + # send 1 btc to our third watch-only address + self.log.info('Send 1 btc to user') + miner_wallet.sendtoaddress(wo3, 1) + + # generate more blocks and check blockcount + self.generatetoaddress(minernode, COINBASE_MATURITY, m1) + assert_equal(minernode.getblockcount(), initial_mine + 500) + + self.log.info('Check user\'s final balance and transaction count') + assert_equal(wo_wallet.getbalance(), 16) + assert_equal(len(wo_wallet.listtransactions()), 3) + + self.log.info('Check transaction times') + for tx in wo_wallet.listtransactions(): + if tx['address'] == wo1: + assert_equal(tx['blocktime'], cur_time + ten_days) + assert_equal(tx['time'], cur_time + ten_days) + elif tx['address'] == wo2: + assert_equal(tx['blocktime'], cur_time + ten_days + ten_days) + assert_equal(tx['time'], cur_time + ten_days + ten_days) + elif tx['address'] == wo3: + assert_equal(tx['blocktime'], cur_time + ten_days + ten_days + ten_days) + assert_equal(tx['time'], cur_time + ten_days + ten_days + ten_days) + + # restore user wallet without rescan + self.log.info('Restore user wallet on another node without rescan') + restorenode.createwallet(wallet_name='wo', disable_private_keys=True) + restorewo_wallet = restorenode.get_wallet_rpc('wo') + + restorewo_wallet.importaddress(wo1, rescan=False) + restorewo_wallet.importaddress(wo2, rescan=False) + restorewo_wallet.importaddress(wo3, rescan=False) + + # check user has 0 balance and no transactions + assert_equal(restorewo_wallet.getbalance(), 0) + assert_equal(len(restorewo_wallet.listtransactions()), 0) + + # proceed to rescan, first with an incomplete one, then with a full rescan + self.log.info('Rescan last history part') + restorewo_wallet.rescanblockchain(initial_mine + 350) + self.log.info('Rescan all history') + restorewo_wallet.rescanblockchain() + + self.log.info('Check user\'s final balance and transaction count after restoration') + assert_equal(restorewo_wallet.getbalance(), 16) + assert_equal(len(restorewo_wallet.listtransactions()), 3) + + self.log.info('Check transaction times after restoration') + for tx in restorewo_wallet.listtransactions(): + if tx['address'] == wo1: + assert_equal(tx['blocktime'], cur_time + ten_days) + assert_equal(tx['time'], cur_time + ten_days) + elif tx['address'] == wo2: + assert_equal(tx['blocktime'], cur_time + ten_days + ten_days) + assert_equal(tx['time'], cur_time + ten_days + ten_days) + elif tx['address'] == wo3: + assert_equal(tx['blocktime'], cur_time + ten_days + ten_days + ten_days) + assert_equal(tx['time'], cur_time + ten_days + ten_days + ten_days) + + +if __name__ == '__main__': + TransactionTimeRescanTest().main() diff --git a/test/functional/wallet_txn_clone.py b/test/functional/wallet_txn_clone.py index 3eb525a9bc..7f178d7d46 100755 --- a/test/functional/wallet_txn_clone.py +++ b/test/functional/wallet_txn_clone.py @@ -7,6 +7,7 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + find_vout_for_address ) from test_framework.messages import ( COIN, @@ -33,6 +34,13 @@ class TxnMallTest(BitcoinTestFramework): super().setup_network() self.disconnect_nodes(1, 2) + def spend_txid(self, txid, vout, outputs): + inputs = [{"txid": txid, "vout": vout}] + tx = self.nodes[0].createrawtransaction(inputs, outputs) + tx = self.nodes[0].fundrawtransaction(tx) + tx = self.nodes[0].signrawtransactionwithwallet(tx['hex']) + return self.nodes[0].sendrawtransaction(tx['hex']) + def run_test(self): if self.options.segwit: output_type = "p2sh-segwit" @@ -49,6 +57,7 @@ class TxnMallTest(BitcoinTestFramework): node0_address1 = self.nodes[0].getnewaddress(address_type=output_type) node0_txid1 = self.nodes[0].sendtoaddress(node0_address1, 1219) node0_tx1 = self.nodes[0].gettransaction(node0_txid1) + self.nodes[0].lockunspent(False, [{"txid":node0_txid1, "vout": find_vout_for_address(self.nodes[0], node0_txid1, node0_address1)}]) node0_address2 = self.nodes[0].getnewaddress(address_type=output_type) node0_txid2 = self.nodes[0].sendtoaddress(node0_address2, 29) @@ -61,8 +70,8 @@ class TxnMallTest(BitcoinTestFramework): node1_address = self.nodes[1].getnewaddress() # Send tx1, and another transaction tx2 that won't be cloned - txid1 = self.nodes[0].sendtoaddress(node1_address, 40) - txid2 = self.nodes[0].sendtoaddress(node1_address, 20) + txid1 = self.spend_txid(node0_txid1, find_vout_for_address(self.nodes[0], node0_txid1, node0_address1), {node1_address: 40}) + txid2 = self.spend_txid(node0_txid2, find_vout_for_address(self.nodes[0], node0_txid2, node0_address2), {node1_address: 20}) # Construct a clone of tx1, to be malleated rawtx1 = self.nodes[0].getrawtransaction(txid1, 1) diff --git a/test/functional/wallet_txn_doublespend.py b/test/functional/wallet_txn_doublespend.py index bfa171d913..150e4083b9 100755 --- a/test/functional/wallet_txn_doublespend.py +++ b/test/functional/wallet_txn_doublespend.py @@ -9,6 +9,7 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, find_output, + find_vout_for_address ) @@ -29,6 +30,13 @@ class TxnMallTest(BitcoinTestFramework): super().setup_network() self.disconnect_nodes(1, 2) + def spend_txid(self, txid, vout, outputs): + inputs = [{"txid": txid, "vout": vout}] + tx = self.nodes[0].createrawtransaction(inputs, outputs) + tx = self.nodes[0].fundrawtransaction(tx) + tx = self.nodes[0].signrawtransactionwithwallet(tx['hex']) + return self.nodes[0].sendrawtransaction(tx['hex']) + def run_test(self): # All nodes should start with 1,250 BTC: starting_balance = 1250 @@ -47,6 +55,7 @@ class TxnMallTest(BitcoinTestFramework): node0_address_foo = self.nodes[0].getnewaddress() fund_foo_txid = self.nodes[0].sendtoaddress(node0_address_foo, 1219) fund_foo_tx = self.nodes[0].gettransaction(fund_foo_txid) + self.nodes[0].lockunspent(False, [{"txid":fund_foo_txid, "vout": find_vout_for_address(self.nodes[0], fund_foo_txid, node0_address_foo)}]) node0_address_bar = self.nodes[0].getnewaddress() fund_bar_txid = self.nodes[0].sendtoaddress(node0_address_bar, 29) @@ -77,8 +86,8 @@ class TxnMallTest(BitcoinTestFramework): assert_equal(doublespend["complete"], True) # Create two spends using 1 50 BTC coin each - txid1 = self.nodes[0].sendtoaddress(node1_address, 40) - txid2 = self.nodes[0].sendtoaddress(node1_address, 20) + txid1 = self.spend_txid(fund_foo_txid, find_vout_for_address(self.nodes[0], fund_foo_txid, node0_address_foo), {node1_address: 40}) + txid2 = self.spend_txid(fund_bar_txid, find_vout_for_address(self.nodes[0], fund_bar_txid, node0_address_bar), {node1_address: 20}) # Have node0 mine a block: if (self.options.mine_block): diff --git a/test/functional/wallet_upgradewallet.py b/test/functional/wallet_upgradewallet.py index ed98db55c9..5800880830 100755 --- a/test/functional/wallet_upgradewallet.py +++ b/test/functional/wallet_upgradewallet.py @@ -234,18 +234,13 @@ class UpgradeWalletTest(BitcoinTestFramework): assert_equal(1, hd_chain_version) seed_id = bytearray(seed_id) seed_id.reverse() - old_kvs = new_kvs - # First 2 keys should still be non-HD - for i in range(0, 2): - info = wallet.getaddressinfo(wallet.getnewaddress()) - assert 'hdkeypath' not in info - assert 'hdseedid' not in info - # Next key should be HD + + # New keys (including change) should be HD (the two old keys have been flushed) info = wallet.getaddressinfo(wallet.getnewaddress()) assert_equal(seed_id.hex(), info['hdseedid']) assert_equal('m/0\'/0\'/0\'', info['hdkeypath']) prev_seed_id = info['hdseedid'] - # Change key should be the same keypool + # Change key should be HD and from the same keypool info = wallet.getaddressinfo(wallet.getrawchangeaddress()) assert_equal(prev_seed_id, info['hdseedid']) assert_equal('m/0\'/0\'/1\'', info['hdkeypath']) @@ -291,14 +286,7 @@ class UpgradeWalletTest(BitcoinTestFramework): hd_chain_version, external_counter, seed_id, internal_counter = struct.unpack('<iI20sI', hd_chain) assert_equal(2, hd_chain_version) assert_equal(2, internal_counter) - # Drain the keypool by fetching one external key and one change key. Should still be the same keypool - info = wallet.getaddressinfo(wallet.getnewaddress()) - assert 'hdseedid' not in info - assert 'hdkeypath' not in info - info = wallet.getaddressinfo(wallet.getrawchangeaddress()) - assert 'hdseedid' not in info - assert 'hdkeypath' not in info - # The next addresses are HD and should be on different HD chains + # The next addresses are HD and should be on different HD chains (the one remaining key in each pool should have been flushed) info = wallet.getaddressinfo(wallet.getnewaddress()) ext_id = info['hdseedid'] assert_equal('m/0\'/0\'/0\'', info['hdkeypath']) diff --git a/test/get_previous_releases.py b/test/get_previous_releases.py index e92bb402b5..62fcad04b3 100755 --- a/test/get_previous_releases.py +++ b/test/get_previous_releases.py @@ -190,6 +190,7 @@ def check_host(args) -> int: 'aarch64-*-linux*': 'aarch64-linux-gnu', 'x86_64-*-linux*': 'x86_64-linux-gnu', 'x86_64-apple-darwin*': 'osx64', + 'aarch64-apple-darwin*': 'osx64', } args.platform = '' for pattern, target in platforms.items(): diff --git a/test/lint/README.md b/test/lint/README.md index 7e06308347..c4d76eac94 100644 --- a/test/lint/README.md +++ b/test/lint/README.md @@ -27,10 +27,10 @@ Usage: test/lint/git-subtree-check.sh [-r] DIR [COMMIT] To do a full check with `-r`, make sure that you have fetched the upstream repository branch in which the subtree is maintained: * for `src/secp256k1`: https://github.com/bitcoin-core/secp256k1.git (branch master) -* for `src/leveldb`: https://github.com/bitcoin-core/leveldb.git (branch bitcoin-fork) -* for `src/univalue`: https://github.com/bitcoin-core/univalue.git (branch master) +* for `src/leveldb`: https://github.com/bitcoin-core/leveldb-subtree.git (branch bitcoin-fork) +* for `src/univalue`: https://github.com/bitcoin-core/univalue-subtree.git (branch master) * for `src/crypto/ctaes`: https://github.com/bitcoin-core/ctaes.git (branch master) -* for `src/crc32c`: https://github.com/google/crc32c.git (branch master) +* for `src/crc32c`: https://github.com/bitcoin-core/crc32c-subtree.git (branch bitcoin-fork) To do so, add the upstream repository as remote: diff --git a/test/lint/lint-locale-dependence.sh b/test/lint/lint-locale-dependence.sh index d6312270e7..b119cffec8 100755 --- a/test/lint/lint-locale-dependence.sh +++ b/test/lint/lint-locale-dependence.sh @@ -37,23 +37,15 @@ export LC_ALL=C # See https://doc.qt.io/qt-5/qcoreapplication.html#locale-settings and # https://stackoverflow.com/a/34878283 for more details. +# TODO: Reduce KNOWN_VIOLATIONS by replacing uses of locale dependent stoul/strtol with locale +# independent ToIntegral<T>(...) or the ParseInt*() functions. +# TODO: Reduce KNOWN_VIOLATIONS by replacing uses of locale dependent snprintf with strprintf. KNOWN_VIOLATIONS=( - "src/bitcoin-tx.cpp.*stoul" - "src/dbwrapper.cpp.*stoul" "src/dbwrapper.cpp:.*vsnprintf" - "src/node/blockstorage.cpp:.*atoi" - "src/qt/rpcconsole.cpp:.*atoi" - "src/rest.cpp:.*strtol" "src/test/dbwrapper_tests.cpp:.*snprintf" "src/test/fuzz/locale.cpp" - "src/test/fuzz/parse_numbers.cpp:.*atoi" - "src/torcontrol.cpp:.*atoi" + "src/test/fuzz/string.cpp" "src/torcontrol.cpp:.*strtol" - "src/util/strencodings.cpp:.*atoi" - "src/util/strencodings.cpp:.*strtol" - "src/util/strencodings.cpp:.*strtoul" - "src/util/strencodings.h:.*atoi" - "src/util/system.cpp:.*atoi" ) REGEXP_IGNORE_EXTERNAL_DEPENDENCIES="^src/(crypto/ctaes/|leveldb/|secp256k1/|tinyformat.h|univalue/)" diff --git a/test/lint/lint-logs.sh b/test/lint/lint-logs.sh index 2fbb4a38e7..d6c53e8ff3 100755 --- a/test/lint/lint-logs.sh +++ b/test/lint/lint-logs.sh @@ -7,7 +7,7 @@ # Check that all logs are terminated with '\n' # # Some logs are continued over multiple lines. They should be explicitly -# commented with \* Continued *\ +# commented with /* Continued */ # # There are some instances of LogPrintf() in comments. Those can be # ignored diff --git a/test/lint/lint-python.sh b/test/lint/lint-python.sh index c448fa6f9a..3d22407fd1 100755 --- a/test/lint/lint-python.sh +++ b/test/lint/lint-python.sh @@ -102,7 +102,7 @@ if ! PYTHONWARNINGS="ignore" flake8 --ignore=B,C,E,F,I,N,W --select=$(IFS=","; e EXIT_CODE=1 fi -if ! mypy --ignore-missing-imports --show-error-codes $(git ls-files "test/functional/*.py" "contrib/devtools/*.py"); then +if ! mypy --show-error-codes $(git ls-files "test/functional/*.py" "contrib/devtools/*.py"); then EXIT_CODE=1 fi diff --git a/test/sanitizer_suppressions/ubsan b/test/sanitizer_suppressions/ubsan index 63e7c57ddb..1d608b9ec1 100644 --- a/test/sanitizer_suppressions/ubsan +++ b/test/sanitizer_suppressions/ubsan @@ -5,8 +5,6 @@ # names can be used. # See https://github.com/google/sanitizers/issues/1364 signed-integer-overflow:txmempool.cpp -# nLastSuccess read from peers.dat might cause an overflow in IsTerrible -signed-integer-overflow:addrman.cpp # https://github.com/bitcoin/bitcoin/pull/21798#issuecomment-829180719 signed-integer-overflow:policy/feerate.cpp @@ -24,7 +22,7 @@ unsigned-integer-overflow:arith_uint256.h unsigned-integer-overflow:basic_string.h unsigned-integer-overflow:bench/bench.h unsigned-integer-overflow:bitcoin-tx.cpp -unsigned-integer-overflow:bloom.cpp +unsigned-integer-overflow:common/bloom.cpp unsigned-integer-overflow:chain.cpp unsigned-integer-overflow:chain.h unsigned-integer-overflow:coded_stream.h @@ -50,7 +48,7 @@ implicit-integer-sign-change:*/new_allocator.h implicit-integer-sign-change:addrman.h implicit-integer-sign-change:arith_uint256.cpp implicit-integer-sign-change:bech32.cpp -implicit-integer-sign-change:bloom.cpp +implicit-integer-sign-change:common/bloom.cpp implicit-integer-sign-change:chain.cpp implicit-integer-sign-change:chain.h implicit-integer-sign-change:coins.h diff --git a/test/util/data/bitcoin-util-test.json b/test/util/data/bitcoin-util-test.json index a648c0287a..cca5732aa1 100644 --- a/test/util/data/bitcoin-util-test.json +++ b/test/util/data/bitcoin-util-test.json @@ -295,6 +295,12 @@ "description": "Create a new transaction with a single output script (OP_DROP) in a P2SH, wrapped in a P2SH (output as json)" }, { "exec": "./bitcoin-tx", + "args": ["-create", "outscript=0:999999999999999999999999999999"], + "return_code": 1, + "error_txt": "error: script parse error: decimal numeric value only allowed in the range -0xFFFFFFFF...0xFFFFFFFF", + "description": "Try to parse an output script with a decimal number above the allowed range" + }, + { "exec": "./bitcoin-tx", "args": ["-create", "outscript=0:9999999999"], "return_code": 1, "error_txt": "error: script parse error: decimal numeric value only allowed in the range -0xFFFFFFFF...0xFFFFFFFF", @@ -512,6 +518,30 @@ { "exec": "./bitcoin-tx", "args": ["-create", + "in=5897de6bd6027a475eadd57019d4e6872c396d0716c4875a5f1a6fcfdf385c1f:0:11aa"], + "return_code": 1, + "error_txt": "error: invalid TX sequence id '11aa'", + "description": "Try to parse a sequence number outside the allowed range" + }, + { "exec": "./bitcoin-tx", + "args": + ["-create", + "in=5897de6bd6027a475eadd57019d4e6872c396d0716c4875a5f1a6fcfdf385c1f:0:-1"], + "return_code": 1, + "error_txt": "error: invalid TX sequence id '-1'", + "description": "Try to parse a sequence number outside the allowed range" + }, + { "exec": "./bitcoin-tx", + "args": + ["-create", + "in=5897de6bd6027a475eadd57019d4e6872c396d0716c4875a5f1a6fcfdf385c1f:0:4294967296"], + "return_code": 1, + "error_txt": "error: invalid TX sequence id '4294967296'", + "description": "Try to parse a sequence number outside the allowed range" + }, + { "exec": "./bitcoin-tx", + "args": + ["-create", "in=5897de6bd6027a475eadd57019d4e6872c396d0716c4875a5f1a6fcfdf385c1f:0:4294967293", "outaddr=0.18:13tuJJDR2RgArmgfv6JScSdreahzgc4T6o"], "output_cmp": "txcreatedata_seq0.hex", @@ -519,6 +549,14 @@ }, { "exec": "./bitcoin-tx", "args": + ["-create", + "in=5897de6bd6027a475eadd57019d4e6872c396d0716c4875a5f1a6fcfdf385c1f:0: 4294967293 ", + "outaddr=0.18:13tuJJDR2RgArmgfv6JScSdreahzgc4T6o"], + "output_cmp": "txcreatedata_seq0.hex", + "description": "Creates a new transaction with one input with sequence number (+whitespace) and one address output" + }, + { "exec": "./bitcoin-tx", + "args": ["-json", "-create", "in=5897de6bd6027a475eadd57019d4e6872c396d0716c4875a5f1a6fcfdf385c1f:0:4294967293", @@ -542,14 +580,26 @@ "description": "Adds a new input with sequence number to a transaction (output in json)" }, { "exec": "./bitcoin-tx", + "args": ["-create", "outmultisig=1:-2:3:02a5:021:02df", "nversion=1"], + "return_code": 1, + "error_txt": "error: invalid multisig required number '-2'", + "description": "Try to parse a multisig number outside the allowed range" + }, + { "exec": "./bitcoin-tx", + "args": ["-create", "outmultisig=1:2:3a:02a5:021:02df", "nversion=1"], + "return_code": 1, + "error_txt": "error: invalid multisig total number '3a'", + "description": "Try to parse a multisig number outside the allowed range" + }, + { "exec": "./bitcoin-tx", "args": ["-create", "outmultisig=1:2:3:02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397:021ac43c7ff740014c3b33737ede99c967e4764553d1b2b83db77c83b8715fa72d:02df2089105c77f266fa11a9d33f05c735234075f2e8780824c6b709415f9fb485", "nversion=1"], "output_cmp": "txcreatemultisig1.hex", "description": "Creates a new transaction with a single 2-of-3 multisig output" }, { "exec": "./bitcoin-tx", - "args": ["-json", "-create", "outmultisig=1:2:3:02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397:021ac43c7ff740014c3b33737ede99c967e4764553d1b2b83db77c83b8715fa72d:02df2089105c77f266fa11a9d33f05c735234075f2e8780824c6b709415f9fb485", "nversion=1"], + "args": ["-json", "-create", "outmultisig=1: 2 : 3 :02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397:021ac43c7ff740014c3b33737ede99c967e4764553d1b2b83db77c83b8715fa72d:02df2089105c77f266fa11a9d33f05c735234075f2e8780824c6b709415f9fb485", "nversion=1"], "output_cmp": "txcreatemultisig1.json", - "description": "Creates a new transaction with a single 2-of-3 multisig output (output in json)" + "description": "Creates a new transaction with a single 2-of-3 multisig output (with whitespace, output in json)" }, { "exec": "./bitcoin-tx", "args": ["-create", "outmultisig=1:2:3:02a5613bd857b7048924264d1e70e08fb2a7e6527d32b7ab1bb993ac59964ff397:021ac43c7ff740014c3b33737ede99c967e4764553d1b2b83db77c83b8715fa72d:02df2089105c77f266fa11a9d33f05c735234075f2e8780824c6b709415f9fb485:S", "nversion=1"], |